From bd073039718096ba2f77d91ce78c8a8feb8586f1 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 2 Nov 2016 21:25:17 -0700 Subject: [PATCH 1/6] First phase of connection resolver --- graphene/relay/connection.py | 55 ++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 46dbba98..b4639ef9 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -90,17 +90,39 @@ class ConnectionMeta(ObjectTypeMeta): class Connection(six.with_metaclass(ConnectionMeta, ObjectType)): - pass + + @classmethod + def Field(cls, *args, **kwargs): # noqa: N802 + return ConnectionField(cls, *args, **kwargs) + + @classmethod + def connection_resolver(cls, resolved, args, context, info): + if isinstance(resolved, cls): + return resolved + + assert isinstance(resolved, Iterable), ( + 'Resolved value from the connection field have to be iterable or instance of {}. ' + 'Received "{}"' + ).format(cls, resolved) + connection = connection_from_list( + resolved, + args, + connection_type=cls, + edge_type=cls.Edge, + pageinfo_type=PageInfo + ) + connection.iterable = resolved + return connection -class IterableConnectionField(Field): +class ConnectionField(Field): def __init__(self, type, *args, **kwargs): kwargs.setdefault('before', String()) kwargs.setdefault('after', String()) kwargs.setdefault('first', Int()) kwargs.setdefault('last', Int()) - super(IterableConnectionField, self).__init__( + super(ConnectionField, self).__init__( type, *args, **kwargs @@ -108,7 +130,7 @@ class IterableConnectionField(Field): @property def type(self): - type = super(IterableConnectionField, self).type + type = super(ConnectionField, self).type if is_node(type): connection_type = type.Connection else: @@ -118,37 +140,16 @@ class IterableConnectionField(Field): ).format(str(self), connection_type) return connection_type - @classmethod - def resolve_connection(cls, connection_type, args, resolved): - if isinstance(resolved, connection_type): - return resolved - - assert isinstance(resolved, Iterable), ( - 'Resolved value from the connection field have to be iterable or instance of {}. ' - 'Received "{}"' - ).format(connection_type, resolved) - connection = connection_from_list( - resolved, - args, - connection_type=connection_type, - edge_type=connection_type.Edge, - pageinfo_type=PageInfo - ) - connection.iterable = resolved - return connection - @classmethod def connection_resolver(cls, resolver, connection_type, root, args, context, info): resolved = resolver(root, args, context, info) - on_resolve = partial(cls.resolve_connection, connection_type, args) + on_resolve = partial(connection_type.connection_resolver, args=args, context=context, info=info) if isinstance(resolved, Promise): return resolved.then(on_resolve) return on_resolve(resolved) def get_resolver(self, parent_resolver): - resolver = super(IterableConnectionField, self).get_resolver(parent_resolver) + resolver = super(ConnectionField, self).get_resolver(parent_resolver) return partial(self.connection_resolver, resolver, self.type) - -ConnectionField = IterableConnectionField From 072b2f3dd0abb4dcf61597393dabf29fdc1ce1b1 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 3 Nov 2016 00:07:28 -0700 Subject: [PATCH 2/6] Improved Connection abstractions. Removed default Node.Connection --- graphene/relay/connection.py | 16 ++++++--------- graphene/relay/node.py | 20 ------------------- graphene/relay/tests/test_connection.py | 6 +++++- graphene/relay/tests/test_connection_query.py | 17 ++++++++++------ graphene/relay/tests/test_mutation.py | 10 ++++++++-- graphene/relay/tests/test_node.py | 9 --------- graphene/types/interface.py | 4 ---- graphene/types/objecttype.py | 4 ---- 8 files changed, 30 insertions(+), 56 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index b4639ef9..6ccebdf3 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -97,9 +97,6 @@ class Connection(six.with_metaclass(ConnectionMeta, ObjectType)): @classmethod def connection_resolver(cls, resolved, args, context, info): - if isinstance(resolved, cls): - return resolved - assert isinstance(resolved, Iterable), ( 'Resolved value from the connection field have to be iterable or instance of {}. ' 'Received "{}"' @@ -131,19 +128,18 @@ class ConnectionField(Field): @property def type(self): type = super(ConnectionField, self).type - if is_node(type): - connection_type = type.Connection - else: - connection_type = type - assert issubclass(connection_type, Connection), ( + assert issubclass(type, Connection), ( '{} type have to be a subclass of Connection. Received "{}".' - ).format(str(self), connection_type) - return connection_type + ).format(str(self), type) + return type @classmethod def connection_resolver(cls, resolver, connection_type, root, args, context, info): resolved = resolver(root, args, context, info) + if isinstance(resolved, connection_type): + return resolved + on_resolve = partial(connection_type.connection_resolver, args=args, context=context, info=info) if isinstance(resolved, Promise): return resolved.then(on_resolve) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index c1bbe6d5..e95c19fc 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -21,18 +21,6 @@ def is_node(objecttype): return False -def get_default_connection(cls): - from .connection import Connection - assert issubclass(cls, ObjectType), ( - 'Can only get connection type on implemented Nodes.' - ) - - class Meta: - node = cls - - return type('{}Connection'.format(cls.__name__), (Connection,), {'Meta': Meta}) - - class GlobalID(Field): def __init__(self, node, *args, **kwargs): @@ -101,11 +89,3 @@ class Node(six.with_metaclass(NodeMeta, Interface)): @classmethod def to_global_id(cls, type, id): return to_global_id(type, id) - - @classmethod - def implements(cls, objecttype): - get_connection = getattr(objecttype, 'get_connection', None) - if not get_connection: - get_connection = partial(get_default_connection, objecttype) - - objecttype.Connection = get_connection() diff --git a/graphene/relay/tests/test_connection.py b/graphene/relay/tests/test_connection.py index 87f937ae..19b54ad1 100644 --- a/graphene/relay/tests/test_connection.py +++ b/graphene/relay/tests/test_connection.py @@ -97,7 +97,11 @@ def test_edge_with_bases(): def test_edge_on_node(): - Edge = MyObject.Connection.Edge + class MyObjectConnection(Connection): + class Meta: + node = MyObject + + Edge = MyObjectConnection.Edge assert Edge._meta.name == 'MyObjectEdge' edge_fields = Edge._meta.fields assert list(edge_fields.keys()) == ['node', 'cursor'] diff --git a/graphene/relay/tests/test_connection_query.py b/graphene/relay/tests/test_connection_query.py index 068081d6..72614a64 100644 --- a/graphene/relay/tests/test_connection_query.py +++ b/graphene/relay/tests/test_connection_query.py @@ -4,7 +4,7 @@ from graphql_relay.utils import base64 from promise import Promise from ...types import ObjectType, Schema, String -from ..connection import ConnectionField, PageInfo +from ..connection import ConnectionField, Connection, PageInfo from ..node import Node letter_chars = ['A', 'B', 'C', 'D', 'E'] @@ -18,10 +18,15 @@ class Letter(ObjectType): letter = String() +class LetterConnection(Connection): + class Meta: + node = Letter + + class Query(ObjectType): - letters = ConnectionField(Letter) - connection_letters = ConnectionField(Letter) - promise_letters = ConnectionField(Letter) + letters = LetterConnection.Field() + connection_letters = LetterConnection.Field() + promise_letters = LetterConnection.Field() node = Node.Field() @@ -32,13 +37,13 @@ class Query(ObjectType): return Promise.resolve(list(letters.values())) def resolve_connection_letters(self, args, context, info): - return Letter.Connection( + return LetterConnection( page_info=PageInfo( has_next_page=True, has_previous_page=False ), edges=[ - Letter.Connection.Edge( + LetterConnection.Edge( node=Letter(id=0, letter='A'), cursor='a-cursor' ), diff --git a/graphene/relay/tests/test_mutation.py b/graphene/relay/tests/test_mutation.py index 34fbb936..2b23977f 100644 --- a/graphene/relay/tests/test_mutation.py +++ b/graphene/relay/tests/test_mutation.py @@ -3,6 +3,7 @@ import pytest from ...types import (AbstractType, Argument, Field, InputField, InputObjectType, NonNull, ObjectType, Schema) from ...types.scalars import String +from ..connection import Connection from ..mutation import ClientIDMutation from ..node import Node @@ -19,6 +20,11 @@ class MyNode(ObjectType): name = String() +class MyNodeConnection(Connection): + class Meta: + node = MyNode + + class SaySomething(ClientIDMutation): class Input: @@ -37,13 +43,13 @@ class OtherMutation(ClientIDMutation): additional_field = String() name = String() - my_node_edge = Field(MyNode.Connection.Edge) + my_node_edge = Field(MyNodeConnection.Edge) @classmethod def mutate_and_get_payload(cls, args, context, info): shared = args.get('shared', '') additionalField = args.get('additionalField', '') - edge_type = MyNode.Connection.Edge + edge_type = MyNodeConnection.Edge return OtherMutation(name=shared + additionalField, my_node_edge=edge_type( cursor='1', node=MyNode(name='name'))) diff --git a/graphene/relay/tests/test_node.py b/graphene/relay/tests/test_node.py index 12c7c2c2..d174d8aa 100644 --- a/graphene/relay/tests/test_node.py +++ b/graphene/relay/tests/test_node.py @@ -52,15 +52,6 @@ def test_node_good(): assert 'id' in MyNode._meta.fields -def test_node_get_connection(): - connection = MyNode.Connection - assert issubclass(connection, Connection) - - -def test_node_get_connection_dont_duplicate(): - assert MyNode.Connection == MyNode.Connection - - def test_node_query(): executed = schema.execute( '{ node(id:"%s") { ... on MyNode { name } } }' % to_global_id("MyNode", 1) diff --git a/graphene/types/interface.py b/graphene/types/interface.py index cc8361e6..c4ee8959 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -56,7 +56,3 @@ class Interface(six.with_metaclass(InterfaceMeta)): def __init__(self, *args, **kwargs): raise Exception("An Interface cannot be intitialized") - - @classmethod - def implements(cls, objecttype): - pass diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index f06dbf5e..c7bcc0a5 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -45,10 +45,6 @@ class ObjectTypeMeta(AbstractTypeMeta): ) cls = type.__new__(cls, name, bases, dict(attrs, _meta=options)) - - for interface in options.interfaces: - interface.implements(cls) - return cls def __str__(cls): # noqa: N802 From 1d8f5ded331a5dd8d7305d6c75d460ec7e33de86 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 3 Nov 2016 00:17:30 -0700 Subject: [PATCH 3/6] Simplified ConnectionField type checking --- graphene/relay/connection.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 6ccebdf3..d0dcaf22 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -119,20 +119,15 @@ class ConnectionField(Field): kwargs.setdefault('after', String()) kwargs.setdefault('first', Int()) kwargs.setdefault('last', Int()) + assert issubclass(type, Connection), ( + '{} type have to be a subclass of Connection. Received "{}".' + ).format(str(self), type) super(ConnectionField, self).__init__( type, *args, **kwargs ) - @property - def type(self): - type = super(ConnectionField, self).type - assert issubclass(type, Connection), ( - '{} type have to be a subclass of Connection. Received "{}".' - ).format(str(self), type) - return type - @classmethod def connection_resolver(cls, resolver, connection_type, root, args, context, info): resolved = resolver(root, args, context, info) From 42b93b5546858ecd74c7cac5ebb09add368eac48 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 3 Nov 2016 00:36:14 -0700 Subject: [PATCH 4/6] Fixed examples --- examples/starwars_relay/schema.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/starwars_relay/schema.py b/examples/starwars_relay/schema.py index 8ad1a643..cdbf86c8 100644 --- a/examples/starwars_relay/schema.py +++ b/examples/starwars_relay/schema.py @@ -17,6 +17,11 @@ class Ship(graphene.ObjectType): return get_ship(id) +class ShipConnection(graphene.Connection): + class Meta: + node = Ship + + class Faction(graphene.ObjectType): '''A faction in the Star Wars saga''' @@ -24,7 +29,7 @@ class Faction(graphene.ObjectType): interfaces = (relay.Node, ) name = graphene.String(description='The name of the faction.') - ships = relay.ConnectionField(Ship, description='The ships used by the faction.') + ships = ShipConnection.Field(description='The ships used by the faction.') @resolve_only_args def resolve_ships(self, **args): From 3fa65aa343ce535f0736eb6c7332f13620ba071b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 3 Nov 2016 20:21:45 -0700 Subject: [PATCH 5/6] Improved connection type of checking --- graphene/relay/connection.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index d0dcaf22..3b1cac5a 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -95,6 +95,10 @@ class Connection(six.with_metaclass(ConnectionMeta, ObjectType)): def Field(cls, *args, **kwargs): # noqa: N802 return ConnectionField(cls, *args, **kwargs) + @classmethod + def is_type_of(cls, root, context, info): + return isinstance(root, cls) + @classmethod def connection_resolver(cls, resolved, args, context, info): assert isinstance(resolved, Iterable), ( @@ -132,7 +136,7 @@ class ConnectionField(Field): def connection_resolver(cls, resolver, connection_type, root, args, context, info): resolved = resolver(root, args, context, info) - if isinstance(resolved, connection_type): + if connection_type.is_type_of(resolved, context, info): return resolved on_resolve = partial(connection_type.connection_resolver, args=args, context=context, info=info) From e6733d56b20bc6c8a5c065b1bcc03b6cd2a1feb2 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 3 Nov 2016 20:24:26 -0700 Subject: [PATCH 6/6] Improved support for promise-like connections --- graphene/relay/connection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 3b1cac5a..e6d100a4 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -5,7 +5,7 @@ from functools import partial import six from graphql_relay import connection_from_list -from promise import Promise +from promise import Promise, is_thenable, promisify from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union) @@ -140,8 +140,8 @@ class ConnectionField(Field): return resolved on_resolve = partial(connection_type.connection_resolver, args=args, context=context, info=info) - if isinstance(resolved, Promise): - return resolved.then(on_resolve) + if is_thenable(resolved): + return promisify(resolved).then(on_resolve) return on_resolve(resolved)