From 3e5319cf70ee5860052388e6ce0073df433b163d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 31 Aug 2018 19:41:20 +0200 Subject: [PATCH 1/3] Abstract thenables (promise, coroutine) out of relay Connections and Mutations --- .travis.yml | 30 +++--- graphene/relay/connection.py | 7 +- graphene/relay/mutation.py | 8 +- graphene/utils/thenables.py | 39 ++++++++ graphene/utils/thenables_asyncio.py | 6 ++ setup.py | 2 +- tests_asyncio/test_relay_connection.py | 128 +++++++++++++++++++++++++ tests_asyncio/test_relay_mutation.py | 100 +++++++++++++++++++ tox.ini | 13 ++- 9 files changed, 302 insertions(+), 31 deletions(-) create mode 100644 graphene/utils/thenables.py create mode 100644 graphene/utils/thenables_asyncio.py create mode 100644 tests_asyncio/test_relay_connection.py create mode 100644 tests_asyncio/test_relay_mutation.py diff --git a/.travis.yml b/.travis.yml index 399ce134..87aea137 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,22 @@ 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=py37 + python: 3.7 + - 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 diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index a942c01b..2782865c 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -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) diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index 1b8b855e..ee758e78 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -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) diff --git a/graphene/utils/thenables.py b/graphene/utils/thenables.py new file mode 100644 index 00000000..c1fab663 --- /dev/null +++ b/graphene/utils/thenables.py @@ -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) diff --git a/graphene/utils/thenables_asyncio.py b/graphene/utils/thenables_asyncio.py new file mode 100644 index 00000000..3c01b1a2 --- /dev/null +++ b/graphene/utils/thenables_asyncio.py @@ -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() + diff --git a/setup.py b/setup.py index be2111f8..d9f62bec 100644 --- a/setup.py +++ b/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, diff --git a/tests_asyncio/test_relay_connection.py b/tests_asyncio/test_relay_connection.py new file mode 100644 index 00000000..ec86fef6 --- /dev/null +++ b/tests_asyncio/test_relay_connection.py @@ -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}, + } + } diff --git a/tests_asyncio/test_relay_mutation.py b/tests_asyncio/test_relay_mutation.py new file mode 100644 index 00000000..2992c8ee --- /dev/null +++ b/tests_asyncio/test_relay_mutation.py @@ -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"}}, + } + } diff --git a/tox.ini b/tox.ini index f8e6f347..d52ea642 100644 --- a/tox.ini +++ b/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 + PYTHONPATH = .:{envdir} +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 From 3d41a500c9a4d52990d65f1c162ce6cb411a9ded Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 31 Aug 2018 20:01:03 +0200 Subject: [PATCH 2/3] Fixed lint & imports --- .travis.yml | 2 -- graphene/utils/thenables.py | 2 +- graphene/utils/thenables_asyncio.py | 1 - tests_asyncio/test_relay_mutation.py | 11 +---------- tox.ini | 4 ++-- 5 files changed, 4 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 87aea137..8cd9e0c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,8 +9,6 @@ matrix: python: 3.5 - env: TOXENV=py36 python: 3.6 - - env: TOXENV=py37 - python: 3.7 - env: TOXENV=pypy python: pypy-5.7.1 - env: TOXENV=pre-commit diff --git a/graphene/utils/thenables.py b/graphene/utils/thenables.py index c1fab663..d18d7e61 100644 --- a/graphene/utils/thenables.py +++ b/graphene/utils/thenables.py @@ -18,7 +18,7 @@ try: from .thenables_asyncio import await_and_execute except ImportError: - def isawaitable(obj): + def isawaitable(obj): # type: ignore return False diff --git a/graphene/utils/thenables_asyncio.py b/graphene/utils/thenables_asyncio.py index 3c01b1a2..d5f93182 100644 --- a/graphene/utils/thenables_asyncio.py +++ b/graphene/utils/thenables_asyncio.py @@ -3,4 +3,3 @@ def await_and_execute(obj, on_resolve): return on_resolve(await obj) return build_resolve_async() - diff --git a/tests_asyncio/test_relay_mutation.py b/tests_asyncio/test_relay_mutation.py index 2992c8ee..42ea5fc7 100644 --- a/tests_asyncio/test_relay_mutation.py +++ b/tests_asyncio/test_relay_mutation.py @@ -1,16 +1,7 @@ import pytest from graphql.execution.executors.asyncio import AsyncioExecutor -from graphene.types import ( - ID, - Argument, - Field, - InputField, - InputObjectType, - NonNull, - ObjectType, - Schema, -) +from graphene.types import ID, Field, ObjectType, Schema from graphene.types.scalars import String from graphene.relay.mutation import ClientIDMutation diff --git a/tox.ini b/tox.ini index d52ea642..2b7ae59c 100644 --- a/tox.ini +++ b/tox.ini @@ -3,11 +3,11 @@ envlist = flake8,py27,py34,py35,py36,py37,pre-commit,pypy,mypy skipsdist = true [testenv] -deps = +deps = .[test] py{35,36,37}: pytest-asyncio setenv = - PYTHONPATH = .:{envdir} + PYTHONPATH = .:{envdir} 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} From 9512528a779983967c76c67739aa6127db6a46f1 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 31 Aug 2018 20:09:29 +0200 Subject: [PATCH 3/3] Fixed async funcs --- graphene/utils/thenables.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/graphene/utils/thenables.py b/graphene/utils/thenables.py index d18d7e61..a3089595 100644 --- a/graphene/utils/thenables.py +++ b/graphene/utils/thenables.py @@ -6,10 +6,13 @@ This includes: """ try: - from promise import Promise, is_thenable + from promise import Promise, is_thenable # type: ignore except ImportError: - def is_thenable(obj): + class Promise(object): # type: ignore + pass + + def is_thenable(obj): # type: ignore return False @@ -28,7 +31,7 @@ def maybe_thenable(obj, on_resolve): returning the same type of object inputed. If the object is not thenable, it should return on_resolve(obj) """ - if isawaitable(obj): + if isawaitable(obj) and not isinstance(obj, Promise): return await_and_execute(obj, on_resolve) if is_thenable(obj):