Merge pull request #824 from graphql-python/feature/async-relay

Abstract thenables (promise, coroutine) out of relay
This commit is contained in:
Syrus Akbary 2018-08-31 20:51:48 +02:00 committed by GitHub
commit 563ef221d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 292 additions and 30 deletions

View File

@ -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

View File

@ -3,11 +3,11 @@ from collections import Iterable, OrderedDict
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
@ -139,10 +139,7 @@ class IterableConnectionField(Field):
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)

View File

@ -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):
@ -69,7 +68,4 @@ class ClientIDMutation(Mutation):
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)

View File

@ -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)

View File

@ -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()

View File

@ -50,6 +50,7 @@ tests_require = [
"pytest-mock",
"snapshottest",
"coveralls",
"promise",
"six",
"mock",
"pytz",
@ -84,7 +85,6 @@ setup(
"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,

View File

@ -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},
}
}

View File

@ -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
tox.ini
View File

@ -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