mirror of
https://github.com/graphql-python/graphene.git
synced 2024-11-22 17:46:57 +03:00
Merge pull request #824 from graphql-python/feature/async-relay
Abstract thenables (promise, coroutine) out of relay
This commit is contained in:
commit
563ef221d4
28
.travis.yml
28
.travis.yml
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
42
graphene/utils/thenables.py
Normal file
42
graphene/utils/thenables.py
Normal 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)
|
5
graphene/utils/thenables_asyncio.py
Normal file
5
graphene/utils/thenables_asyncio.py
Normal 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()
|
2
setup.py
2
setup.py
|
@ -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,
|
||||
|
|
128
tests_asyncio/test_relay_connection.py
Normal file
128
tests_asyncio/test_relay_connection.py
Normal 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},
|
||||
}
|
||||
}
|
91
tests_asyncio/test_relay_mutation.py
Normal file
91
tests_asyncio/test_relay_mutation.py
Normal 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
11
tox.ini
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user