mirror of
https://github.com/graphql-python/graphene.git
synced 2025-02-02 12:44:15 +03:00
Abstract thenables (promise, coroutine) out of relay Connections and Mutations
This commit is contained in:
parent
5777d85f99
commit
3e5319cf70
30
.travis.yml
30
.travis.yml
|
@ -1,20 +1,22 @@
|
||||||
language: python
|
language: python
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- env: TOXENV=py27
|
- env: TOXENV=py27
|
||||||
python: 2.7
|
python: 2.7
|
||||||
- env: TOXENV=py34
|
- env: TOXENV=py34
|
||||||
python: 3.4
|
python: 3.4
|
||||||
- env: TOXENV=py35
|
- env: TOXENV=py35
|
||||||
python: 3.5
|
python: 3.5
|
||||||
- env: TOXENV=py36
|
- env: TOXENV=py36
|
||||||
python: 3.6
|
python: 3.6
|
||||||
- env: TOXENV=pypy
|
- env: TOXENV=py37
|
||||||
python: pypy-5.7.1
|
python: 3.7
|
||||||
- env: TOXENV=pre-commit
|
- env: TOXENV=pypy
|
||||||
python: 3.6
|
python: pypy-5.7.1
|
||||||
- env: TOXENV=mypy
|
- env: TOXENV=pre-commit
|
||||||
python: 3.6
|
python: 3.6
|
||||||
|
- env: TOXENV=mypy
|
||||||
|
python: 3.6
|
||||||
install:
|
install:
|
||||||
- pip install coveralls tox
|
- pip install coveralls tox
|
||||||
script: tox
|
script: tox
|
||||||
|
|
|
@ -3,11 +3,11 @@ from collections import Iterable, OrderedDict
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from graphql_relay import connection_from_list
|
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 import Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union
|
||||||
from ..types.field import Field
|
from ..types.field import Field
|
||||||
from ..types.objecttype import ObjectType, ObjectTypeOptions
|
from ..types.objecttype import ObjectType, ObjectTypeOptions
|
||||||
|
from ..utils.thenables import maybe_thenable
|
||||||
from .node import is_node
|
from .node import is_node
|
||||||
|
|
||||||
|
|
||||||
|
@ -139,10 +139,7 @@ class IterableConnectionField(Field):
|
||||||
connection_type = connection_type.of_type
|
connection_type = connection_type.of_type
|
||||||
|
|
||||||
on_resolve = partial(cls.resolve_connection, connection_type, args)
|
on_resolve = partial(cls.resolve_connection, connection_type, args)
|
||||||
if is_thenable(resolved):
|
return maybe_thenable(resolved, on_resolve)
|
||||||
return Promise.resolve(resolved).then(on_resolve)
|
|
||||||
|
|
||||||
return on_resolve(resolved)
|
|
||||||
|
|
||||||
def get_resolver(self, parent_resolver):
|
def get_resolver(self, parent_resolver):
|
||||||
resolver = super(IterableConnectionField, self).get_resolver(parent_resolver)
|
resolver = super(IterableConnectionField, self).get_resolver(parent_resolver)
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import re
|
import re
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from promise import Promise, is_thenable
|
|
||||||
|
|
||||||
from ..types import Field, InputObjectType, String
|
from ..types import Field, InputObjectType, String
|
||||||
from ..types.mutation import Mutation
|
from ..types.mutation import Mutation
|
||||||
|
from ..utils.thenables import maybe_thenable
|
||||||
|
|
||||||
|
|
||||||
class ClientIDMutation(Mutation):
|
class ClientIDMutation(Mutation):
|
||||||
|
@ -69,7 +68,4 @@ class ClientIDMutation(Mutation):
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
result = cls.mutate_and_get_payload(root, info, **input)
|
result = cls.mutate_and_get_payload(root, info, **input)
|
||||||
if is_thenable(result):
|
return maybe_thenable(result, on_resolve)
|
||||||
return Promise.resolve(result).then(on_resolve)
|
|
||||||
|
|
||||||
return on_resolve(result)
|
|
||||||
|
|
39
graphene/utils/thenables.py
Normal file
39
graphene/utils/thenables.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
"""
|
||||||
|
This file is used mainly as a bridge for thenable abstractions.
|
||||||
|
This includes:
|
||||||
|
- Promises
|
||||||
|
- Asyncio Coroutines
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from promise import Promise, is_thenable
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
def is_thenable(obj):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from inspect import isawaitable
|
||||||
|
from .thenables_asyncio import await_and_execute
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
def isawaitable(obj):
|
||||||
|
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):
|
||||||
|
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)
|
6
graphene/utils/thenables_asyncio.py
Normal file
6
graphene/utils/thenables_asyncio.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
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",
|
"pytest-mock",
|
||||||
"snapshottest",
|
"snapshottest",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
|
"promise",
|
||||||
"six",
|
"six",
|
||||||
"mock",
|
"mock",
|
||||||
"pytz",
|
"pytz",
|
||||||
|
@ -84,7 +85,6 @@ setup(
|
||||||
"six>=1.10.0,<2",
|
"six>=1.10.0,<2",
|
||||||
"graphql-core>=2.1,<3",
|
"graphql-core>=2.1,<3",
|
||||||
"graphql-relay>=0.4.5,<1",
|
"graphql-relay>=0.4.5,<1",
|
||||||
"promise>=2.1,<3",
|
|
||||||
"aniso8601>=3,<4",
|
"aniso8601>=3,<4",
|
||||||
],
|
],
|
||||||
tests_require=tests_require,
|
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},
|
||||||
|
}
|
||||||
|
}
|
100
tests_asyncio/test_relay_mutation.py
Normal file
100
tests_asyncio/test_relay_mutation.py
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import pytest
|
||||||
|
from graphql.execution.executors.asyncio import AsyncioExecutor
|
||||||
|
|
||||||
|
from graphene.types import (
|
||||||
|
ID,
|
||||||
|
Argument,
|
||||||
|
Field,
|
||||||
|
InputField,
|
||||||
|
InputObjectType,
|
||||||
|
NonNull,
|
||||||
|
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]
|
[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
|
skipsdist = true
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
deps = .[test]
|
deps =
|
||||||
|
.[test]
|
||||||
|
py{35,36,37}: pytest-asyncio
|
||||||
setenv =
|
setenv =
|
||||||
PYTHONPATH = .:{envdir}
|
PYTHONPATH = .:{envdir}
|
||||||
commands=
|
commands =
|
||||||
py.test --cov=graphene graphene examples
|
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]
|
[testenv:pre-commit]
|
||||||
basepython=python3.6
|
basepython=python3.6
|
||||||
|
|
Loading…
Reference in New Issue
Block a user