Skip to content

Commit

Permalink
Merge pull request #824 from graphql-python/feature/async-relay
Browse files Browse the repository at this point in the history
Abstract thenables (promise, coroutine) out of relay
  • Loading branch information
syrusakbary committed Aug 31, 2018
2 parents 5777d85 + 9512528 commit 563ef22
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 30 deletions.
28 changes: 14 additions & 14 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 2 additions & 5 deletions graphene/relay/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
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


Expand Down Expand Up @@ -139,10 +139,7 @@ def connection_resolver(cls, resolver, connection_type, root, info, **args):
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)
Expand Down
8 changes: 2 additions & 6 deletions graphene/relay/mutation.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -69,7 +68,4 @@ def on_resolve(payload):
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 changes: 42 additions & 0 deletions graphene/utils/thenables.py
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions graphene/utils/thenables_asyncio.py
Original file line number Diff line number Diff line change
@@ -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 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def run_tests(self):
"pytest-mock",
"snapshottest",
"coveralls",
"promise",
"six",
"mock",
"pytz",
Expand Down Expand Up @@ -84,7 +85,6 @@ def run_tests(self):
"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,
Expand Down
128 changes: 128 additions & 0 deletions tests_asyncio/test_relay_connection.py
Original file line number Diff line number Diff line change
@@ -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 changes: 91 additions & 0 deletions tests_asyncio/test_relay_mutation.py
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit 563ef22

Please sign in to comment.