Skip to content

Commit 563ef22

Browse files
authored
Merge pull request #824 from graphql-python/feature/async-relay
Abstract thenables (promise, coroutine) out of relay
2 parents 5777d85 + 9512528 commit 563ef22

File tree

9 files changed

+292
-30
lines changed

9 files changed

+292
-30
lines changed

.travis.yml

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
language: python
22
matrix:
33
include:
4-
- env: TOXENV=py27
5-
python: 2.7
6-
- env: TOXENV=py34
7-
python: 3.4
8-
- env: TOXENV=py35
9-
python: 3.5
10-
- env: TOXENV=py36
11-
python: 3.6
12-
- env: TOXENV=pypy
13-
python: pypy-5.7.1
14-
- env: TOXENV=pre-commit
15-
python: 3.6
16-
- env: TOXENV=mypy
17-
python: 3.6
4+
- env: TOXENV=py27
5+
python: 2.7
6+
- env: TOXENV=py34
7+
python: 3.4
8+
- env: TOXENV=py35
9+
python: 3.5
10+
- env: TOXENV=py36
11+
python: 3.6
12+
- env: TOXENV=pypy
13+
python: pypy-5.7.1
14+
- env: TOXENV=pre-commit
15+
python: 3.6
16+
- env: TOXENV=mypy
17+
python: 3.6
1818
install:
1919
- pip install coveralls tox
2020
script: tox

graphene/relay/connection.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from functools import partial
44

55
from graphql_relay import connection_from_list
6-
from promise import Promise, is_thenable
76

87
from ..types import Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union
98
from ..types.field import Field
109
from ..types.objecttype import ObjectType, ObjectTypeOptions
10+
from ..utils.thenables import maybe_thenable
1111
from .node import is_node
1212

1313

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

141141
on_resolve = partial(cls.resolve_connection, connection_type, args)
142-
if is_thenable(resolved):
143-
return Promise.resolve(resolved).then(on_resolve)
144-
145-
return on_resolve(resolved)
142+
return maybe_thenable(resolved, on_resolve)
146143

147144
def get_resolver(self, parent_resolver):
148145
resolver = super(IterableConnectionField, self).get_resolver(parent_resolver)

graphene/relay/mutation.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import re
22
from collections import OrderedDict
33

4-
from promise import Promise, is_thenable
5-
64
from ..types import Field, InputObjectType, String
75
from ..types.mutation import Mutation
6+
from ..utils.thenables import maybe_thenable
87

98

109
class ClientIDMutation(Mutation):
@@ -69,7 +68,4 @@ def on_resolve(payload):
6968
return payload
7069

7170
result = cls.mutate_and_get_payload(root, info, **input)
72-
if is_thenable(result):
73-
return Promise.resolve(result).then(on_resolve)
74-
75-
return on_resolve(result)
71+
return maybe_thenable(result, on_resolve)

