Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstract thenables (promise, coroutine) out of relay #824

Merged
merged 3 commits into from
Aug 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
language: python
matrix:
include:
- env: TOXENV=py27
python: 2.7
- env: TOXENV=py34
python: 3.4
- env: TOXENV=py35
python: 3.5
- env: TOXENV=py36
python: 3.6
- env: TOXENV=pypy
python: pypy-5.7.1
- env: TOXENV=pre-commit
python: 3.6
- env: TOXENV=mypy
python: 3.6
- env: TOXENV=py27
python: 2.7
- env: TOXENV=py34
python: 3.4
- env: TOXENV=py35
python: 3.5
- env: TOXENV=py36
python: 3.6
- env: TOXENV=pypy
python: pypy-5.7.1
- env: TOXENV=pre-commit
python: 3.6
- env: TOXENV=mypy
python: 3.6
install:
- pip install coveralls tox
script: tox
Expand Down
7 changes: 2 additions & 5 deletions graphene/relay/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
from functools import partial

from graphql_relay import connection_from_list
from promise import Promise, is_thenable

from ..types import Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union
from ..types.field import Field
from ..types.objecttype import ObjectType, ObjectTypeOptions
from ..utils.thenables import maybe_thenable
from .node import is_node


Expand Down Expand Up @@ -139,10 +139,7 @@ def connection_resolver(cls, resolver, connection_type, root, info, **args):
connection_type = connection_type.of_type

on_resolve = partial(cls.resolve_connection, connection_type, args)
if is_thenable(resolved):
return Promise.resolve(resolved).then(on_resolve)

return on_resolve(resolved)
return maybe_thenable(resolved, on_resolve)

def get_resolver(self, parent_resolver):
resolver = super(IterableConnectionField, self).get_resolver(parent_resolver)
Expand Down
8 changes: 2 additions & 6 deletions graphene/relay/mutation.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import re
from collections import OrderedDict

from promise import Promise, is_thenable

from ..types import Field, InputObjectType, String
from ..types.mutation import Mutation
from ..utils.thenables import maybe_thenable


class ClientIDMutation(Mutation):
Expand Down Expand Up @@ -69,7 +68,4 @@ def on_resolve(payload):
return payload

result = cls.mutate_and_get_payload(root, info, **input)
if is_thenable(result):
return Promise.resolve(result).then(on_resolve)

