From 9a84d595a1e5d151ee0e41400d2cce63fb08f974 Mon Sep 17 00:00:00 2001 From: Syrus Akbary <me@syrusakbary.com> Date: Fri, 25 Sep 2015 16:35:17 -0700 Subject: [PATCH 1/8] First relay version --- .travis.yml | 2 +- graphene/__init__.py | 4 +++ graphene/core/fields.py | 16 +++------ graphene/core/options.py | 6 ++-- graphene/core/types.py | 25 ++++++++++---- graphene/core/utils.py | 30 +++++++++++++++++ graphene/relay/__init__.py | 2 ++ graphene/signals.py | 5 +++ setup.py | 1 + tests/starwars/schema.py | 2 +- tests/starwars_relay/schema.py | 61 ++++++++++++++++++++++++++++++++++ tox.ini | 2 ++ 12 files changed, 134 insertions(+), 22 deletions(-) create mode 100644 graphene/core/utils.py create mode 100644 graphene/signals.py create mode 100644 tests/starwars_relay/schema.py diff --git a/.travis.yml b/.travis.yml index d254bd80..0435df42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ sudo: false python: - 2.7 install: -- pip install pytest pytest-cov coveralls flake8 six +- pip install pytest pytest-cov coveralls flake8 six blinker - pip install git+https://github.com/dittos/graphqllib.git # Last version of graphqllib - pip install graphql-relay - python setup.py develop diff --git a/graphene/__init__.py b/graphene/__init__.py index 42442952..6024313c 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -26,3 +26,7 @@ from graphene.core.types import ( from graphene.decorators import ( resolve_only_args ) + +from graphene.relay import ( + Relay +) diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 876416b1..d37fed1d 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -1,4 +1,3 @@ -import inspect from graphql.core.type import ( GraphQLField, GraphQLList, @@ -9,8 +8,8 @@ from graphql.core.type import ( GraphQLID, GraphQLArgument, ) -from graphene.core.types import ObjectType, Interface from graphene.utils import cached_property +from graphene.core.utils import get_object_type class Field(object): def __init__(self, field_type, resolve=None, null=True, args=None, description='', **extra_args): @@ -45,16 +44,11 @@ class Field(object): @cached_property def type(self): - field_type = self.field_type - _is_class = inspect.isclass(field_type) - if _is_class and issubclass(field_type, ObjectType): - field_type = field_type._meta.type - elif isinstance(field_type, Field): - field_type = field_type.type - elif field_type == 'self': - field_type = self.object_type._meta.type + if isinstance(self.field_type, Field): + field_type = self.field_type.type + else: + field_type = get_object_type(self.field_type, self.object_type) field_type = self.type_wrapper(field_type) - return field_type def type_wrapper(self, field_type): diff --git a/graphene/core/options.py b/graphene/core/options.py index f55cd494..5a016dff 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -1,6 +1,8 @@ from graphene.utils import cached_property -DEFAULT_NAMES = ('description', 'name', 'interface', 'type_name', 'interfaces', 'proxy') +DEFAULT_NAMES = ('description', 'name', 'interface', + 'type_name', 'interfaces', 'proxy') + class Options(object): def __init__(self, meta=None): @@ -62,7 +64,7 @@ class Options(object): @cached_property def fields_map(self): - return {f.field_name:f for f in self.fields} + return {f.field_name: f for f in self.fields} @cached_property def type(self): diff --git a/graphene/core/types.py b/graphene/core/types.py index 250559e5..a5645b16 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -8,8 +8,10 @@ from graphql.core.type import ( ) from graphql.core import graphql +from graphene import signals from graphene.core.options import Options + class ObjectTypeMeta(type): def __new__(cls, name, bases, attrs): super_new = super(ObjectTypeMeta, cls).__new__ @@ -20,7 +22,10 @@ class ObjectTypeMeta(type): module = attrs.pop('__module__') doc = attrs.pop('__doc__', None) - new_class = super_new(cls, name, bases, {'__module__': module, '__doc__': doc}) + new_class = super_new(cls, name, bases, { + '__module__': module, + '__doc__': doc + }) attr_meta = attrs.pop('Meta', None) if not attr_meta: meta = getattr(new_class, 'Meta', None) @@ -51,7 +56,7 @@ class ObjectTypeMeta(type): # moment). for field in parent_fields: if field.field_name in field_names: - raise FieldError( + raise Exception( 'Local field %r in class %r clashes ' 'with field of similar name from ' 'base class %r' % (field.field_name, name, base.__name__) @@ -61,8 +66,12 @@ class ObjectTypeMeta(type): new_class._meta.interfaces.append(base) # new_class._meta.parents.extend(base._meta.parents) + new_class._prepare() return new_class + def _prepare(cls): + signals.class_prepared.send(cls) + def add_to_class(cls, name, value): # We should call the contribute_to_class method only if it's bound if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'): @@ -73,15 +82,17 @@ class ObjectTypeMeta(type): class ObjectType(six.with_metaclass(ObjectTypeMeta)): def __init__(self, instance=None): + signals.pre_init.send(self.__class__, instance=instance) self.instance = instance + signals.post_init.send(self.__class__, instance=self) def get_field(self, field): return getattr(self.instance, field, None) def resolve(self, field_name, args, info): if field_name not in self._meta.fields_map.keys(): - raise Exception('Field %s not found in model'%field_name) - custom_resolve_fn = 'resolve_%s'%field_name + raise Exception('Field %s not found in model' % field_name) + custom_resolve_fn = 'resolve_%s' % field_name if hasattr(self, custom_resolve_fn): resolve_fn = getattr(self, custom_resolve_fn) return resolve_fn(args, info) @@ -104,13 +115,13 @@ class ObjectType(six.with_metaclass(ObjectTypeMeta)): cls._meta.type_name, description=cls._meta.description, resolve_type=cls.resolve_type, - fields=lambda: {name:field.field for name, field in fields.items()} + fields=lambda: {name: field.field for name, field in fields.items()} ) return GraphQLObjectType( cls._meta.type_name, description=cls._meta.description, interfaces=[i._meta.type for i in cls._meta.interfaces], - fields=lambda: {name:field.field for name, field in fields.items()} + fields=lambda: {name: field.field for name, field in fields.items()} ) @@ -125,7 +136,7 @@ class Schema(object): self.query = query self.query_type = query._meta.type self._schema = GraphQLSchema(query=self.query_type, mutation=mutation) - + def execute(self, request='', root=None, vars=None, operation_name=None): return graphql( self._schema, diff --git a/graphene/core/utils.py b/graphene/core/utils.py new file mode 100644 index 00000000..2441e644 --- /dev/null +++ b/graphene/core/utils.py @@ -0,0 +1,30 @@ +import inspect + +from graphene.core.types import ObjectType +from graphene import signals + +registered_object_types = [] + + +def get_object_type(field_type, object_type=None): + _is_class = inspect.isclass(field_type) + if _is_class and issubclass(field_type, ObjectType): + field_type = field_type._meta.type + elif isinstance(field_type, basestring): + if field_type == 'self': + field_type = object_type._meta.type + else: + object_type = get_registered_object_type(field_type) + field_type = object_type._meta.type + return field_type + + +def get_registered_object_type(name): + for object_type in registered_object_types: + if object_type._meta.type_name == name: + return object_type + return None + +@signals.class_prepared.connect +def object_type_created(sender): + registered_object_types.append(sender) diff --git a/graphene/relay/__init__.py b/graphene/relay/__init__.py index e69de29b..80feb122 100644 --- a/graphene/relay/__init__.py +++ b/graphene/relay/__init__.py @@ -0,0 +1,2 @@ +class Relay(object): + pass diff --git a/graphene/signals.py b/graphene/signals.py new file mode 100644 index 00000000..954d02a1 --- /dev/null +++ b/graphene/signals.py @@ -0,0 +1,5 @@ +from blinker import Signal + +class_prepared = Signal() +pre_init = Signal() +post_init = Signal() diff --git a/setup.py b/setup.py index b5222dc0..705d1ac3 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ setup( install_requires=[ 'six', + 'blinker', 'graphqllib', 'graphql-relay' ], diff --git a/tests/starwars/schema.py b/tests/starwars/schema.py index 1a7e570d..018a1fc8 100644 --- a/tests/starwars/schema.py +++ b/tests/starwars/schema.py @@ -18,7 +18,7 @@ def wrap_character(character): class Character(graphene.Interface): id = graphene.IDField() name = graphene.StringField() - friends = graphene.ListField('self') + friends = graphene.ListField('Character') appearsIn = graphene.ListField(Episode) def resolve_friends(self, args, *_): diff --git a/tests/starwars_relay/schema.py b/tests/starwars_relay/schema.py new file mode 100644 index 00000000..1a7e570d --- /dev/null +++ b/tests/starwars_relay/schema.py @@ -0,0 +1,61 @@ +import graphene +from graphene import resolve_only_args + +from .data import getHero, getHuman, getCharacter, getDroid, Human as _Human, Droid as _Droid + +Episode = graphene.Enum('Episode', dict( + NEWHOPE = 4, + EMPIRE = 5, + JEDI = 6 +)) + +def wrap_character(character): + if isinstance(character, _Human): + return Human(character) + elif isinstance(character, _Droid): + return Droid(character) + +class Character(graphene.Interface): + id = graphene.IDField() + name = graphene.StringField() + friends = graphene.ListField('self') + appearsIn = graphene.ListField(Episode) + + def resolve_friends(self, args, *_): + return [wrap_character(getCharacter(f)) for f in self.instance.friends] + +class Human(Character): + homePlanet = graphene.StringField() + + +class Droid(Character): + primaryFunction = graphene.StringField() + + +class Query(graphene.ObjectType): + hero = graphene.Field(Character, + episode = graphene.Argument(Episode) + ) + human = graphene.Field(Human, + id = graphene.Argument(graphene.String) + ) + droid = graphene.Field(Droid, + id = graphene.Argument(graphene.String) + ) + + @resolve_only_args + def resolve_hero(self, episode): + return wrap_character(getHero(episode)) + + @resolve_only_args + def resolve_human(self, id): + return wrap_character(getHuman(id)) + if human: + return Human(human) + + @resolve_only_args + def resolve_droid(self, id): + return wrap_character(getDroid(id)) + + +Schema = graphene.Schema(query=Query) diff --git a/tox.ini b/tox.ini index 09e27276..735a042d 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,8 @@ deps= pytest>=2.7.2 django>=1.8.0,<1.9 flake8 + six + blinker singledispatch commands= py.test From cd216447c256f4aef8501653955ea2d80eb993a0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary <me@syrusakbary.com> Date: Fri, 25 Sep 2015 16:36:18 -0700 Subject: [PATCH 2/8] Added test relay schema --- tests/starwars_relay/schema.py | 36 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/starwars_relay/schema.py b/tests/starwars_relay/schema.py index 1a7e570d..a6a0a750 100644 --- a/tests/starwars_relay/schema.py +++ b/tests/starwars_relay/schema.py @@ -1,47 +1,49 @@ import graphene -from graphene import resolve_only_args +from graphene import resolve_only_args, relay -from .data import getHero, getHuman, getCharacter, getDroid, Human as _Human, Droid as _Droid +from .data import ( + getHero, getHuman, getCharacter, getDroid, + Human as _Human, Droid as _Droid) Episode = graphene.Enum('Episode', dict( - NEWHOPE = 4, - EMPIRE = 5, - JEDI = 6 + NEWHOPE=4, + EMPIRE=5, + JEDI=6 )) + def wrap_character(character): - if isinstance(character, _Human): + if isinstance(character, _Human): return Human(character) elif isinstance(character, _Droid): return Droid(character) + class Character(graphene.Interface): - id = graphene.IDField() name = graphene.StringField() - friends = graphene.ListField('self') + friends = relay.Connection('self') appearsIn = graphene.ListField(Episode) def resolve_friends(self, args, *_): return [wrap_character(getCharacter(f)) for f in self.instance.friends] -class Human(Character): + +class Human(relay.Node, Character): homePlanet = graphene.StringField() -class Droid(Character): +class Droid(relay.Node, Character): primaryFunction = graphene.StringField() class Query(graphene.ObjectType): hero = graphene.Field(Character, - episode = graphene.Argument(Episode) - ) + episode=graphene.Argument(Episode)) human = graphene.Field(Human, - id = graphene.Argument(graphene.String) - ) + id=graphene.Argument(graphene.String)) droid = graphene.Field(Droid, - id = graphene.Argument(graphene.String) - ) + id=graphene.Argument(graphene.String)) + node = graphene.Field(relay.Node) @resolve_only_args def resolve_hero(self, episode): @@ -50,8 +52,6 @@ class Query(graphene.ObjectType): @resolve_only_args def resolve_human(self, id): return wrap_character(getHuman(id)) - if human: - return Human(human) @resolve_only_args def resolve_droid(self, id): From 1ec2f5a4c30304abfe13d6f411cca2330a29d84d Mon Sep 17 00:00:00 2001 From: Syrus Akbary <me@syrusakbary.com> Date: Fri, 25 Sep 2015 19:41:11 -0700 Subject: [PATCH 3/8] Added app_label --- graphene/core/options.py | 5 +++-- graphene/core/types.py | 2 +- graphene/core/utils.py | 30 ++++++++++++++++++++++++------ tests/starwars_relay/schema.py | 2 +- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/graphene/core/options.py b/graphene/core/options.py index 5a016dff..a277be15 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -1,17 +1,18 @@ from graphene.utils import cached_property -DEFAULT_NAMES = ('description', 'name', 'interface', +DEFAULT_NAMES = ('app_label', 'description', 'name', 'interface', 'type_name', 'interfaces', 'proxy') class Options(object): - def __init__(self, meta=None): + def __init__(self, meta=None, app_label=None): self.meta = meta self.local_fields = [] self.interface = False self.proxy = False self.interfaces = [] self.parents = [] + self.app_label = app_label def contribute_to_class(self, cls, name): cls._meta = self diff --git a/graphene/core/types.py b/graphene/core/types.py index a5645b16..9b136458 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -33,7 +33,7 @@ class ObjectTypeMeta(type): meta = attr_meta base_meta = getattr(new_class, '_meta', None) - new_class.add_to_class('_meta', Options(meta)) + new_class.add_to_class('_meta', Options(meta, module)) if base_meta and base_meta.proxy: new_class._meta.interface = base_meta.interface # Add all attributes to the class. diff --git a/graphene/core/utils.py b/graphene/core/utils.py index 2441e644..d6ad10d4 100644 --- a/graphene/core/utils.py +++ b/graphene/core/utils.py @@ -14,16 +14,34 @@ def get_object_type(field_type, object_type=None): if field_type == 'self': field_type = object_type._meta.type else: - object_type = get_registered_object_type(field_type) + object_type = get_registered_object_type(field_type, object_type) field_type = object_type._meta.type return field_type -def get_registered_object_type(name): - for object_type in registered_object_types: - if object_type._meta.type_name == name: - return object_type - return None +def get_registered_object_type(name, object_type=None): + app_label = None + object_type_name = name + + if '.' in name: + app_label, object_type_name = name.split('.', 1) + elif object_type: + app_label = object_type._meta.app_label + + # Filter all registered object types which have the same name + ots = [ot for ot in registered_object_types if ot._meta.type_name == name] + # If the list have more than one object type with the name, filter by + # the app_label + if len(ots)>1 and app_label: + ots = [ot for ot in ots if ot._meta.app_label == app_label] + + if len(ots)>1: + raise Exception('Multiple ObjectTypes returned with the name %s' % name) + if not ots: + raise Exception('No ObjectType found with name %s' % name) + + return ots[0] + @signals.class_prepared.connect def object_type_created(sender): diff --git a/tests/starwars_relay/schema.py b/tests/starwars_relay/schema.py index a6a0a750..b2e65db5 100644 --- a/tests/starwars_relay/schema.py +++ b/tests/starwars_relay/schema.py @@ -21,7 +21,7 @@ def wrap_character(character): class Character(graphene.Interface): name = graphene.StringField() - friends = relay.Connection('self') + friends = relay.Connection('Character') appearsIn = graphene.ListField(Episode) def resolve_friends(self, args, *_): From e9cf8616ba5949810e0f8436e946bb8749840c5e Mon Sep 17 00:00:00 2001 From: Syrus Akbary <me@syrusakbary.com> Date: Fri, 25 Sep 2015 19:54:14 -0700 Subject: [PATCH 4/8] Improved app_label logic --- graphene/core/types.py | 7 ++++++- graphene/core/utils.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/graphene/core/types.py b/graphene/core/types.py index 9b136458..e83591b1 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -33,7 +33,12 @@ class ObjectTypeMeta(type): meta = attr_meta base_meta = getattr(new_class, '_meta', None) - new_class.add_to_class('_meta', Options(meta, module)) + if '.' in module: + app_label, _ = module.rsplit('.', 1) + else: + app_label = module + + new_class.add_to_class('_meta', Options(meta, app_label)) if base_meta and base_meta.proxy: new_class._meta.interface = base_meta.interface # Add all attributes to the class. diff --git a/graphene/core/utils.py b/graphene/core/utils.py index d6ad10d4..69039d89 100644 --- a/graphene/core/utils.py +++ b/graphene/core/utils.py @@ -24,12 +24,12 @@ def get_registered_object_type(name, object_type=None): object_type_name = name if '.' in name: - app_label, object_type_name = name.split('.', 1) + app_label, object_type_name = name.rsplit('.', 1) elif object_type: app_label = object_type._meta.app_label # Filter all registered object types which have the same name - ots = [ot for ot in registered_object_types if ot._meta.type_name == name] + ots = [ot for ot in registered_object_types if ot._meta.type_name == object_type_name] # If the list have more than one object type with the name, filter by # the app_label if len(ots)>1 and app_label: From d2dc25cc07a60c9bfaa1cd33463fd87609f297da Mon Sep 17 00:00:00 2001 From: Syrus Akbary <me@syrusakbary.com> Date: Fri, 25 Sep 2015 20:01:14 -0700 Subject: [PATCH 5/8] Improved syntax in starwars --- tests/starwars/schema.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/starwars/schema.py b/tests/starwars/schema.py index 018a1fc8..9fbee302 100644 --- a/tests/starwars/schema.py +++ b/tests/starwars/schema.py @@ -9,21 +9,24 @@ Episode = graphene.Enum('Episode', dict( JEDI = 6 )) + def wrap_character(character): if isinstance(character, _Human): return Human(character) elif isinstance(character, _Droid): return Droid(character) + class Character(graphene.Interface): id = graphene.IDField() name = graphene.StringField() - friends = graphene.ListField('Character') + friends = graphene.ListField('self') appearsIn = graphene.ListField(Episode) def resolve_friends(self, args, *_): return [wrap_character(getCharacter(f)) for f in self.instance.friends] + class Human(Character): homePlanet = graphene.StringField() @@ -50,8 +53,6 @@ class Query(graphene.ObjectType): @resolve_only_args def resolve_human(self, id): return wrap_character(getHuman(id)) - if human: - return Human(human) @resolve_only_args def resolve_droid(self, id): From 1b7caac39b255035ce04cccb07c54e85f8bb07a8 Mon Sep 17 00:00:00 2001 From: Syrus Akbary <me@syrusakbary.com> Date: Fri, 25 Sep 2015 23:25:10 -0700 Subject: [PATCH 6/8] =?UTF-8?q?First=20working=20version=20with=20relay=20?= =?UTF-8?q?=F0=9F=92=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- graphene/__init__.py | 5 - graphene/core/fields.py | 6 + graphene/core/options.py | 1 - graphene/core/types.py | 4 + graphene/core/utils.py | 16 ++- graphene/relay/__init__.py | 87 ++++++++++++++- tests/core/test_types.py | 4 +- tests/starwars_relay/__init__.py | 0 tests/starwars_relay/data.py | 98 ++++++++++++++++ tests/starwars_relay/schema.py | 74 ++++++------ tests/starwars_relay/schema_other.py | 60 ++++++++++ tests/starwars_relay/test_connections.py | 37 ++++++ .../test_objectidentification.py | 105 ++++++++++++++++++ 13 files changed, 441 insertions(+), 56 deletions(-) create mode 100644 tests/starwars_relay/__init__.py create mode 100644 tests/starwars_relay/data.py create mode 100644 tests/starwars_relay/schema_other.py create mode 100644 tests/starwars_relay/test_connections.py create mode 100644 tests/starwars_relay/test_objectidentification.py diff --git a/graphene/__init__.py b/graphene/__init__.py index 6024313c..c84eeb8c 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -1,7 +1,6 @@ from graphql.core.type import ( GraphQLEnumType as Enum, GraphQLArgument as Argument, - # GraphQLSchema as Schema, GraphQLString as String, GraphQLInt as Int, GraphQLID as ID @@ -26,7 +25,3 @@ from graphene.core.types import ( from graphene.decorators import ( resolve_only_args ) - -from graphene.relay import ( - Relay -) diff --git a/graphene/core/fields.py b/graphene/core/fields.py index d37fed1d..478e3630 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -99,6 +99,12 @@ class Field(object): return '<%s>' % path +class NativeField(Field): + def __init__(self, field=None): + super(NativeField, self).__init__(None) + self.field = field or getattr(self, 'field') + + class TypeField(Field): def __init__(self, *args, **kwargs): super(TypeField, self).__init__(self.field_type, *args, **kwargs) diff --git a/graphene/core/options.py b/graphene/core/options.py index a277be15..c3dc13af 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -54,7 +54,6 @@ class Options(object): def add_field(self, field): self.local_fields.append(field) - setattr(self.parent, field.field_name, field) @cached_property def fields(self): diff --git a/graphene/core/types.py b/graphene/core/types.py index e83591b1..3ffb466d 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -91,6 +91,10 @@ class ObjectType(six.with_metaclass(ObjectTypeMeta)): self.instance = instance signals.post_init.send(self.__class__, instance=self) + def __getattr__(self, name): + if self.instance: + return getattr(self.instance, name) + def get_field(self, field): return getattr(self.instance, field, None) diff --git a/graphene/core/utils.py b/graphene/core/utils.py index 69039d89..b50a7664 100644 --- a/graphene/core/utils.py +++ b/graphene/core/utils.py @@ -7,17 +7,23 @@ registered_object_types = [] def get_object_type(field_type, object_type=None): + native_type = get_type(field_type, object_type) + if native_type: + field_type = native_type._meta.type + return field_type + + +def get_type(field_type, object_type=None): _is_class = inspect.isclass(field_type) if _is_class and issubclass(field_type, ObjectType): - field_type = field_type._meta.type + return field_type elif isinstance(field_type, basestring): if field_type == 'self': - field_type = object_type._meta.type + return object_type else: object_type = get_registered_object_type(field_type, object_type) - field_type = object_type._meta.type - return field_type - + return object_type + return None def get_registered_object_type(name, object_type=None): app_label = None diff --git a/graphene/relay/__init__.py b/graphene/relay/__init__.py index 80feb122..d93aa7ac 100644 --- a/graphene/relay/__init__.py +++ b/graphene/relay/__init__.py @@ -1,2 +1,85 @@ -class Relay(object): - pass +import collections + +from graphene import signals +from graphene.core.fields import Field, NativeField +from graphene.core.types import Interface +from graphene.core.utils import get_type +from graphene.utils import cached_property + +from graphql_relay.node.node import ( + nodeDefinitions, + globalIdField, + fromGlobalId +) +from graphql_relay.connection.arrayconnection import ( + connectionFromArray +) +from graphql_relay.connection.connection import ( + connectionArgs, + connectionDefinitions +) + +registered_nodes = {} + + +def getNode(globalId, *args): + resolvedGlobalId = fromGlobalId(globalId) + _type, _id = resolvedGlobalId.type, resolvedGlobalId.id + if _type in registered_nodes: + object_type = registered_nodes[_type] + return object_type.get_node(_id) + + +def getNodeType(obj): + return obj._meta.type + + +_nodeDefinitions = nodeDefinitions(getNode, getNodeType) + + +class Node(Interface): + @classmethod + def get_graphql_type(cls): + if cls is Node: + # Return only nodeInterface when is the Node Inerface + return _nodeDefinitions.nodeInterface + return super(Node, cls).get_graphql_type() + + +class NodeField(NativeField): + field = _nodeDefinitions.nodeField + + +class ConnectionField(Field): + def __init__(self, field_type, resolve=None, description=''): + super(ConnectionField, self).__init__(field_type, resolve=resolve, + args=connectionArgs, description=description) + + def resolve(self, instance, args, info): + resolved = super(ConnectionField, self).resolve(instance, args, info) + if resolved: + assert isinstance(resolved, collections.Iterable), 'Resolved value from the connection field have to be iterable' + return connectionFromArray(resolved, args) + + @cached_property + def type(self): + object_type = get_type(self.field_type, self.object_type) + assert issubclass(object_type, Node), 'Only nodes have connections.' + return object_type.connection + + +@signals.class_prepared.connect +def object_type_created(object_type): + if issubclass(object_type, Node): + type_name = object_type._meta.type_name + assert type_name not in registered_nodes, 'Two nodes with the same type_name: %s' % type_name + registered_nodes[type_name] = object_type + # def getId(*args, **kwargs): + # print '**GET ID', args, kwargs + # return 2 + field = NativeField(globalIdField(type_name)) + object_type.add_to_class('id', field) + assert hasattr(object_type, 'get_node'), 'get_node classmethod not found in %s Node' % type_name + + connection = connectionDefinitions(type_name, object_type._meta.type).connectionType + object_type.add_to_class('connection', connection) diff --git a/tests/core/test_types.py b/tests/core/test_types.py index e8d3ec69..b17cae48 100644 --- a/tests/core/test_types.py +++ b/tests/core/test_types.py @@ -29,7 +29,7 @@ def test_interface(): assert Character._meta.type_name == 'Character' assert isinstance(object_type, GraphQLInterfaceType) assert object_type.description == 'Character description' - assert object_type.get_fields() == {'name': Character.name.field} + assert object_type.get_fields() == {'name': Character._meta.fields_map['name'].field} def test_object_type(): object_type = Human._meta.type @@ -37,5 +37,5 @@ def test_object_type(): assert Human._meta.type_name == 'Human' assert isinstance(object_type, GraphQLObjectType) assert object_type.description == 'Human description' - assert object_type.get_fields() == {'name': Character.name.field, 'friends': Human.friends.field} + assert object_type.get_fields() == {'name': Character._meta.fields_map['name'].field, 'friends': Human._meta.fields_map['friends'].field} assert object_type.get_interfaces() == [Character._meta.type] diff --git a/tests/starwars_relay/__init__.py b/tests/starwars_relay/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/starwars_relay/data.py b/tests/starwars_relay/data.py new file mode 100644 index 00000000..31ac591d --- /dev/null +++ b/tests/starwars_relay/data.py @@ -0,0 +1,98 @@ +from collections import namedtuple + +Ship = namedtuple('Ship',['id', 'name']) +Faction = namedtuple('Faction',['id', 'name', 'ships']) + +xwing = Ship( + id='1', + name='X-Wing', +) + +ywing = Ship( + id='2', + name='Y-Wing', +) + +awing = Ship( + id='3', + name='A-Wing', +) + +# Yeah, technically it's Corellian. But it flew in the service of the rebels, +# so for the purposes of this demo it's a rebel ship. +falcon = Ship( + id='4', + name='Millenium Falcon', +) + +homeOne = Ship( + id='5', + name='Home One', +) + +tieFighter = Ship( + id='6', + name='TIE Fighter', +) + +tieInterceptor = Ship( + id='7', + name='TIE Interceptor', +) + +executor = Ship( + id='8', + name='Executor', +) + +rebels = Faction( + id='1', + name='Alliance to Restore the Republic', + ships=['1', '2', '3', '4', '5'] +) + +empire = Faction( + id='2', + name='Galactic Empire', + ships= ['6', '7', '8'] +) + +data = { + 'Faction': { + '1': rebels, + '2': empire + }, + 'Ship': { + '1': xwing, + '2': ywing, + '3': awing, + '4': falcon, + '5': homeOne, + '6': tieFighter, + '7': tieInterceptor, + '8': executor + } +} + +def createShip(shipName, factionId): + nextShip = len(data['Ship'].keys())+1 + newShip = Ship( + id=str(nextShip), + name=shipName + ) + data['Ship'][newShip.id] = newShip + data['Faction'][factionId].ships.append(newShip.id) + return newShip + + +def getShip(_id): + return data['Ship'][_id] + +def getFaction(_id): + return data['Faction'][_id] + +def getRebels(): + return rebels + +def getEmpire(): + return empire diff --git a/tests/starwars_relay/schema.py b/tests/starwars_relay/schema.py index b2e65db5..55e03e6c 100644 --- a/tests/starwars_relay/schema.py +++ b/tests/starwars_relay/schema.py @@ -2,60 +2,52 @@ import graphene from graphene import resolve_only_args, relay from .data import ( - getHero, getHuman, getCharacter, getDroid, - Human as _Human, Droid as _Droid) - -Episode = graphene.Enum('Episode', dict( - NEWHOPE=4, - EMPIRE=5, - JEDI=6 -)) + getFaction, + getShip, + getRebels, + getEmpire, +) -def wrap_character(character): - if isinstance(character, _Human): - return Human(character) - elif isinstance(character, _Droid): - return Droid(character) +class Ship(relay.Node): + '''A ship in the Star Wars saga''' + name = graphene.StringField(description='The name of the ship.') + + @classmethod + def get_node(cls, id): + ship = getShip(id) + if ship: + return Ship(ship) -class Character(graphene.Interface): - name = graphene.StringField() - friends = relay.Connection('Character') - appearsIn = graphene.ListField(Episode) +class Faction(relay.Node): + '''A faction in the Star Wars saga''' + name = graphene.StringField(description='The name of the faction.') + ships = relay.ConnectionField(Ship, description='The ships used by the faction.') - def resolve_friends(self, args, *_): - return [wrap_character(getCharacter(f)) for f in self.instance.friends] + @resolve_only_args + def resolve_ships(self, **kwargs): + return [Ship(getShip(ship)) for ship in self.instance.ships] - -class Human(relay.Node, Character): - homePlanet = graphene.StringField() - - -class Droid(relay.Node, Character): - primaryFunction = graphene.StringField() + @classmethod + def get_node(cls, id): + faction = getFaction(id) + if faction: + return Faction(faction) class Query(graphene.ObjectType): - hero = graphene.Field(Character, - episode=graphene.Argument(Episode)) - human = graphene.Field(Human, - id=graphene.Argument(graphene.String)) - droid = graphene.Field(Droid, - id=graphene.Argument(graphene.String)) - node = graphene.Field(relay.Node) + rebels = graphene.Field(Faction) + empire = graphene.Field(Faction) + node = relay.NodeField() @resolve_only_args - def resolve_hero(self, episode): - return wrap_character(getHero(episode)) + def resolve_rebels(self): + return Faction(getRebels()) @resolve_only_args - def resolve_human(self, id): - return wrap_character(getHuman(id)) - - @resolve_only_args - def resolve_droid(self, id): - return wrap_character(getDroid(id)) + def resolve_empire(self): + return Faction(getEmpire()) Schema = graphene.Schema(query=Query) diff --git a/tests/starwars_relay/schema_other.py b/tests/starwars_relay/schema_other.py new file mode 100644 index 00000000..dd33bba7 --- /dev/null +++ b/tests/starwars_relay/schema_other.py @@ -0,0 +1,60 @@ +import graphene +from graphene import resolve_only_args, relay + +from .data import ( + getHero, getHuman, getCharacter, getDroid, + Human as _Human, Droid as _Droid) + +Episode = graphene.Enum('Episode', dict( + NEWHOPE=4, + EMPIRE=5, + JEDI=6 +)) + +def wrap_character(character): + if isinstance(character, _Human): + return Human(character) + elif isinstance(character, _Droid): + return Droid(character) + + +class Character(graphene.Interface): + name = graphene.StringField() + friends = relay.Connection('Character') + appearsIn = graphene.ListField(Episode) + + def resolve_friends(self, args, *_): + return [wrap_character(getCharacter(f)) for f in self.instance.friends] + + +class Human(relay.Node, Character): + homePlanet = graphene.StringField() + + +class Droid(relay.Node, Character): + primaryFunction = graphene.StringField() + + +class Query(graphene.ObjectType): + hero = graphene.Field(Character, + episode=graphene.Argument(Episode)) + human = graphene.Field(Human, + id=graphene.Argument(graphene.String)) + droid = graphene.Field(Droid, + id=graphene.Argument(graphene.String)) + node = relay.NodeField() + + @resolve_only_args + def resolve_hero(self, episode): + return wrap_character(getHero(episode)) + + @resolve_only_args + def resolve_human(self, id): + return wrap_character(getHuman(id)) + + @resolve_only_args + def resolve_droid(self, id): + return wrap_character(getDroid(id)) + + +Schema = graphene.Schema(query=Query) diff --git a/tests/starwars_relay/test_connections.py b/tests/starwars_relay/test_connections.py new file mode 100644 index 00000000..1bac0a4d --- /dev/null +++ b/tests/starwars_relay/test_connections.py @@ -0,0 +1,37 @@ +from pytest import raises +from graphql.core import graphql + +from .schema import Schema + +def test_correct_fetch_first_ship_rebels(): + query = ''' + query RebelsShipsQuery { + rebels { + name, + ships(first: 1) { + edges { + node { + name + } + } + } + } + } + ''' + expected = { + 'rebels': { + 'name': 'Alliance to Restore the Republic', + 'ships': { + 'edges': [ + { + 'node': { + 'name': 'X-Wing' + } + } + ] + } + } + } + result = Schema.execute(query) + assert result.errors == None + assert result.data == expected diff --git a/tests/starwars_relay/test_objectidentification.py b/tests/starwars_relay/test_objectidentification.py new file mode 100644 index 00000000..85050b6f --- /dev/null +++ b/tests/starwars_relay/test_objectidentification.py @@ -0,0 +1,105 @@ +from pytest import raises +from graphql.core import graphql + +from .schema import Schema + +def test_correctly_fetches_id_name_rebels(): + query = ''' + query RebelsQuery { + rebels { + id + name + } + } + ''' + expected = { + 'rebels': { + 'id': 'RmFjdGlvbjox', + 'name': 'Alliance to Restore the Republic' + } + } + result = Schema.execute(query) + assert result.errors == None + assert result.data == expected + +def test_correctly_refetches_rebels(): + query = ''' + query RebelsRefetchQuery { + node(id: "RmFjdGlvbjox") { + id + ... on Faction { + name + } + } + } + ''' + expected = { + 'node': { + 'id': 'RmFjdGlvbjox', + 'name': 'Alliance to Restore the Republic' + } + } + result = Schema.execute(query) + assert result.errors == None + assert result.data == expected + +def test_correctly_fetches_id_name_empire(): + query = ''' + query EmpireQuery { + empire { + id + name + } + } + ''' + expected = { + 'empire': { + 'id': 'RmFjdGlvbjoy', + 'name': 'Galactic Empire' + } + } + result = Schema.execute(query) + assert result.errors == None + assert result.data == expected + +def test_correctly_refetches_empire(): + query = ''' + query EmpireRefetchQuery { + node(id: "RmFjdGlvbjoy") { + id + ... on Faction { + name + } + } + } + ''' + expected = { + 'node': { + 'id': 'RmFjdGlvbjoy', + 'name': 'Galactic Empire' + } + } + result = Schema.execute(query) + assert result.errors == None + assert result.data == expected + +def test_correctly_refetches_xwing(): + query = ''' + query XWingRefetchQuery { + node(id: "U2hpcDox") { + id + ... on Ship { + name + } + } + } + ''' + expected = { + 'node': { + 'id': 'U2hpcDox', + 'name': 'X-Wing' + } + } + result = Schema.execute(query) + assert result.errors == None + assert result.data == expected From 2d87f527bf5f6d74f271b5b6649119a781243d55 Mon Sep 17 00:00:00 2001 From: Syrus Akbary <me@syrusakbary.com> Date: Fri, 25 Sep 2015 23:31:53 -0700 Subject: [PATCH 7/8] Added Relay Schema example --- README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/README.md b/README.md index 73290509..187481ed 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,62 @@ query = ''' result = Schema.execute(query) ``` +### Relay Schema + +Graphene also supports Relay, check the (Starwars Relay example)[/tests/starwars_relay]! + +```python +import graphene +from graphene import relay + +class Ship(relay.Node): + '''A ship in the Star Wars saga''' + name = graphene.StringField(description='The name of the ship.') + + @classmethod + def get_node(cls, id): + ship = getShip(id) + if ship: + return Ship(ship) + + +class Faction(relay.Node): + '''A faction in the Star Wars saga''' + name = graphene.StringField(description='The name of the faction.') + ships = relay.ConnectionField(Ship, description='The ships used by the faction.') + + @resolve_only_args + def resolve_ships(self, **kwargs): + return [Ship(getShip(ship)) for ship in self.instance.ships] + + @classmethod + def get_node(cls, id): + faction = getFaction(id) + if faction: + return Faction(faction) + + +class Query(graphene.ObjectType): + rebels = graphene.Field(Faction) + empire = graphene.Field(Faction) + node = relay.NodeField() + + @resolve_only_args + def resolve_rebels(self): + return Faction(getRebels()) + + @resolve_only_args + def resolve_empire(self): + return Faction(getEmpire()) + + +Schema = graphene.Schema(query=Query) + +# Later on, for querying +Schema.execute('''rebels { name }''') + +``` + ## Contributing After cloning this repo, ensure dependencies are installed by running: From dde58ae4b1800ecc072d7c8ea4c64704d9f5aff3 Mon Sep 17 00:00:00 2001 From: Syrus Akbary <me@syrusakbary.com> Date: Fri, 25 Sep 2015 23:48:53 -0700 Subject: [PATCH 8/8] Added some relay tests. --- tests/relay/test_relay.py | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/relay/test_relay.py diff --git a/tests/relay/test_relay.py b/tests/relay/test_relay.py new file mode 100644 index 00000000..c532949f --- /dev/null +++ b/tests/relay/test_relay.py @@ -0,0 +1,41 @@ +from pytest import raises + +import graphene +from graphene import relay + + +class OtherNode(relay.Node): + name = graphene.StringField() + + @classmethod + def get_node(cls, id): + pass + + +def test_field_no_contributed_raises_error(): + with raises(Exception) as excinfo: + class Part(relay.Node): + x = graphene.StringField() + + assert 'get_node' in str(excinfo.value) + + +def test_node_should_have_connection(): + assert OtherNode.connection + + +def test_node_should_have_id_field(): + assert 'id' in OtherNode._meta.fields_map + + +def test_field_no_contributed_raises_error(): + with raises(Exception) as excinfo: + class Ship(graphene.ObjectType): + name = graphene.StringField() + + + class Faction(relay.Node): + name = graphene.StringField() + ships = relay.ConnectionField(Ship) + + assert 'same type_name' in str(excinfo.value)