Abstract thenables (promise, coroutine) out of relay Connections and Mutations

This commit is contained in:
Syrus Akbary 2018-08-31 19:41:20 +02:00
parent 5777d85f99
commit 3e5319cf70
9 changed files with 302 additions and 31 deletions

View File

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

View File

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

View File

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

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

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

View File

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

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

13
tox.ini
View File

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