return on_resolve(result)
return maybe_thenable(result, on_resolve)
42 changes: 42 additions & 0 deletions graphene/utils/thenables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
This file is used mainly as a bridge for thenable abstractions.
This includes:
- Promises
- Asyncio Coroutines
"""

try:
from promise import Promise, is_thenable # type: ignore
except ImportError:

class Promise(object): # type: ignore
pass

def is_thenable(obj): # type: ignore
return False


try:
from inspect import isawaitable
from .thenables_asyncio import await_and_execute
except ImportError:

def isawaitable(obj): # type: ignore
return False


def maybe_thenable(obj, on_resolve):
"""
Execute a on_resolve function once the thenable is resolved,
returning the same type of object inputed.
If the object is not thenable, it should return on_resolve(obj)
"""
if isawaitable(obj) and not isinstance(obj, Promise):
return await_and_execute(obj, on_resolve)

if is_thenable(obj):
return Promise.resolve(obj).then(on_resolve)

# If it's not awaitable not a Promise, return
# the function executed over the object
return on_resolve(obj)
5 changes: 5 additions & 0 deletions graphene/utils/thenables_asyncio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def await_and_execute(obj, on_resolve):
async def build_resolve_async():
return on_resolve(await obj)

return build_resolve_async()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def run_tests(self):
"pytest-mock",
"snapshottest",
"coveralls",
"promise",
"six",
"mock",
"pytz",
Expand Down Expand Up @@ -84,7 +85,6 @@ def run_tests(self):
"six>=1.10.0,<2",
"graphql-core>=2.1,<3",
"graphql-relay>=0.4.5,<1",
"promise>=2.1,<3",
"aniso8601>=3,<4",
],
tests_require=tests_require,
Expand Down
128 changes: 128 additions & 0 deletions tests_asyncio/test_relay_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import pytest

from collections import OrderedDict
from graphql.execution.executors.asyncio import AsyncioExecutor

from graphql_relay.utils import base64

from graphene.types import ObjectType, Schema, String
from graphene.relay.connection import Connection, ConnectionField, PageInfo
from graphene.relay.node import Node

letter_chars = ["A", "B", "C", "D", "E"]


class Letter(ObjectType):
class Meta:
interfaces = (Node,)

letter = String()


class LetterConnection(Connection):
class Meta:
node = Letter


class Query(ObjectType):
letters = ConnectionField(LetterConnection)
connection_letters = ConnectionField(LetterConnection)
promise_letters = ConnectionField(LetterConnection)

node = Node.Field()

def resolve_letters(self, info, **args):
return list(letters.values())

async def resolve_promise_letters(self, info, **args):
return list(letters.values())

def resolve_connection_letters(self, info, **args):
return LetterConnection(
page_info=PageInfo(has_next_page=True, has_previous_page=False),
edges=[
LetterConnection.Edge(node=Letter(id=0, letter="A"), cursor="a-cursor")
],
)


schema = Schema(Query)

letters = OrderedDict()
for i, letter in enumerate(letter_chars):
letters[letter] = Letter(id=i, letter=letter)


def edges(selected_letters):
return [
{
"node": {"id": base64("Letter:%s" % l.id), "letter": l.letter},
"cursor": base64("arrayconnection:%s" % l.id),
}
for l in [letters[i] for i in selected_letters]
]


def cursor_for(ltr):
letter = letters[ltr]
return base64("arrayconnection:%s" % letter.id)


def execute(args=""):
if args:
args = "(" + args + ")"

return schema.execute(
"""
{
letters%s {
edges {
node {
id
letter
}
cursor
}
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
}
}
}
"""
% args
)


@pytest.mark.asyncio
async def test_connection_promise():
result = await schema.execute(
"""
{
promiseLetters(first:1) {
edges {
node {
id
letter
}
}
pageInfo {
hasPreviousPage
hasNextPage
}
}
}
""",
executor=AsyncioExecutor(),
return_promise=True,
)

assert not result.errors
assert result.data == {
"promiseLetters": {
"edges": [{"node": {"id": "TGV0dGVyOjA=", "letter": "A"}}],
"pageInfo": {"hasPreviousPage": False, "hasNextPage": True},
}
}
91 changes: 91 additions & 0 deletions tests_asyncio/test_relay_mutation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import pytest
from graphql.execution.executors.asyncio import AsyncioExecutor

from graphene.types import ID, Field, ObjectType, Schema
from graphene.types.scalars import String
from graphene.relay.mutation import ClientIDMutation


class SharedFields(object):
shared = String()


class MyNode(ObjectType):
# class Meta:
# interfaces = (Node, )
id = ID()
name = String()


class SaySomethingAsync(ClientIDMutation):
class Input:
what = String()

phrase = String()

@staticmethod
async def mutate_and_get_payload(self, info, what, client_mutation_id=None):
return SaySomethingAsync(phrase=str(what))


# MyEdge = MyNode.Connection.Edge
class MyEdge(ObjectType):
node = Field(MyNode)
cursor = String()


class OtherMutation(ClientIDMutation):
class Input(SharedFields):
additional_field = String()

name = String()
my_node_edge = Field(MyEdge)

@staticmethod
def mutate_and_get_payload(
self, info, shared="", additional_field="", client_mutation_id=None
):
edge_type = MyEdge
return OtherMutation(
name=shared + additional_field,
my_node_edge=edge_type(cursor="1", node=MyNode(name="name")),
)


class RootQuery(ObjectType):
something = String()


class Mutation(ObjectType):
say_promise = SaySomethingAsync.Field()
other = OtherMutation.Field()


schema = Schema(query=RootQuery, mutation=Mutation)


@pytest.mark.asyncio
async def test_node_query_promise():
executed = await schema.execute(
'mutation a { sayPromise(input: {what:"hello", clientMutationId:"1"}) { phrase } }',
executor=AsyncioExecutor(),
return_promise=True,
)
assert not executed.errors
assert executed.data == {"sayPromise": {"phrase": "hello"}}


@pytest.mark.asyncio
async def test_edge_query():
executed = await schema.execute(
'mutation a { other(input: {clientMutationId:"1"}) { clientMutationId, myNodeEdge { cursor node { name }} } }',
executor=AsyncioExecutor(),
return_promise=True,
)
assert not executed.errors
assert dict(executed.data) == {
"other": {
"clientMutationId": "1",
"myNodeEdge": {"cursor": "1", "node": {"name": "name"}},
}
}
11 changes: 7 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
[tox]
envlist = flake8,py27,py33,py34,py35,py36,pre-commit,pypy,mypy
envlist = flake8,py27,py34,py35,py36,py37,pre-commit,pypy,mypy
skipsdist = true

[testenv]
deps = .[test]
deps =
.[test]
py{35,36,37}: pytest-asyncio
setenv =
PYTHONPATH = .:{envdir}
commands=
py.test --cov=graphene graphene examples
commands =
py{27,34,py}: py.test --cov=graphene graphene examples {posargs}
py{35,36,37}: py.test --cov=graphene graphene examples tests_asyncio {posargs}

[testenv:pre-commit]
basepython=python3.6
Expand Down