graphene/utils/thenables.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""
2+
This file is used mainly as a bridge for thenable abstractions.
3+
This includes:
4+
- Promises
5+
- Asyncio Coroutines
6+
"""
7+
8+
try:
9+
from promise import Promise, is_thenable # type: ignore
10+
except ImportError:
11+
12+
class Promise(object): # type: ignore
13+
pass
14+
15+
def is_thenable(obj): # type: ignore
16+
return False
17+
18+
19+
try:
20+
from inspect import isawaitable
21+
from .thenables_asyncio import await_and_execute
22+
except ImportError:
23+
24+
def isawaitable(obj): # type: ignore
25+
return False
26+
27+
28+
def maybe_thenable(obj, on_resolve):
29+
"""
30+
Execute a on_resolve function once the thenable is resolved,
31+
returning the same type of object inputed.
32+
If the object is not thenable, it should return on_resolve(obj)
33+
"""
34+
if isawaitable(obj) and not isinstance(obj, Promise):
35+
return await_and_execute(obj, on_resolve)
36+
37+
if is_thenable(obj):
38+
return Promise.resolve(obj).then(on_resolve)
39+
40+
# If it's not awaitable not a Promise, return
41+
# the function executed over the object
42+
return on_resolve(obj)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def await_and_execute(obj, on_resolve):
2+
async def build_resolve_async():
3+
return on_resolve(await obj)
4+
5+
return build_resolve_async()

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def run_tests(self):
5050
"pytest-mock",
5151
"snapshottest",
5252
"coveralls",
53+
"promise",
5354
"six",
5455
"mock",
5556
"pytz",
@@ -84,7 +85,6 @@ def run_tests(self):
8485
"six>=1.10.0,<2",
8586
"graphql-core>=2.1,<3",
8687
"graphql-relay>=0.4.5,<1",
87-
"promise>=2.1,<3",
8888
"aniso8601>=3,<4",
8989
],
9090
tests_require=tests_require,
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import pytest
2+
3+
from collections import OrderedDict
4+
from graphql.execution.executors.asyncio import AsyncioExecutor
5+
6+
from graphql_relay.utils import base64
7+
8+
from graphene.types import ObjectType, Schema, String
9+
from graphene.relay.connection import Connection, ConnectionField, PageInfo
10+
from graphene.relay.node import Node
11+
12+
letter_chars = ["A", "B", "C", "D", "E"]
13+
14+
15+
class Letter(ObjectType):
16+
class Meta:
17+
interfaces = (Node,)
18+
19+
letter = String()
20+
21+
22+
class LetterConnection(Connection):
23+
class Meta:
24+
node = Letter
25+
26+
27+
class Query(ObjectType):
28+
letters = ConnectionField(LetterConnection)
29+
connection_letters = ConnectionField(LetterConnection)
30+
promise_letters = ConnectionField(LetterConnection)
31+
32+
node = Node.Field()
33+
34+
def resolve_letters(self, info, **args):
35+
return list(letters.values())
36+
37+
async def resolve_promise_letters(self, info, **args):
38+
return list(letters.values())
39+
40+
def resolve_connection_letters(self, info, **args):
41+
return LetterConnection(
42+
page_info=PageInfo(has_next_page=True, has_previous_page=False),
43+
edges=[
44+
LetterConnection.Edge(node=Letter(id=0, letter="A"), cursor="a-cursor")
45+
],
46+
)
47+
48+
49+
schema = Schema(Query)
50+
51+
letters = OrderedDict()
52+
for i, letter in enumerate(letter_chars):
53+
letters[letter] = Letter(id=i, letter=letter)
54+
55+
56+
def edges(selected_letters):
57+
return [
58+
{
59+
"node": {"id": base64("Letter:%s" % l.id), "letter": l.letter},
60+
"cursor": base64("arrayconnection:%s" % l.id),
61+
}
62+
for l in [letters[i] for i in selected_letters]
63+
]
64+
65+
66+
def cursor_for(ltr):
67+
letter = letters[ltr]
68+
return base64("arrayconnection:%s" % letter.id)
69+
70+
71+
def execute(args=""):
72+
if args:
73+
args = "(" + args + ")"
74+
75+
return schema.execute(
76+
"""
77+
{
78+
letters%s {
79+
edges {
80+
node {
81+
id
82+
letter
83+
}
84+
cursor
85+
}
86+
pageInfo {
87+
hasPreviousPage
88+
hasNextPage
89+
startCursor
90+
endCursor
91+
}
92+
}
93+
}
94+
"""
95+
% args
96+
)
97+
98+
99+
@pytest.mark.asyncio
100+
async def test_connection_promise():
101+
result = await schema.execute(
102+
"""
103+
{
104+
promiseLetters(first:1) {
105+
edges {
106+
node {
107+
id
108+
letter
109+
}
110+
}
111+
pageInfo {
112+
hasPreviousPage
113+
hasNextPage
114+
}
115+
}
116+
}
117+
""",
118+
executor=AsyncioExecutor(),
119+
return_promise=True,
120+
)
121+
122+
assert not result.errors
123+
assert result.data == {
124+
"promiseLetters": {
125+
"edges": [{"node": {"id": "TGV0dGVyOjA=", "letter": "A"}}],
126+
"pageInfo": {"hasPreviousPage": False, "hasNextPage": True},
127+
}
128+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import pytest
2+
from graphql.execution.executors.asyncio import AsyncioExecutor
3+
4+
from graphene.types import ID, Field, ObjectType, Schema
5+
from graphene.types.scalars import String
6+
from graphene.relay.mutation import ClientIDMutation
7+
8+
9+
class SharedFields(object):
10+
shared = String()
11+
12+
13+
class MyNode(ObjectType):
14+
# class Meta:
15+
# interfaces = (Node, )
16+
id = ID()
17+
name = String()
18+
19+
20+
class SaySomethingAsync(ClientIDMutation):
21+
class Input:
22+
what = String()
23+
24+
phrase = String()
25+
26+
@staticmethod
27+
async def mutate_and_get_payload(self, info, what, client_mutation_id=None):
28+
return SaySomethingAsync(phrase=str(what))
29+
30+
31+
# MyEdge = MyNode.Connection.Edge
32+
class MyEdge(ObjectType):
33+
node = Field(MyNode)
34+
cursor = String()
35+
36+
37+
class OtherMutation(ClientIDMutation):
38+
class Input(SharedFields):
39+
additional_field = String()
40+
41+
name = String()
42+
my_node_edge = Field(MyEdge)
43+
44+
@staticmethod
45+
def mutate_and_get_payload(
46+
self, info, shared="", additional_field="", client_mutation_id=None
47+
):
48+
edge_type = MyEdge
49+
return OtherMutation(
50+
name=shared + additional_field,
51+
my_node_edge=edge_type(cursor="1", node=MyNode(name="name")),
52+
)
53+
54+
55+
class RootQuery(ObjectType):
56+
something = String()
57+
58+
59+
class Mutation(ObjectType):
60+
say_promise = SaySomethingAsync.Field()
61+
other = OtherMutation.Field()
62+
63+
64+
schema = Schema(query=RootQuery, mutation=Mutation)
65+
66+
67+
@pytest.mark.asyncio
68+
async def test_node_query_promise():
69+
executed = await schema.execute(
70+
'mutation a { sayPromise(input: {what:"hello", clientMutationId:"1"}) { phrase } }',
71+
executor=AsyncioExecutor(),
72+
return_promise=True,
73+
)
74+
assert not executed.errors
75+
assert executed.data == {"sayPromise": {"phrase": "hello"}}
76+
77+
78+
@pytest.mark.asyncio
79+
async def test_edge_query():
80+
executed = await schema.execute(
81+
'mutation a { other(input: {clientMutationId:"1"}) { clientMutationId, myNodeEdge { cursor node { name }} } }',
82+
executor=AsyncioExecutor(),
83+
return_promise=True,
84+
)
85+
assert not executed.errors
86+
assert dict(executed.data) == {
87+
"other": {
88+
"clientMutationId": "1",
89+
"myNodeEdge": {"cursor": "1", "node": {"name": "name"}},
90+
}
91+
}

tox.ini

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
[tox]
2-
envlist = flake8,py27,py33,py34,py35,py36,pre-commit,pypy,mypy
2+
envlist = flake8,py27,py34,py35,py36,py37,pre-commit,pypy,mypy
33
skipsdist = true
44

55
[testenv]
6-
deps = .[test]
6+
deps =
7+
.[test]
8+
py{35,36,37}: pytest-asyncio
79
setenv =
810
PYTHONPATH = .:{envdir}
9-
commands=
10-
py.test --cov=graphene graphene examples
11+
commands =
12+
py{27,34,py}: py.test --cov=graphene graphene examples {posargs}
13+
py{35,36,37}: py.test --cov=graphene graphene examples tests_asyncio {posargs}
1114

1215
[testenv:pre-commit]
1316
basepython=python3.6

0 commit comments

Comments
 (0)