From 6321c52bd201829344d1c3c36db0f085d404e744 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 12 Jul 2017 21:21:16 -0700 Subject: [PATCH] Fixed Connection tests --- examples/starwars_relay/schema.py | 7 +- .../snap_test_objectidentification.py | 60 +++++++++++++ .../tests/test_objectidentification.py | 62 +------------ graphene/__init__.py | 18 ++-- graphene/relay/connection.py | 89 +++++++++---------- graphene/relay/tests/test_connection.py | 16 +--- graphene/relay/tests/test_connection_query.py | 17 ++-- graphene/types/enum.py | 5 +- graphene/types/objecttype.py | 5 +- graphene/utils/subclass_with_meta.py | 11 ++- graphene/utils/tests/test_module_loading.py | 11 ++- 11 files changed, 151 insertions(+), 150 deletions(-) diff --git a/examples/starwars_relay/schema.py b/examples/starwars_relay/schema.py index 8ad1a643..f914dc2b 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(relay.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 = relay.ConnectionField(ShipConnection, description='The ships used by the faction.') @resolve_only_args def resolve_ships(self, **args): diff --git a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py index a6095bb0..bce35808 100644 --- a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py +++ b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py @@ -51,3 +51,63 @@ snapshots['test_correctly_refetches_xwing 1'] = { } } } + +snapshots['test_str_schema 1'] = '''schema { + query: Query + mutation: Mutation +} + +type Faction implements Node { + id: ID! + name: String + ships(before: String, after: String, first: Int, last: Int): ShipConnection +} + +type IntroduceShip { + ship: Ship + faction: Faction + clientMutationId: String +} + +input IntroduceShipInput { + shipName: String! + factionId: String! + clientMutationId: String +} + +type Mutation { + introduceShip(input: IntroduceShipInput!): IntroduceShip +} + +interface Node { + id: ID! +} + +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String +} + +type Query { + rebels: Faction + empire: Faction + node(id: ID!): Node +} + +type Ship implements Node { + id: ID! + name: String +} + +type ShipConnection { + pageInfo: PageInfo! + edges: [ShipEdge]! +} + +type ShipEdge { + node: Ship + cursor: String! +} +''' diff --git a/examples/starwars_relay/tests/test_objectidentification.py b/examples/starwars_relay/tests/test_objectidentification.py index d1b4c529..9ee1d495 100644 --- a/examples/starwars_relay/tests/test_objectidentification.py +++ b/examples/starwars_relay/tests/test_objectidentification.py @@ -7,66 +7,8 @@ setup() client = Client(schema) -def test_str_schema(): - assert str(schema) == '''schema { - query: Query - mutation: Mutation -} - -type Faction implements Node { - id: ID! - name: String - ships(before: String, after: String, first: Int, last: Int): ShipConnection -} - -input IntroduceShipInput { - shipName: String! - factionId: String! - clientMutationId: String -} - -type IntroduceShipPayload { - ship: Ship - faction: Faction - clientMutationId: String -} - -type Mutation { - introduceShip(input: IntroduceShipInput!): IntroduceShipPayload -} - -interface Node { - id: ID! -} - -type PageInfo { - hasNextPage: Boolean! - hasPreviousPage: Boolean! - startCursor: String - endCursor: String -} - -type Query { - rebels: Faction - empire: Faction - node(id: ID!): Node -} - -type Ship implements Node { - id: ID! - name: String -} - -type ShipConnection { - pageInfo: PageInfo! - edges: [ShipEdge]! -} - -type ShipEdge { - node: Ship - cursor: String! -} -''' +def test_str_schema(snapshot): + snapshot.assert_match(str(schema)) def test_correctly_fetches_id_name_rebels(snapshot): diff --git a/graphene/__init__.py b/graphene/__init__.py index 556a8175..c15f2cbe 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -33,15 +33,15 @@ if not __SETUP__: Dynamic, Union, ) - # from .relay import ( - # Node, - # is_node, - # GlobalID, - # ClientIDMutation, - # Connection, - # ConnectionField, - # PageInfo - # ) + from .relay import ( + Node, + is_node, + GlobalID, + ClientIDMutation, + Connection, + ConnectionField, + PageInfo + ) from .utils.resolve_only_args import resolve_only_args from .utils.module_loading import lazy_import diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index bc33e58b..dea5feab 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -7,10 +7,10 @@ import six from graphql_relay import connection_from_list from promise import Promise, is_thenable -from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, +from ..types import (Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union) from ..types.field import Field -from ..types.objecttype import ObjectType +from ..types.objecttype import ObjectType, ObjectTypeOptions from ..utils.props import props from .node import is_node @@ -39,56 +39,47 @@ class PageInfo(ObjectType): ) -class ConnectionMeta(type): - - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of Model - # (excluding Model class itself). - if not is_base_type(bases, ConnectionMeta): - return type.__new__(cls, name, bases, attrs) - - options = Options( - attrs.pop('Meta', None), - name=name, - description=None, - node=None, - ) - options.interfaces = () - options.local_fields = OrderedDict() - - assert options.node, 'You have to provide a node in {}.Meta'.format(cls.__name__) - assert issubclass(options.node, (Scalar, Enum, ObjectType, Interface, Union, NonNull)), ( - 'Received incompatible node "{}" for Connection {}.' - ).format(options.node, name) - - base_name = re.sub('Connection$', '', options.name) or options.node._meta.name - if not options.name: - options.name = '{}Connection'.format(base_name) - - edge_class = attrs.pop('Edge', None) - - class EdgeBase(AbstractType): - node = Field(options.node, description='The item at the end of the edge') - cursor = String(required=True, description='A cursor for use in pagination') - - edge_name = '{}Edge'.format(base_name) - if edge_class and issubclass(edge_class, AbstractType): - edge = type(edge_name, (EdgeBase, edge_class, ObjectType, ), {}) - else: - edge_attrs = props(edge_class) if edge_class else {} - edge = type(edge_name, (EdgeBase, ObjectType, ), edge_attrs) - - class ConnectionBase(AbstractType): - page_info = Field(PageInfo, name='pageInfo', required=True) - edges = NonNull(List(edge)) - - bases = (ConnectionBase, ) + bases - attrs = dict(attrs, _meta=options, Edge=edge) - return ObjectTypeMeta.__new__(cls, name, bases, attrs) +class ConnectionOptions(ObjectTypeOptions): + node = None class Connection(ObjectType): - pass + class Meta: + abstract = True + @classmethod + def __init_subclass_with_meta__(cls, node=None, name=None, **options): + _meta = ConnectionOptions(cls) + assert node, 'You have to provide a node in {}.Meta'.format(cls.__name__) + assert issubclass(node, (Scalar, Enum, ObjectType, Interface, Union, NonNull)), ( + 'Received incompatible node "{}" for Connection {}.' + ).format(node, cls.__name__) + + base_name = re.sub('Connection$', '', name or cls.__name__) or node._meta.name + if not name: + name = '{}Connection'.format(base_name) + + edge_class = getattr(cls, 'Edge', None) + _node = node + class EdgeBase(object): + node = Field(_node, description='The item at the end of the edge') + cursor = String(required=True, description='A cursor for use in pagination') + + edge_name = '{}Edge'.format(base_name) + if edge_class: + edge_bases = (edge_class, EdgeBase, ObjectType,) + else: + edge_bases = (EdgeBase, ObjectType,) + + edge = type(edge_name, edge_bases, {}) + cls.Edge = edge + + _meta.name = name + _meta.node = node + _meta.fields = OrderedDict([ + ('page_info', Field(PageInfo, name='pageInfo', required=True)), + ('edges', Field(NonNull(List(edge)))), + ]) + return super(Connection, cls).__init_subclass_with_meta__(_meta=_meta, **options) class IterableConnectionField(Field): diff --git a/graphene/relay/tests/test_connection.py b/graphene/relay/tests/test_connection.py index 87f937ae..f44bc5ab 100644 --- a/graphene/relay/tests/test_connection.py +++ b/graphene/relay/tests/test_connection.py @@ -1,5 +1,5 @@ -from ...types import AbstractType, Field, List, NonNull, ObjectType, String, Argument, Int +from ...types import Field, List, NonNull, ObjectType, String, Argument, Int from ..connection import Connection, PageInfo, ConnectionField from ..node import Node @@ -38,7 +38,7 @@ def test_connection(): def test_connection_inherit_abstracttype(): - class BaseConnection(AbstractType): + class BaseConnection(object): extra = String() class MyObjectConnection(BaseConnection, Connection): @@ -73,7 +73,7 @@ def test_edge(): def test_edge_with_bases(): - class BaseEdge(AbstractType): + class BaseEdge(object): extra = String() class MyObjectConnection(Connection): @@ -96,16 +96,6 @@ def test_edge_with_bases(): assert edge_fields['other'].type == String -def test_edge_on_node(): - Edge = MyObject.Connection.Edge - assert Edge._meta.name == 'MyObjectEdge' - edge_fields = Edge._meta.fields - assert list(edge_fields.keys()) == ['node', 'cursor'] - - assert isinstance(edge_fields['node'], Field) - assert edge_fields['node'].type == MyObject - - def test_pageinfo(): assert PageInfo._meta.name == 'PageInfo' fields = PageInfo._meta.fields diff --git a/graphene/relay/tests/test_connection_query.py b/graphene/relay/tests/test_connection_query.py index 068081d6..3b543552 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 Connection, ConnectionField, 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 = ConnectionField(LetterConnection) + connection_letters = ConnectionField(LetterConnection) + promise_letters = ConnectionField(LetterConnection) 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/types/enum.py b/graphene/types/enum.py index 231fa61c..0103a3b2 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -5,6 +5,7 @@ import six from .base import BaseOptions, BaseType from .unmountedtype import UnmountedType from graphene.pyutils.init_subclass import InitSubclassMeta +from graphene.utils.subclass_with_meta import SubclassWithMeta_Meta try: from enum import Enum as PyEnum @@ -25,7 +26,7 @@ class EnumOptions(BaseOptions): enum = None # type: Enum -class EnumMeta(InitSubclassMeta): +class EnumMeta(SubclassWithMeta_Meta): def get(cls, value): return cls._meta.enum(value) @@ -51,7 +52,7 @@ class Enum(six.with_metaclass(EnumMeta, UnmountedType, BaseType)): @classmethod def __init_subclass_with_meta__(cls, enum=None, **options): _meta = EnumOptions(cls) - _meta.enum = enum or PyEnum(cls.__name__, dict(cls.__dict__, __eq__=eq_enum)) + _meta.enum = enum or PyEnum(cls.__name__, OrderedDict(cls.__dict__, __eq__=eq_enum)) for key, value in _meta.enum.__members__.items(): setattr(cls, key, value) super(Enum, cls).__init_subclass_with_meta__(_meta=_meta, **options) diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index c7bb34c5..ac7214f6 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -20,7 +20,9 @@ class ObjectType(BaseType): have a name, but most importantly describe their fields. ''' @classmethod - def __init_subclass_with_meta__(cls, interfaces=(), possible_types=(), default_resolver=None, _meta=None, **options): + def __init_subclass_with_meta__(cls, interfaces=(), possible_types=(), default_resolver=None, _meta=None, abstract=False, **options): + if abstract: + return if not _meta: _meta = ObjectTypeOptions(cls) @@ -46,6 +48,7 @@ class ObjectType(BaseType): _meta.fields.update(fields) else: _meta.fields = fields + _meta.interfaces = interfaces _meta.possible_types = possible_types _meta.default_resolver = default_resolver diff --git a/graphene/utils/subclass_with_meta.py b/graphene/utils/subclass_with_meta.py index 63fd0ec0..00ddb09f 100644 --- a/graphene/utils/subclass_with_meta.py +++ b/graphene/utils/subclass_with_meta.py @@ -1,12 +1,17 @@ +import six + from .props import props from ..pyutils.init_subclass import InitSubclassMeta -class SubclassWithMeta(object): +class SubclassWithMeta_Meta(InitSubclassMeta): + def __repr__(cls): + return cls._meta.name + + +class SubclassWithMeta(six.with_metaclass(SubclassWithMeta_Meta)): """This class improves __init_subclass__ to receive automatically the options from meta""" # We will only have the metaclass in Python 2 - __metaclass__ = InitSubclassMeta - def __init_subclass__(cls, **meta_options): """This method just terminates the super() chain""" _Meta = getattr(cls, "Meta", None) diff --git a/graphene/utils/tests/test_module_loading.py b/graphene/utils/tests/test_module_loading.py index 769fde8b..7184ed2c 100644 --- a/graphene/utils/tests/test_module_loading.py +++ b/graphene/utils/tests/test_module_loading.py @@ -1,7 +1,6 @@ from pytest import raises -from graphene import String -from graphene.types.objecttype import ObjectTypeMeta +from graphene import String, ObjectType from ..module_loading import lazy_import, import_string @@ -9,8 +8,8 @@ def test_import_string(): MyString = import_string('graphene.String') assert MyString == String - MyObjectTypeMeta = import_string('graphene.ObjectType', '__class__') - assert MyObjectTypeMeta == ObjectTypeMeta + MyObjectTypeMeta = import_string('graphene.ObjectType', '__doc__') + assert MyObjectTypeMeta == ObjectType.__doc__ def test_import_string_module(): @@ -52,6 +51,6 @@ def test_lazy_import(): MyString = f() assert MyString == String - f = lazy_import('graphene.ObjectType', '__class__') + f = lazy_import('graphene.ObjectType', '__doc__') MyObjectTypeMeta = f() - assert MyObjectTypeMeta == ObjectTypeMeta + assert MyObjectTypeMeta == ObjectType.__doc__