From b193c98e3572b65064e3e84a194948756ea19ee5 Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Tue, 26 Jul 2016 18:13:45 +0100 Subject: [PATCH 1/9] Allow ConnectionFields to have ObjectTypes as per relay spec --- graphene/relay/__init__.py | 9 ++-- graphene/relay/connection.py | 85 ++++++++++++++++++++++++++++++++ graphene/relay/fields.py | 6 +-- graphene/relay/types.py | 95 ------------------------------------ 4 files changed, 94 insertions(+), 101 deletions(-) create mode 100644 graphene/relay/connection.py diff --git a/graphene/relay/__init__.py b/graphene/relay/__init__.py index 9e169237..343d3fcc 100644 --- a/graphene/relay/__init__.py +++ b/graphene/relay/__init__.py @@ -6,12 +6,15 @@ from .fields import ( from .types import ( Node, - PageInfo, - Edge, - Connection, ClientIDMutation ) +from .connection import ( + PageInfo, + Connection, + Edge, +) + from .utils import is_node __all__ = ['ConnectionField', 'NodeField', 'GlobalIDField', 'Node', diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py new file mode 100644 index 00000000..2c9e3952 --- /dev/null +++ b/graphene/relay/connection.py @@ -0,0 +1,85 @@ +from ..core.classtypes import ObjectType +from ..core.types import Field, Boolean, String, List +from ..utils import memoize + + +class PageInfo(ObjectType): + + def __init__(self, start_cursor="", end_cursor="", + has_previous_page=False, has_next_page=False, **kwargs): + super(PageInfo, self).__init__(**kwargs) + self.startCursor = start_cursor + self.endCursor = end_cursor + self.hasPreviousPage = has_previous_page + self.hasNextPage = has_next_page + + hasNextPage = Boolean( + required=True, + description='When paginating forwards, are there more items?') + hasPreviousPage = Boolean( + required=True, + description='When paginating backwards, are there more items?') + startCursor = String( + description='When paginating backwards, the cursor to continue.') + endCursor = String( + description='When paginating forwards, the cursor to continue.') + +class Edge(ObjectType): + '''An edge in a connection.''' + cursor = String( + required=True, description='A cursor for use in pagination') + + @classmethod + @memoize + def for_node(cls, node): + from graphene.relay.utils import is_node + # assert is_node(node), 'ObjectTypes in a edge have to be Nodes' + node_field = Field(node, description='The item at the end of the edge') + return type( + '%s%s' % (node._meta.type_name, cls._meta.type_name), + (cls,), + {'node_type': node, 'node': node_field}) + + +class Connection(ObjectType): + '''A connection to a list of items.''' + + def __init__(self, edges, page_info, **kwargs): + super(Connection, self).__init__(**kwargs) + self.edges = edges + self.pageInfo = page_info + + class Meta: + type_name = 'DefaultConnection' + + pageInfo = Field(PageInfo, required=True, + description='The Information to aid in pagination') + + _connection_data = None + + @classmethod + @memoize + def for_node(cls, node, edge_type=None): + from graphene.relay.utils import is_node + edge_type = edge_type or Edge.for_node(node) + edges = List(edge_type, description='Information to aid in pagination.') + return type( + '%s%s' % (node._meta.type_name, cls._meta.type_name), + (cls,), + {'edge_type': edge_type, 'edges': edges}) + + @classmethod + def from_list(cls, iterable, args, context, info): + assert isinstance( + iterable, Iterable), 'Resolved value from the connection field have to be iterable' + connection = connection_from_list( + iterable, args, connection_type=cls, + edge_type=cls.edge_type, pageinfo_type=PageInfo) + connection.set_connection_data(iterable) + return connection + + def set_connection_data(self, data): + self._connection_data = data + + def get_connection_data(self): + return self._connection_data diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index e79b9592..e7238f5a 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -6,6 +6,7 @@ from ..core.fields import Field from ..core.types.definitions import NonNull from ..core.types.scalars import ID, Int, String from ..utils.wrap_resolver_function import has_context, with_context +from .connection import Connection, Edge class ConnectionField(Field): @@ -45,19 +46,18 @@ class ConnectionField(Field): return connection_type.from_list(resolved, args, context, info) def get_connection_type(self, node): - connection_type = self.connection_type or node.get_connection_type() + connection_type = self.connection_type or Connection edge_type = self.get_edge_type(node) return connection_type.for_node(node, edge_type=edge_type) def get_edge_type(self, node): - edge_type = self.edge_type or node.get_edge_type() + edge_type = self.edge_type or Edge return edge_type.for_node(node) def get_type(self, schema): from graphene.relay.utils import is_node type = schema.T(self.type) node = schema.objecttype(type) - assert is_node(node), 'Only nodes have connections.' schema.register(node) connection_type = self.get_connection_type(node) diff --git a/graphene/relay/types.py b/graphene/relay/types.py index 3ab55770..e79519b2 100644 --- a/graphene/relay/types.py +++ b/graphene/relay/types.py @@ -19,90 +19,6 @@ from ..utils.wrap_resolver_function import has_context, with_context from .fields import GlobalIDField -class PageInfo(ObjectType): - - def __init__(self, start_cursor="", end_cursor="", - has_previous_page=False, has_next_page=False, **kwargs): - super(PageInfo, self).__init__(**kwargs) - self.startCursor = start_cursor - self.endCursor = end_cursor - self.hasPreviousPage = has_previous_page - self.hasNextPage = has_next_page - - hasNextPage = Boolean( - required=True, - description='When paginating forwards, are there more items?') - hasPreviousPage = Boolean( - required=True, - description='When paginating backwards, are there more items?') - startCursor = String( - description='When paginating backwards, the cursor to continue.') - endCursor = String( - description='When paginating forwards, the cursor to continue.') - - -class Edge(ObjectType): - '''An edge in a connection.''' - cursor = String( - required=True, description='A cursor for use in pagination') - - @classmethod - @memoize - def for_node(cls, node): - from graphene.relay.utils import is_node - assert is_node(node), 'ObjectTypes in a edge have to be Nodes' - node_field = Field(node, description='The item at the end of the edge') - return type( - '%s%s' % (node._meta.type_name, cls._meta.type_name), - (cls,), - {'node_type': node, 'node': node_field}) - - -class Connection(ObjectType): - '''A connection to a list of items.''' - - def __init__(self, edges, page_info, **kwargs): - super(Connection, self).__init__(**kwargs) - self.edges = edges - self.pageInfo = page_info - - class Meta: - type_name = 'DefaultConnection' - - pageInfo = Field(PageInfo, required=True, - description='The Information to aid in pagination') - - _connection_data = None - - @classmethod - @memoize - def for_node(cls, node, edge_type=None): - from graphene.relay.utils import is_node - edge_type = edge_type or Edge.for_node(node) - assert is_node(node), 'ObjectTypes in a connection have to be Nodes' - edges = List(edge_type, description='Information to aid in pagination.') - return type( - '%s%s' % (node._meta.type_name, cls._meta.type_name), - (cls,), - {'edge_type': edge_type, 'edges': edges}) - - @classmethod - def from_list(cls, iterable, args, context, info): - assert isinstance( - iterable, Iterable), 'Resolved value from the connection field have to be iterable' - connection = connection_from_list( - iterable, args, connection_type=cls, - edge_type=cls.edge_type, pageinfo_type=PageInfo) - connection.set_connection_data(iterable) - return connection - - def set_connection_data(self, data): - self._connection_data = data - - def get_connection_data(self): - return self._connection_data - - class NodeMeta(InterfaceMeta): def construct_get_node(cls): @@ -153,17 +69,6 @@ class Node(six.with_metaclass(NodeMeta, Interface)): def to_global_id(self): return self.global_id(self.id) - connection_type = Connection - edge_type = Edge - - @classmethod - def get_connection_type(cls): - return cls.connection_type - - @classmethod - def get_edge_type(cls): - return cls.edge_type - class MutationInputType(InputObjectType): clientMutationId = String(required=True) From dcda05f52957ae9f1d58d053ae167068bc2b9760 Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Tue, 26 Jul 2016 18:16:26 +0100 Subject: [PATCH 2/9] Fix imports. --- graphene/contrib/django/types.py | 3 ++- graphene/contrib/sqlalchemy/types.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index f8f9cb2b..938572bb 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -4,7 +4,8 @@ import six from django.db import models from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta -from ...relay.types import Connection, Node, NodeMeta +from ...relay.types import Node, NodeMeta +from ...relay.connection import Connection from .converter import convert_django_field_with_choices from .options import DjangoOptions from .utils import get_reverse_fields diff --git a/graphene/contrib/sqlalchemy/types.py b/graphene/contrib/sqlalchemy/types.py index 20202ab7..7f9ab98b 100644 --- a/graphene/contrib/sqlalchemy/types.py +++ b/graphene/contrib/sqlalchemy/types.py @@ -5,7 +5,8 @@ from sqlalchemy.inspection import inspect as sqlalchemyinspect from sqlalchemy.orm.exc import NoResultFound from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta -from ...relay.types import Connection, Node, NodeMeta +from ...relay.types import Node, NodeMeta +from ...relay.connection import Connection from .converter import (convert_sqlalchemy_column, convert_sqlalchemy_relationship) from .options import SQLAlchemyOptions From 6811f93aa432410f5ab6ebc255cf25257e621dea Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Tue, 26 Jul 2016 18:24:15 +0100 Subject: [PATCH 3/9] Fix last imports. --- examples/starwars_relay/tests/test_connections.py | 3 +++ graphene/relay/connection.py | 4 ++++ graphene/relay/types.py | 2 -- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/starwars_relay/tests/test_connections.py b/examples/starwars_relay/tests/test_connections.py index 17b6865f..93527350 100644 --- a/examples/starwars_relay/tests/test_connections.py +++ b/examples/starwars_relay/tests/test_connections.py @@ -34,5 +34,8 @@ def test_correct_fetch_first_ship_rebels(): } } result = schema.execute(query) + print('-------------------------------') + print(result) + print(result.errors) assert not result.errors assert result.data == expected diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 2c9e3952..b0652823 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -1,3 +1,7 @@ +from collections import Iterable + +from graphql_relay.connection.arrayconnection import connection_from_list + from ..core.classtypes import ObjectType from ..core.types import Field, Boolean, String, List from ..utils import memoize diff --git a/graphene/relay/types.py b/graphene/relay/types.py index e79519b2..e3c33e30 100644 --- a/graphene/relay/types.py +++ b/graphene/relay/types.py @@ -1,11 +1,9 @@ import inspect import warnings -from collections import Iterable from functools import wraps import six -from graphql_relay.connection.arrayconnection import connection_from_list from graphql_relay.node.node import to_global_id from ..core.classtypes import InputObjectType, Interface, Mutation, ObjectType From 603d4770eb3f1233748588525c56cc2d0a53a337 Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Tue, 26 Jul 2016 18:25:02 +0100 Subject: [PATCH 4/9] Remove print statements. --- examples/starwars_relay/tests/test_connections.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/starwars_relay/tests/test_connections.py b/examples/starwars_relay/tests/test_connections.py index 93527350..17b6865f 100644 --- a/examples/starwars_relay/tests/test_connections.py +++ b/examples/starwars_relay/tests/test_connections.py @@ -34,8 +34,5 @@ def test_correct_fetch_first_ship_rebels(): } } result = schema.execute(query) - print('-------------------------------') - print(result) - print(result.errors) assert not result.errors assert result.data == expected From 2a288eab9d98b90120b1cb446623611b13a943bc Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Thu, 28 Jul 2016 12:11:42 +0100 Subject: [PATCH 5/9] Fix a bug where you can not directly return `relay.Connection.for_node(NodeType)(edges=edges, page_info=relay.PageInfo(has_next_page=has_next_page))` in a resolver --- graphene/relay/fields.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index e7238f5a..b80437be 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -24,8 +24,8 @@ class ConnectionField(Field): last=Int(), description=description, **kwargs) - self.connection_type = connection_type - self.edge_type = edge_type + self.connection_type = connection_type or Connection + self.edge_type = edge_type or Edge @with_context def resolver(self, instance, args, context, info): @@ -38,7 +38,7 @@ class ConnectionField(Field): else: resolved = super(ConnectionField, self).resolver(instance, args, info) - if isinstance(resolved, connection_type): + if isinstance(resolved, self.connection_type): return resolved return self.from_list(connection_type, resolved, args, context, info) @@ -46,13 +46,10 @@ class ConnectionField(Field): return connection_type.from_list(resolved, args, context, info) def get_connection_type(self, node): - connection_type = self.connection_type or Connection - edge_type = self.get_edge_type(node) - return connection_type.for_node(node, edge_type=edge_type) + return self.connection_type.for_node(node) def get_edge_type(self, node): - edge_type = self.edge_type or Edge - return edge_type.for_node(node) + return self.edge_type.for_node(node) def get_type(self, schema): from graphene.relay.utils import is_node From 18bb5030afdbfe4105e40c1864a58b10b5f8dc1d Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Thu, 28 Jul 2016 12:16:24 +0100 Subject: [PATCH 6/9] fix linting issues in connection py --- graphene/relay/connection.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index b0652823..304fb4c2 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -28,6 +28,7 @@ class PageInfo(ObjectType): endCursor = String( description='When paginating forwards, the cursor to continue.') + class Edge(ObjectType): '''An edge in a connection.''' cursor = String( @@ -36,8 +37,6 @@ class Edge(ObjectType): @classmethod @memoize def for_node(cls, node): - from graphene.relay.utils import is_node - # assert is_node(node), 'ObjectTypes in a edge have to be Nodes' node_field = Field(node, description='The item at the end of the edge') return type( '%s%s' % (node._meta.type_name, cls._meta.type_name), @@ -64,7 +63,6 @@ class Connection(ObjectType): @classmethod @memoize def for_node(cls, node, edge_type=None): - from graphene.relay.utils import is_node edge_type = edge_type or Edge.for_node(node) edges = List(edge_type, description='Information to aid in pagination.') return type( From a72bee4fa5261ebb639d8d5d968bb4d395e76dcc Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Thu, 28 Jul 2016 12:16:50 +0100 Subject: [PATCH 7/9] fix linting issues in fields.py --- graphene/relay/fields.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index b80437be..d93cdcf8 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -52,7 +52,6 @@ class ConnectionField(Field): return self.edge_type.for_node(node) def get_type(self, schema): - from graphene.relay.utils import is_node type = schema.T(self.type) node = schema.objecttype(type) schema.register(node) From 0d9e645beac81dc111e5d0ba25e643586b3af66a Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Thu, 28 Jul 2016 12:17:27 +0100 Subject: [PATCH 8/9] fix linting issues in types.py --- graphene/relay/types.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/graphene/relay/types.py b/graphene/relay/types.py index e3c33e30..e1a1ffc5 100644 --- a/graphene/relay/types.py +++ b/graphene/relay/types.py @@ -6,13 +6,12 @@ import six from graphql_relay.node.node import to_global_id -from ..core.classtypes import InputObjectType, Interface, Mutation, ObjectType +from ..core.classtypes import InputObjectType, Interface, Mutation from ..core.classtypes.interface import InterfaceMeta from ..core.classtypes.mutation import MutationMeta -from ..core.types import Boolean, Field, List, String +from ..core.types import String from ..core.types.argument import ArgumentsGroup from ..core.types.definitions import NonNull -from ..utils import memoize from ..utils.wrap_resolver_function import has_context, with_context from .fields import GlobalIDField From 878391be4e5175be69d42f332fcb30264dbd996d Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Fri, 29 Jul 2016 09:15:55 +0100 Subject: [PATCH 9/9] Add tests for both fixes. --- graphene/relay/tests/test_query.py | 90 ++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/graphene/relay/tests/test_query.py b/graphene/relay/tests/test_query.py index 08e57226..0c5f0fd2 100644 --- a/graphene/relay/tests/test_query.py +++ b/graphene/relay/tests/test_query.py @@ -19,6 +19,9 @@ class MyNode(relay.Node): def get_node(cls, id, info): return MyNode(id=id, name='mo') +class MyObject(graphene.ObjectType): + name = graphene.String() + class SpecialNode(relay.Node): value = graphene.String() @@ -29,6 +32,9 @@ class SpecialNode(relay.Node): value = "!!!" if context.get('is_special') else "???" return SpecialNode(id=id, value=value) +def _create_my_node_edge(myNode): + edge_type = relay.Edge.for_node(MyNode) + return edge_type(node=myNode, cursor=str(myNode.id)) class Query(graphene.ObjectType): my_node = relay.NodeField(MyNode) @@ -40,6 +46,12 @@ class Query(graphene.ObjectType): context_nodes = relay.ConnectionField( MyNode, connection_type=MyConnection, customArg=graphene.String()) + connection_type_nodes = relay.ConnectionField( + MyNode, connection_type=MyConnection) + + all_my_objects = relay.ConnectionField( + MyObject, connection_type=MyConnection) + def resolve_all_my_nodes(self, args, info): custom_arg = args.get('customArg') assert custom_arg == "1" @@ -51,6 +63,16 @@ class Query(graphene.ObjectType): assert custom_arg == "1" return [MyNode(name='my')] + def resolve_connection_type_nodes(self, args, info): + edges = [_create_my_node_edge(n) for n in [MyNode(id='1', name='my')]] + connection_type = MyConnection.for_node(MyNode) + + return connection_type( + edges=edges, page_info=relay.PageInfo(has_next_page=True)) + + def resolve_all_my_objects(self, args, info): + return [MyObject(name='my_object')] + schema.query = Query @@ -135,6 +157,74 @@ def test_connectionfield_context_query(): assert result.data == expected +def test_connectionfield_resolve_returns_connection_type_directly(): + query = ''' + query RebelsShipsQuery { + connectionTypeNodes { + edges { + node { + name + } + }, + myCustomField + pageInfo { + hasNextPage + } + } + } + ''' + expected = { + 'connectionTypeNodes': { + 'edges': [{ + 'node': { + 'name': 'my' + } + }], + 'myCustomField': 'Custom', + 'pageInfo': { + 'hasNextPage': True, + } + } + } + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + +def test_connectionfield_resolve_returning_objects(): + query = ''' + query RebelsShipsQuery { + allMyObjects { + edges { + node { + name + } + }, + myCustomField + pageInfo { + hasNextPage + } + } + } + ''' + expected = { + 'allMyObjects': { + 'edges': [{ + 'node': { + 'name': 'my_object' + } + }], + 'myCustomField': 'Custom', + 'pageInfo': { + 'hasNextPage': False, + } + } + } + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + @pytest.mark.parametrize('specialness,value', [(True, '!!!'), (False, '???')]) def test_get_node_info(specialness, value): query = '''