From 9a84d595a1e5d151ee0e41400d2cce63fb08f974 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 25 Sep 2015 16:35:17 -0700 Subject: [PATCH 01/77] 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 Date: Fri, 25 Sep 2015 16:36:18 -0700 Subject: [PATCH 02/77] 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 Date: Fri, 25 Sep 2015 19:41:11 -0700 Subject: [PATCH 03/77] 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 Date: Fri, 25 Sep 2015 19:54:14 -0700 Subject: [PATCH 04/77] 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 Date: Fri, 25 Sep 2015 20:01:14 -0700 Subject: [PATCH 05/77] 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 Date: Fri, 25 Sep 2015 23:25:10 -0700 Subject: [PATCH 06/77] =?UTF-8?q?First=20working=20version=20with=20relay?= =?UTF-8?q?=20=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 Date: Fri, 25 Sep 2015 23:31:53 -0700 Subject: [PATCH 07/77] 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 Date: Fri, 25 Sep 2015 23:48:53 -0700 Subject: [PATCH 08/77] 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) From 8f66593cb2ae2a6de802c21d43aa440247b440d3 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 26 Sep 2015 00:07:01 -0700 Subject: [PATCH 09/77] Updated Graphene main description --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 187481ed..0b3e0019 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Graphene: GraphQL Object Mapper +# Graphene: Python DSL for GraphQL This is a library to use GraphQL in Python in a easy way. It will map the models/fields to internal GraphQL-py objects without effort. diff --git a/setup.py b/setup.py index 705d1ac3..739f729f 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ setup( name='graphene', version='0.1', - description='Graphene: GraphQL Object Mapper', + description='Graphene: Python DSL for GraphQL', url='https://github.com/syrusakbary/graphene', From eafc9a102e440cc3152034a681cd966f2c5753a4 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 26 Sep 2015 03:36:00 -0700 Subject: [PATCH 10/77] Improved tests and schema definition. --- graphene/__init__.py | 21 ++++++--- graphene/core/fields.py | 23 +++++++-- graphene/core/options.py | 7 +-- graphene/core/schema.py | 73 +++++++++++++++++++++++++++++ graphene/core/types.py | 29 ++---------- graphene/core/utils.py | 54 --------------------- graphene/env.py | 9 ++++ graphene/relay/__init__.py | 86 ++-------------------------------- graphene/relay/nodes.py | 37 +++++++++++++++ graphene/relay/relay.py | 51 ++++++++++++++++++++ tests/core/test_types.py | 9 ++-- tests/relay/test_relay.py | 26 ++++++---- tests/starwars/schema.py | 3 ++ tests/starwars_relay/schema.py | 14 +++++- 14 files changed, 255 insertions(+), 187 deletions(-) create mode 100644 graphene/core/schema.py delete mode 100644 graphene/core/utils.py create mode 100644 graphene/env.py create mode 100644 graphene/relay/nodes.py create mode 100644 graphene/relay/relay.py diff --git a/graphene/__init__.py b/graphene/__init__.py index c84eeb8c..4fd823ce 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -6,6 +6,21 @@ from graphql.core.type import ( GraphQLID as ID ) +from graphene import signals + +from graphene.core.schema import ( + Schema +) + +from graphene.env import ( + get_global_schema +) + +from graphene.core.types import ( + ObjectType, + Interface +) + from graphene.core.fields import ( Field, StringField, @@ -16,12 +31,6 @@ from graphene.core.fields import ( NonNullField, ) -from graphene.core.types import ( - ObjectType, - Interface, - Schema -) - from graphene.decorators import ( resolve_only_args ) diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 478e3630..430ec9f9 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -1,3 +1,4 @@ +import inspect from graphql.core.type import ( GraphQLField, GraphQLList, @@ -9,7 +10,7 @@ from graphql.core.type import ( GraphQLArgument, ) from graphene.utils import cached_property -from graphene.core.utils import get_object_type +from graphene.core.types import ObjectType class Field(object): def __init__(self, field_type, resolve=None, null=True, args=None, description='', **extra_args): @@ -25,6 +26,7 @@ class Field(object): def contribute_to_class(self, cls, name): self.field_name = name self.object_type = cls + self.schema = cls._meta.schema if isinstance(self.field_type, Field) and not self.field_type.object_type: self.field_type.contribute_to_class(cls, name) cls._meta.add_field(self) @@ -42,12 +44,27 @@ class Field(object): resolve_fn = lambda root, args, info: root.resolve(self.field_name, args, info) return resolve_fn(instance, args, info) + def get_object_type(self): + field_type = self.field_type + _is_class = inspect.isclass(field_type) + if _is_class and issubclass(field_type, ObjectType): + return field_type + elif isinstance(field_type, basestring): + if field_type == 'self': + return self.object_type + elif self.schema: + return self.schema.get_type(field_type) + @cached_property def type(self): - if isinstance(self.field_type, Field): + field_type = self.field_type + if isinstance(field_type, Field): field_type = self.field_type.type else: - field_type = get_object_type(self.field_type, self.object_type) + object_type = self.get_object_type() + if object_type: + field_type = object_type._meta.type + field_type = self.type_wrapper(field_type) return field_type diff --git a/graphene/core/options.py b/graphene/core/options.py index c3dc13af..e5eac894 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -1,18 +1,19 @@ +from graphene.env import get_global_schema from graphene.utils import cached_property -DEFAULT_NAMES = ('app_label', 'description', 'name', 'interface', +DEFAULT_NAMES = ('description', 'name', 'interface', 'schema', 'type_name', 'interfaces', 'proxy') class Options(object): - def __init__(self, meta=None, app_label=None): + def __init__(self, meta=None, schema=None): self.meta = meta self.local_fields = [] self.interface = False self.proxy = False + self.schema = schema or get_global_schema() self.interfaces = [] self.parents = [] - self.app_label = app_label def contribute_to_class(self, cls, name): cls._meta = self diff --git a/graphene/core/schema.py b/graphene/core/schema.py new file mode 100644 index 00000000..4a5519af --- /dev/null +++ b/graphene/core/schema.py @@ -0,0 +1,73 @@ +from graphql.core import graphql +from graphql.core.type import ( + GraphQLSchema +) +from graphene import signals +from graphene.utils import cached_property +# from graphene.relay.nodes import create_node_definitions + +class Schema(object): + _query = None + + def __init__(self, query=None, mutation=None, name='Schema'): + self.mutation = mutation + self.query = query + self.name = name + self._types = {} + + def __repr__(self): + return '' % str(self.name) + + # @cachedproperty + # def node_definitions(self): + # return [object, object] + # # from graphene.relay import create_node_definitions + # # return create_node_definitions(schema=self) + + # @property + # def Node(self): + # return self.node_definitions[0] + + # @property + # def NodeField(self): + # return self.node_definitions[1] + + @property + def query(self): + return self._query + @query.setter + def query(self, query): + if not query: + return + self._query = query + self._query_type = query._meta.type + self._schema = GraphQLSchema(query=self._query_type, mutation=self.mutation) + + def register_type(self, type): + type_name = type._meta.type_name + if type_name in self._types: + raise Exception('Type name %s already registered in %r' % (type_name, self)) + self._types[type_name] = type + + def get_type(self, type_name): + if type_name not in self._types: + raise Exception('Type %s not found in %r' % (type_name, self)) + return self._types[type_name] + + def execute(self, request='', root=None, vars=None, operation_name=None): + return graphql( + self._schema, + request=request, + root=root or self.query(), + vars=vars, + operation_name=operation_name + ) + + +@signals.class_prepared.connect +def object_type_created(object_type): + schema = object_type._meta.schema + if schema: + schema.register_type(object_type) + +from graphene.env import get_global_schema diff --git a/graphene/core/types.py b/graphene/core/types.py index 3ffb466d..f020a72a 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -3,10 +3,8 @@ import six from graphql.core.type import ( GraphQLObjectType, - GraphQLInterfaceType, - GraphQLSchema + GraphQLInterfaceType ) -from graphql.core import graphql from graphene import signals from graphene.core.options import Options @@ -33,12 +31,9 @@ class ObjectTypeMeta(type): meta = attr_meta base_meta = getattr(new_class, '_meta', None) - if '.' in module: - app_label, _ = module.rsplit('.', 1) - else: - app_label = module + schema = (base_meta and base_meta.schema) - new_class.add_to_class('_meta', Options(meta, app_label)) + new_class.add_to_class('_meta', Options(meta, schema)) if base_meta and base_meta.proxy: new_class._meta.interface = base_meta.interface # Add all attributes to the class. @@ -54,6 +49,8 @@ class ObjectTypeMeta(type): # Things without _meta aren't functional models, so they're # uninteresting parents. continue + if base._meta.schema != new_class._meta.schema: + raise Exception('The parent schema is not the same') parent_fields = base._meta.local_fields # Check for clashes between locally declared fields and those @@ -138,19 +135,3 @@ class Interface(ObjectType): class Meta: interface = True proxy = True - - -class Schema(object): - def __init__(self, query, mutation=None): - 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, - request=request, - root=root or self.query(), - vars=vars, - operation_name=operation_name - ) diff --git a/graphene/core/utils.py b/graphene/core/utils.py deleted file mode 100644 index b50a7664..00000000 --- a/graphene/core/utils.py +++ /dev/null @@ -1,54 +0,0 @@ -import inspect - -from graphene.core.types import ObjectType -from graphene import signals - -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): - return field_type - elif isinstance(field_type, basestring): - if field_type == 'self': - return object_type - else: - object_type = get_registered_object_type(field_type, object_type) - 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.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 == 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: - 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): - registered_object_types.append(sender) diff --git a/graphene/env.py b/graphene/env.py new file mode 100644 index 00000000..604dbc2c --- /dev/null +++ b/graphene/env.py @@ -0,0 +1,9 @@ +from graphene.core.schema import Schema + +_global_schema = None + +def get_global_schema(): + global _global_schema + if not _global_schema: + _global_schema = Schema(name='Global Schema') + return _global_schema diff --git a/graphene/relay/__init__.py b/graphene/relay/__init__.py index d93aa7ac..b7a71ce5 100644 --- a/graphene/relay/__init__.py +++ b/graphene/relay/__init__.py @@ -1,85 +1,5 @@ -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 +from graphene.relay.nodes import ( + create_node_definitions ) -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) +from graphene.relay.relay import * diff --git a/graphene/relay/nodes.py b/graphene/relay/nodes.py new file mode 100644 index 00000000..81223a52 --- /dev/null +++ b/graphene/relay/nodes.py @@ -0,0 +1,37 @@ +from graphql_relay.node.node import ( + nodeDefinitions, + fromGlobalId +) + +def create_node_definitions(getNode=None, getNodeType=None, schema=None): + from graphene.core.types import Interface + from graphene.core.fields import Field, NativeField + if not getNode: + def getNode(globalId, *args): + from graphene.env import get_global_schema + _schema = schema or get_global_schema() + resolvedGlobalId = fromGlobalId(globalId) + _type, _id = resolvedGlobalId.type, resolvedGlobalId.id + object_type = _schema.get_type(_type) + return object_type.get_node(_id) + + if not getNodeType: + 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 + + return Node, NodeField diff --git a/graphene/relay/relay.py b/graphene/relay/relay.py new file mode 100644 index 00000000..2df13e81 --- /dev/null +++ b/graphene/relay/relay.py @@ -0,0 +1,51 @@ +import collections + +from graphene import signals +from graphene.utils import cached_property + +from graphql_relay.node.node import ( + globalIdField +) +from graphql_relay.connection.arrayconnection import ( + connectionFromArray +) +from graphql_relay.connection.connection import ( + connectionArgs, + connectionDefinitions +) +from graphene.relay.nodes import create_node_definitions +from graphene.core.fields import Field, NativeField + +Node, NodeField = create_node_definitions() + +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 = self.get_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 + # 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 b17cae48..1408caa2 100644 --- a/tests/core/test_types.py +++ b/tests/core/test_types.py @@ -18,15 +18,18 @@ from graphene.core.types import ( class Character(Interface): '''Character description''' name = StringField() - + class Meta: + type_name = 'core.Character' class Human(Character): '''Human description''' friends = StringField() + class Meta: + type_name = 'core.Human' def test_interface(): object_type = Character._meta.type assert Character._meta.interface == True - assert Character._meta.type_name == 'Character' + assert Character._meta.type_name == 'core.Character' assert isinstance(object_type, GraphQLInterfaceType) assert object_type.description == 'Character description' assert object_type.get_fields() == {'name': Character._meta.fields_map['name'].field} @@ -34,7 +37,7 @@ def test_interface(): def test_object_type(): object_type = Human._meta.type assert Human._meta.interface == False - assert Human._meta.type_name == 'Human' + assert Human._meta.type_name == 'core.Human' assert isinstance(object_type, GraphQLObjectType) assert object_type.description == 'Human description' assert object_type.get_fields() == {'name': Character._meta.fields_map['name'].field, 'friends': Human._meta.fields_map['friends'].field} diff --git a/tests/relay/test_relay.py b/tests/relay/test_relay.py index c532949f..2bcdc836 100644 --- a/tests/relay/test_relay.py +++ b/tests/relay/test_relay.py @@ -3,6 +3,7 @@ from pytest import raises import graphene from graphene import relay +schema = graphene.Schema() class OtherNode(relay.Node): name = graphene.StringField() @@ -28,14 +29,19 @@ 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() +# def test_field_no_contributed_raises_error(): +# with raises(Exception) as excinfo: +# class Ship(graphene.ObjectType): +# name = graphene.StringField() +# class Meta: +# schema = schema - - class Faction(relay.Node): - name = graphene.StringField() - ships = relay.ConnectionField(Ship) - - assert 'same type_name' in str(excinfo.value) +# class Faction(relay.Node): +# name = graphene.StringField() +# ships = relay.ConnectionField(Ship) +# @classmethod +# def get_node(cls): +# pass +# class Meta: +# schema = schema +# assert 'same type_name' in str(excinfo.value) diff --git a/tests/starwars/schema.py b/tests/starwars/schema.py index 9fbee302..d3527a83 100644 --- a/tests/starwars/schema.py +++ b/tests/starwars/schema.py @@ -46,6 +46,9 @@ class Query(graphene.ObjectType): id = graphene.Argument(graphene.String) ) + class Meta: + type_name = 'core.Query' + @resolve_only_args def resolve_hero(self, episode): return wrap_character(getHero(episode)) diff --git a/tests/starwars_relay/schema.py b/tests/starwars_relay/schema.py index 55e03e6c..f1a12050 100644 --- a/tests/starwars_relay/schema.py +++ b/tests/starwars_relay/schema.py @@ -8,6 +8,7 @@ from .data import ( getEmpire, ) +schema = graphene.Schema() class Ship(relay.Node): '''A ship in the Star Wars saga''' @@ -19,6 +20,8 @@ class Ship(relay.Node): if ship: return Ship(ship) + # class Meta: + # schema = schema class Faction(relay.Node): '''A faction in the Star Wars saga''' @@ -35,6 +38,9 @@ class Faction(relay.Node): if faction: return Faction(faction) + # class Meta: + # schema = schema + class Query(graphene.ObjectType): rebels = graphene.Field(Faction) @@ -50,4 +56,10 @@ class Query(graphene.ObjectType): return Faction(getEmpire()) -Schema = graphene.Schema(query=Query) + # class Meta: + # schema = schema + +print '*CACA', schema._types + +schema.query = Query +Schema = schema From 311209760db9d8548133bf34e9394662678574fc Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 27 Sep 2015 20:19:33 -0700 Subject: [PATCH 11/77] Refactored code allowing multiple schemas at the same time. --- graphene/__init__.py | 2 + graphene/core/fields.py | 2 +- graphene/core/options.py | 2 +- graphene/core/schema.py | 24 ++------ graphene/core/types.py | 20 ++++++- graphene/relay/__init__.py | 17 +++++- graphene/relay/connections.py | 35 ++++++++++++ graphene/relay/fields.py | 30 ++++++++++ graphene/relay/nodes.py | 35 +++++++----- graphene/relay/relay.py | 57 ++++--------------- graphene/relay/utils.py | 3 + graphene/signals.py | 1 + tests/core/test_types.py | 1 + tests/relay/test_relay.py | 1 + tests/starwars_relay/schema.py | 15 +---- tests/starwars_relay/test_connections.py | 4 +- .../test_objectidentification.py | 12 ++-- 17 files changed, 153 insertions(+), 108 deletions(-) create mode 100644 graphene/relay/connections.py create mode 100644 graphene/relay/fields.py create mode 100644 graphene/relay/utils.py diff --git a/graphene/__init__.py b/graphene/__init__.py index 4fd823ce..61996539 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -34,3 +34,5 @@ from graphene.core.fields import ( from graphene.decorators import ( resolve_only_args ) + +import graphene.relay diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 430ec9f9..383138d6 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -10,7 +10,6 @@ from graphql.core.type import ( GraphQLArgument, ) from graphene.utils import cached_property -from graphene.core.types import ObjectType class Field(object): def __init__(self, field_type, resolve=None, null=True, args=None, description='', **extra_args): @@ -45,6 +44,7 @@ class Field(object): return resolve_fn(instance, args, info) def get_object_type(self): + from graphene.core.types import ObjectType field_type = self.field_type _is_class = inspect.isclass(field_type) if _is_class and issubclass(field_type, ObjectType): diff --git a/graphene/core/options.py b/graphene/core/options.py index e5eac894..5d0263f0 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -11,7 +11,7 @@ class Options(object): self.local_fields = [] self.interface = False self.proxy = False - self.schema = schema or get_global_schema() + self.schema = schema self.interfaces = [] self.parents = [] diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 4a5519af..132a4760 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -4,7 +4,7 @@ from graphql.core.type import ( ) from graphene import signals from graphene.utils import cached_property -# from graphene.relay.nodes import create_node_definitions + class Schema(object): _query = None @@ -14,27 +14,15 @@ class Schema(object): self.query = query self.name = name self._types = {} - + signals.init_schema.send(self) + def __repr__(self): - return '' % str(self.name) - - # @cachedproperty - # def node_definitions(self): - # return [object, object] - # # from graphene.relay import create_node_definitions - # # return create_node_definitions(schema=self) - - # @property - # def Node(self): - # return self.node_definitions[0] - - # @property - # def NodeField(self): - # return self.node_definitions[1] + return '' % (str(self.name), hash(self)) @property def query(self): return self._query + @query.setter def query(self, query): if not query: @@ -69,5 +57,3 @@ def object_type_created(object_type): schema = object_type._meta.schema if schema: schema.register_type(object_type) - -from graphene.env import get_global_schema diff --git a/graphene/core/types.py b/graphene/core/types.py index f020a72a..978fd41e 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -49,8 +49,8 @@ class ObjectTypeMeta(type): # Things without _meta aren't functional models, so they're # uninteresting parents. continue - if base._meta.schema != new_class._meta.schema: - raise Exception('The parent schema is not the same') + # if base._meta.schema != new_class._meta.schema: + # raise Exception('The parent schema is not the same') parent_fields = base._meta.local_fields # Check for clashes between locally declared fields and those @@ -135,3 +135,19 @@ class Interface(ObjectType): class Meta: interface = True proxy = True + +@signals.init_schema.connect +def add_types_to_schema(schema): + own_schema = schema + class _Interface(Interface): + class Meta: + schema = own_schema + proxy = True + + class _ObjectType(ObjectType): + class Meta: + schema = own_schema + proxy = True + + setattr(own_schema, 'Interface', _Interface) + setattr(own_schema, 'ObjectType', _ObjectType) diff --git a/graphene/relay/__init__.py b/graphene/relay/__init__.py index b7a71ce5..4e353804 100644 --- a/graphene/relay/__init__.py +++ b/graphene/relay/__init__.py @@ -2,4 +2,19 @@ from graphene.relay.nodes import ( create_node_definitions ) -from graphene.relay.relay import * +from graphene.relay.fields import ( + ConnectionField, +) + +import graphene.relay.connections + +from graphene.relay.relay import ( + Relay +) + +from graphene.env import get_global_schema + +schema = get_global_schema() +relay = schema.relay + +Node, NodeField = relay.Node, relay.NodeField diff --git a/graphene/relay/connections.py b/graphene/relay/connections.py new file mode 100644 index 00000000..24f2a61d --- /dev/null +++ b/graphene/relay/connections.py @@ -0,0 +1,35 @@ +import collections + +from graphql_relay.node.node import ( + globalIdField +) +from graphql_relay.connection.connection import ( + connectionDefinitions +) + +from graphene import signals + +from graphene.core.fields import NativeField +from graphene.relay.utils import get_relay +from graphene.relay.relay import Relay + + +@signals.class_prepared.connect +def object_type_created(object_type): + relay = get_relay(object_type._meta.schema) + if relay and issubclass(object_type, relay.Node): + type_name = object_type._meta.type_name + # 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) + + +@signals.init_schema.connect +def schema_created(schema): + setattr(schema, 'relay', Relay(schema)) diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py new file mode 100644 index 00000000..acdeb2f5 --- /dev/null +++ b/graphene/relay/fields.py @@ -0,0 +1,30 @@ +import collections + +from graphql_relay.connection.arrayconnection import ( + connectionFromArray +) +from graphql_relay.connection.connection import ( + connectionArgs +) +from graphene.core.fields import Field +from graphene.utils import cached_property +from graphene.relay.utils import get_relay + + +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 = self.get_object_type() + relay = get_relay(object_type._meta.schema) + assert issubclass(object_type, relay.Node), 'Only nodes have connections.' + return object_type.connection diff --git a/graphene/relay/nodes.py b/graphene/relay/nodes.py index 81223a52..643d148f 100644 --- a/graphene/relay/nodes.py +++ b/graphene/relay/nodes.py @@ -2,27 +2,32 @@ from graphql_relay.node.node import ( nodeDefinitions, fromGlobalId ) +from graphene.env import get_global_schema +from graphene.core.types import Interface +from graphene.core.fields import Field, NativeField -def create_node_definitions(getNode=None, getNodeType=None, schema=None): - from graphene.core.types import Interface - from graphene.core.fields import Field, NativeField - if not getNode: - def getNode(globalId, *args): - from graphene.env import get_global_schema - _schema = schema or get_global_schema() - resolvedGlobalId = fromGlobalId(globalId) - _type, _id = resolvedGlobalId.type, resolvedGlobalId.id - object_type = _schema.get_type(_type) - return object_type.get_node(_id) - if not getNodeType: - def getNodeType(obj): - return obj._meta.type +def getSchemaNode(schema=None): + def getNode(globalId, *args): + _schema = schema or get_global_schema() + resolvedGlobalId = fromGlobalId(globalId) + _type, _id = resolvedGlobalId.type, resolvedGlobalId.id + object_type = schema.get_type(_type) + return object_type.get_node(_id) + return getNode + +def getNodeType(obj): + return obj._meta.type + + +def create_node_definitions(getNode=None, getNodeType=getNodeType, schema=None): + getNode = getNode or getSchemaNode(schema) _nodeDefinitions = nodeDefinitions(getNode, getNodeType) + _Interface = getattr(schema,'Interface', Interface) - class Node(Interface): + class Node(_Interface): @classmethod def get_graphql_type(cls): if cls is Node: diff --git a/graphene/relay/relay.py b/graphene/relay/relay.py index 2df13e81..2ff4eb9b 100644 --- a/graphene/relay/relay.py +++ b/graphene/relay/relay.py @@ -1,51 +1,14 @@ -import collections - -from graphene import signals -from graphene.utils import cached_property - -from graphql_relay.node.node import ( - globalIdField +from graphene.relay.nodes import ( + create_node_definitions ) -from graphql_relay.connection.arrayconnection import ( - connectionFromArray + +from graphene.relay.fields import ( + ConnectionField, ) -from graphql_relay.connection.connection import ( - connectionArgs, - connectionDefinitions -) -from graphene.relay.nodes import create_node_definitions -from graphene.core.fields import Field, NativeField - -Node, NodeField = create_node_definitions() - -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 = self.get_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 - # 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) +class Relay(object): + def __init__(self, schema): + self.schema = schema + self.Node, self.NodeField = create_node_definitions(schema=self.schema) + self.ConnectionField = ConnectionField diff --git a/graphene/relay/utils.py b/graphene/relay/utils.py new file mode 100644 index 00000000..cd23632d --- /dev/null +++ b/graphene/relay/utils.py @@ -0,0 +1,3 @@ + +def get_relay(schema): + return getattr(schema, 'relay', None) diff --git a/graphene/signals.py b/graphene/signals.py index 954d02a1..ccb9ef0f 100644 --- a/graphene/signals.py +++ b/graphene/signals.py @@ -1,5 +1,6 @@ from blinker import Signal +init_schema = Signal() class_prepared = Signal() pre_init = Signal() post_init = Signal() diff --git a/tests/core/test_types.py b/tests/core/test_types.py index 1408caa2..0a83398a 100644 --- a/tests/core/test_types.py +++ b/tests/core/test_types.py @@ -20,6 +20,7 @@ class Character(Interface): name = StringField() class Meta: type_name = 'core.Character' + class Human(Character): '''Human description''' friends = StringField() diff --git a/tests/relay/test_relay.py b/tests/relay/test_relay.py index 2bcdc836..7314c096 100644 --- a/tests/relay/test_relay.py +++ b/tests/relay/test_relay.py @@ -4,6 +4,7 @@ import graphene from graphene import relay schema = graphene.Schema() +relay = schema.relay class OtherNode(relay.Node): name = graphene.StringField() diff --git a/tests/starwars_relay/schema.py b/tests/starwars_relay/schema.py index f1a12050..fbbfc49c 100644 --- a/tests/starwars_relay/schema.py +++ b/tests/starwars_relay/schema.py @@ -8,8 +8,6 @@ from .data import ( getEmpire, ) -schema = graphene.Schema() - class Ship(relay.Node): '''A ship in the Star Wars saga''' name = graphene.StringField(description='The name of the ship.') @@ -20,8 +18,6 @@ class Ship(relay.Node): if ship: return Ship(ship) - # class Meta: - # schema = schema class Faction(relay.Node): '''A faction in the Star Wars saga''' @@ -38,9 +34,6 @@ class Faction(relay.Node): if faction: return Faction(faction) - # class Meta: - # schema = schema - class Query(graphene.ObjectType): rebels = graphene.Field(Faction) @@ -56,10 +49,4 @@ class Query(graphene.ObjectType): return Faction(getEmpire()) - # class Meta: - # schema = schema - -print '*CACA', schema._types - -schema.query = Query -Schema = schema +schema = graphene.Schema(query=Query, name='Starwars Relay Schema') diff --git a/tests/starwars_relay/test_connections.py b/tests/starwars_relay/test_connections.py index 1bac0a4d..7d385514 100644 --- a/tests/starwars_relay/test_connections.py +++ b/tests/starwars_relay/test_connections.py @@ -1,7 +1,7 @@ from pytest import raises from graphql.core import graphql -from .schema import Schema +from .schema import schema def test_correct_fetch_first_ship_rebels(): query = ''' @@ -32,6 +32,6 @@ def test_correct_fetch_first_ship_rebels(): } } } - result = Schema.execute(query) + 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 index 85050b6f..1c4a0ba2 100644 --- a/tests/starwars_relay/test_objectidentification.py +++ b/tests/starwars_relay/test_objectidentification.py @@ -1,7 +1,7 @@ from pytest import raises from graphql.core import graphql -from .schema import Schema +from .schema import schema def test_correctly_fetches_id_name_rebels(): query = ''' @@ -18,7 +18,7 @@ def test_correctly_fetches_id_name_rebels(): 'name': 'Alliance to Restore the Republic' } } - result = Schema.execute(query) + result = schema.execute(query) assert result.errors == None assert result.data == expected @@ -39,7 +39,7 @@ def test_correctly_refetches_rebels(): 'name': 'Alliance to Restore the Republic' } } - result = Schema.execute(query) + result = schema.execute(query) assert result.errors == None assert result.data == expected @@ -58,7 +58,7 @@ def test_correctly_fetches_id_name_empire(): 'name': 'Galactic Empire' } } - result = Schema.execute(query) + result = schema.execute(query) assert result.errors == None assert result.data == expected @@ -79,7 +79,7 @@ def test_correctly_refetches_empire(): 'name': 'Galactic Empire' } } - result = Schema.execute(query) + result = schema.execute(query) assert result.errors == None assert result.data == expected @@ -100,6 +100,6 @@ def test_correctly_refetches_xwing(): 'name': 'X-Wing' } } - result = Schema.execute(query) + result = schema.execute(query) assert result.errors == None assert result.data == expected From c79097879d1bf43ddc021b03563fcb25d7e73ff5 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 27 Sep 2015 20:37:47 -0700 Subject: [PATCH 12/77] Improved ObjectType instances --- README.md | 10 ++-------- graphene/core/schema.py | 3 ++- graphene/core/types.py | 8 ++++++++ tests/starwars_relay/schema.py | 8 ++------ 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 0b3e0019..9e6b9e28 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,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): @@ -91,9 +89,7 @@ class Ship(relay.Node): @classmethod def get_node(cls, id): - ship = getShip(id) - if ship: - return Ship(ship) + return Ship(getShip(id)) class Faction(relay.Node): @@ -107,9 +103,7 @@ class Faction(relay.Node): @classmethod def get_node(cls, id): - faction = getFaction(id) - if faction: - return Faction(faction) + return Faction(getFaction(id) class Query(graphene.ObjectType): diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 132a4760..75f11eae 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -43,10 +43,11 @@ class Schema(object): return self._types[type_name] def execute(self, request='', root=None, vars=None, operation_name=None): + root = root or object() return graphql( self._schema, request=request, - root=root or self.query(), + root=self.query(root), vars=vars, operation_name=operation_name ) diff --git a/graphene/core/types.py b/graphene/core/types.py index 978fd41e..6c098a57 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -83,6 +83,13 @@ class ObjectTypeMeta(type): class ObjectType(six.with_metaclass(ObjectTypeMeta)): + def __new__(cls, instance=None, *args, **kwargs): + if cls._meta.interface: + raise Exception("An interface cannot be initialized") + if instance == None: + return None + return super(ObjectType, cls).__new__(cls, instance, *args, **kwargs) + def __init__(self, instance=None): signals.pre_init.send(self.__class__, instance=instance) self.instance = instance @@ -136,6 +143,7 @@ class Interface(ObjectType): interface = True proxy = True + @signals.init_schema.connect def add_types_to_schema(schema): own_schema = schema diff --git a/tests/starwars_relay/schema.py b/tests/starwars_relay/schema.py index fbbfc49c..8731f712 100644 --- a/tests/starwars_relay/schema.py +++ b/tests/starwars_relay/schema.py @@ -14,9 +14,7 @@ class Ship(relay.Node): @classmethod def get_node(cls, id): - ship = getShip(id) - if ship: - return Ship(ship) + return Ship(getShip(id)) class Faction(relay.Node): @@ -30,9 +28,7 @@ class Faction(relay.Node): @classmethod def get_node(cls, id): - faction = getFaction(id) - if faction: - return Faction(faction) + return Faction(getFaction(id)) class Query(graphene.ObjectType): From 2e8707aee66a83b4ac91ca2b33c26624cd1e0ea7 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 28 Sep 2015 00:34:25 -0700 Subject: [PATCH 13/77] First working version with Django. --- graphene/contrib/__init__.py | 0 graphene/contrib/django/__init__.py | 4 + graphene/contrib/django/converter.py | 40 ++++++++++ graphene/contrib/django/options.py | 22 +++++ graphene/contrib/django/types.py | 31 ++++++++ graphene/core/fields.py | 7 +- graphene/core/options.py | 9 ++- graphene/core/types.py | 17 +++- graphene/relay/connections.py | 8 +- graphene/relay/nodes.py | 2 +- tests/contrib_django/__init__.py | 0 tests/contrib_django/data.py | 17 ++++ tests/contrib_django/models.py | 43 ++++++++++ tests/contrib_django/test_schema.py | 115 +++++++++++++++++++++++++++ 14 files changed, 302 insertions(+), 13 deletions(-) create mode 100644 graphene/contrib/__init__.py create mode 100644 graphene/contrib/django/__init__.py create mode 100644 graphene/contrib/django/converter.py create mode 100644 graphene/contrib/django/options.py create mode 100644 graphene/contrib/django/types.py create mode 100644 tests/contrib_django/__init__.py create mode 100644 tests/contrib_django/data.py create mode 100644 tests/contrib_django/models.py create mode 100644 tests/contrib_django/test_schema.py diff --git a/graphene/contrib/__init__.py b/graphene/contrib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/contrib/django/__init__.py b/graphene/contrib/django/__init__.py new file mode 100644 index 00000000..fab7622f --- /dev/null +++ b/graphene/contrib/django/__init__.py @@ -0,0 +1,4 @@ +from graphene.contrib.django.types import ( + DjangoObjectType, + DjangoNode +) diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py new file mode 100644 index 00000000..590d9064 --- /dev/null +++ b/graphene/contrib/django/converter.py @@ -0,0 +1,40 @@ +from singledispatch import singledispatch +from django.db import models + +from graphene.core.fields import ( + StringField, + IDField, + IntField, + BooleanField, + FloatField, +) + +@singledispatch +def convert_django_field(field): + raise Exception("Don't know how to convert the Django field %s"%field) + + +@convert_django_field.register(models.CharField) +def _(field): + return StringField(description=field.help_text) + + +@convert_django_field.register(models.AutoField) +def _(field): + return IDField(description=field.help_text) + + +@convert_django_field.register(models.BigIntegerField) +@convert_django_field.register(models.IntegerField) +def _(field): + return IntField(description=field.help_text) + + +@convert_django_field.register(models.BooleanField) +def _(field): + return BooleanField(description=field.help_text) + + +@convert_django_field.register(models.FloatField) +def _(field): + return FloatField(description=field.help_text) diff --git a/graphene/contrib/django/options.py b/graphene/contrib/django/options.py new file mode 100644 index 00000000..381914f0 --- /dev/null +++ b/graphene/contrib/django/options.py @@ -0,0 +1,22 @@ +import inspect +from django.db import models + +from graphene.core.options import Options + +VALID_ATTRS = ('model', 'only_fields') + +class DjangoOptions(Options): + def __init__(self, *args, **kwargs): + self.model = None + super(DjangoOptions, self).__init__(*args, **kwargs) + self.valid_attrs += VALID_ATTRS + self.only_fields = None + + def contribute_to_class(self, cls, name): + super(DjangoOptions, self).contribute_to_class(cls, name) + if self.proxy: + return + if not self.model: + raise Exception('Django ObjectType %s must have a model in the Meta attr' % cls) + elif not inspect.isclass(self.model) or not issubclass(self.model, models.Model): + raise Exception('Provided model in %s is not a Django model' % cls) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py new file mode 100644 index 00000000..daa7c6b7 --- /dev/null +++ b/graphene/contrib/django/types.py @@ -0,0 +1,31 @@ +import six + +from graphene.core.types import ObjectTypeMeta, ObjectType +from graphene.contrib.django.options import DjangoOptions +from graphene.contrib.django.converter import convert_django_field + +from graphene.relay import Node + +class DjangoObjectTypeMeta(ObjectTypeMeta): + options_cls = DjangoOptions + def add_extra_fields(cls): + if not cls._meta.model: + return + + only_fields = cls._meta.only_fields + # print cls._meta.model._meta._get_fields(forward=False, reverse=True, include_hidden=True) + for field in cls._meta.model._meta.fields: + if only_fields and field.name not in only_fields: + continue + converted_field = convert_django_field(field) + cls.add_to_class(field.name, converted_field) + + +class DjangoObjectType(six.with_metaclass(DjangoObjectTypeMeta, ObjectType)): + class Meta: + proxy = True + + +class DjangoNode(six.with_metaclass(DjangoObjectTypeMeta, Node)): + class Meta: + proxy = True diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 383138d6..12258c6a 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -8,8 +8,10 @@ from graphql.core.type import ( GraphQLBoolean, GraphQLID, GraphQLArgument, + GraphQLFloat, ) from graphene.utils import cached_property +from graphene.core.types import ObjectType class Field(object): def __init__(self, field_type, resolve=None, null=True, args=None, description='', **extra_args): @@ -44,7 +46,6 @@ class Field(object): return resolve_fn(instance, args, info) def get_object_type(self): - from graphene.core.types import ObjectType field_type = self.field_type _is_class = inspect.isclass(field_type) if _is_class and issubclass(field_type, ObjectType): @@ -143,6 +144,10 @@ class IDField(TypeField): field_type = GraphQLID +class FloatField(TypeField): + field_type = GraphQLFloat + + class ListField(Field): def type_wrapper(self, field_type): return GraphQLList(field_type) diff --git a/graphene/core/options.py b/graphene/core/options.py index 5d0263f0..c0d48fc1 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -2,7 +2,7 @@ from graphene.env import get_global_schema from graphene.utils import cached_property DEFAULT_NAMES = ('description', 'name', 'interface', 'schema', - 'type_name', 'interfaces', 'proxy') + 'type_name', 'interfaces', 'proxy') class Options(object): @@ -14,6 +14,7 @@ class Options(object): self.schema = schema self.interfaces = [] self.parents = [] + self.valid_attrs = DEFAULT_NAMES def contribute_to_class(self, cls, name): cls._meta = self @@ -36,7 +37,7 @@ class Options(object): # over it, so we loop over the *original* dictionary instead. if name.startswith('_'): del meta_attrs[name] - for attr_name in DEFAULT_NAMES: + for attr_name in self.valid_attrs: if attr_name in meta_attrs: setattr(self, attr_name, meta_attrs.pop(attr_name)) self.original_attrs[attr_name] = getattr(self, attr_name) @@ -44,9 +45,13 @@ class Options(object): setattr(self, attr_name, getattr(self.meta, attr_name)) self.original_attrs[attr_name] = getattr(self, attr_name) + del self.valid_attrs + # Any leftover attributes must be invalid. if meta_attrs != {}: raise TypeError("'class Meta' got invalid attribute(s): %s" % ','.join(meta_attrs.keys())) + else: + self.proxy = False if self.interfaces != [] and self.interface: raise Exception("A interface cannot inherit from interfaces") diff --git a/graphene/core/types.py b/graphene/core/types.py index 6c098a57..fcafa0d6 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -11,9 +11,11 @@ from graphene.core.options import Options class ObjectTypeMeta(type): + options_cls = Options + def __new__(cls, name, bases, attrs): super_new = super(ObjectTypeMeta, cls).__new__ - parents = [b for b in bases if isinstance(b, ObjectTypeMeta)] + parents = [b for b in bases if isinstance(b, cls)] if not parents: # If this isn't a subclass of Model, don't do anything special. return super_new(cls, name, bases, attrs) @@ -25,20 +27,26 @@ class ObjectTypeMeta(type): '__doc__': doc }) attr_meta = attrs.pop('Meta', None) + proxy = None if not attr_meta: - meta = getattr(new_class, 'Meta', None) + meta = None + # meta = getattr(new_class, 'Meta', None) else: meta = attr_meta + base_meta = getattr(new_class, '_meta', None) schema = (base_meta and base_meta.schema) - new_class.add_to_class('_meta', Options(meta, schema)) + new_class.add_to_class('_meta', new_class.options_cls(meta, schema)) + if base_meta and base_meta.proxy: new_class._meta.interface = base_meta.interface + # Add all attributes to the class. for obj_name, obj in attrs.items(): new_class.add_to_class(obj_name, obj) + new_class.add_extra_fields() new_fields = new_class._meta.local_fields field_names = {f.field_name for f in new_fields} @@ -71,6 +79,9 @@ class ObjectTypeMeta(type): new_class._prepare() return new_class + def add_extra_fields(cls): + pass + def _prepare(cls): signals.class_prepared.send(cls) diff --git a/graphene/relay/connections.py b/graphene/relay/connections.py index 24f2a61d..83d73af5 100644 --- a/graphene/relay/connections.py +++ b/graphene/relay/connections.py @@ -1,5 +1,3 @@ -import collections - from graphql_relay.node.node import ( globalIdField ) @@ -8,7 +6,6 @@ from graphql_relay.connection.connection import ( ) from graphene import signals - from graphene.core.fields import NativeField from graphene.relay.utils import get_relay from graphene.relay.relay import Relay @@ -18,10 +15,9 @@ from graphene.relay.relay import Relay def object_type_created(object_type): relay = get_relay(object_type._meta.schema) if relay and issubclass(object_type, relay.Node): + if object_type._meta.proxy: + return type_name = object_type._meta.type_name - # 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 diff --git a/graphene/relay/nodes.py b/graphene/relay/nodes.py index 643d148f..11e1b6da 100644 --- a/graphene/relay/nodes.py +++ b/graphene/relay/nodes.py @@ -25,7 +25,7 @@ def create_node_definitions(getNode=None, getNodeType=getNodeType, schema=None): getNode = getNode or getSchemaNode(schema) _nodeDefinitions = nodeDefinitions(getNode, getNodeType) - _Interface = getattr(schema,'Interface', Interface) + _Interface = getattr(schema, 'Interface', Interface) class Node(_Interface): @classmethod diff --git a/tests/contrib_django/__init__.py b/tests/contrib_django/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/contrib_django/data.py b/tests/contrib_django/data.py new file mode 100644 index 00000000..107b146c --- /dev/null +++ b/tests/contrib_django/data.py @@ -0,0 +1,17 @@ +from datetime import date + +from .models import Reporter, Article + +r = Reporter(first_name='John', last_name='Smith', email='john@example.com') +r.save() + +r2 = Reporter(first_name='Paul', last_name='Jones', email='paul@example.com') +r2.save() + +a = Article(id=None, headline="This is a test", pub_date=date(2005, 7, 27), reporter=r) +a.save() + +new_article = r.articles.create(headline="John's second story", pub_date=date(2005, 7, 29)) + +new_article2 = Article(headline="Paul's story", pub_date=date(2006, 1, 17)) +r.articles.add(new_article2) diff --git a/tests/contrib_django/models.py b/tests/contrib_django/models.py new file mode 100644 index 00000000..ab37eaf8 --- /dev/null +++ b/tests/contrib_django/models.py @@ -0,0 +1,43 @@ +from __future__ import absolute_import +import django +from django.conf import settings + +settings.configure( + DATABASES={ + 'INSTALLED_APPS': [ + 'graphql.contrib.django', + ], + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'db_test.sqlite', + } + } +) + +from django.db import models + +class Reporter(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + email = models.EmailField() + + def __str__(self): # __unicode__ on Python 2 + return "%s %s" % (self.first_name, self.last_name) + + class Meta: + app_label = 'graphql' + +class Article(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateField() + reporter = models.ForeignKey(Reporter, related_name='articles') + + def __str__(self): # __unicode__ on Python 2 + return self.headline + + class Meta: + ordering = ('headline',) + app_label = 'graphql' + + +django.setup() diff --git a/tests/contrib_django/test_schema.py b/tests/contrib_django/test_schema.py new file mode 100644 index 00000000..cbfea539 --- /dev/null +++ b/tests/contrib_django/test_schema.py @@ -0,0 +1,115 @@ +from py.test import raises +from collections import namedtuple +from pytest import raises +import graphene +from graphene import relay +from graphene.contrib.django import ( + DjangoObjectType, + DjangoNode +) +from .models import Reporter, Article + + +def test_should_raise_if_no_model(): + with raises(Exception) as excinfo: + class Character1(DjangoObjectType): + pass + assert 'model in the Meta' in str(excinfo.value) + + +def test_should_raise_if_model_is_invalid(): + with raises(Exception) as excinfo: + class Character2(DjangoObjectType): + class Meta: + model = 1 + assert 'not a Django model' in str(excinfo.value) + + +def test_should_map_fields(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + + class Query(graphene.ObjectType): + reporter = graphene.Field(ReporterType) + + def resolve_reporter(self, *args, **kwargs): + return ReporterType(Reporter(first_name='ABA', last_name='X')) + + query = ''' + query ReporterQuery { + reporter { + first_name, + last_name, + email + } + } + ''' + expected = { + 'reporter': { + 'first_name': 'ABA', + 'last_name': 'X', + 'email': '' + } + } + Schema = graphene.Schema(query=Query) + result = Schema.execute(query) + assert not result.errors + assert result.data == expected + + +def test_should_map_only_few_fields(): + class Reporter2(DjangoObjectType): + class Meta: + model = Reporter + only_fields = ('id', 'email') + assert Reporter2._meta.fields_map.keys() == ['id', 'email'] + +def test_should_node(): + class ReporterNodeType(DjangoNode): + class Meta: + model = Reporter + + @classmethod + def get_node(cls, id): + return ReporterNodeType(Reporter(id=2, first_name='Cookie Monster')) + + class Query(graphene.ObjectType): + node = relay.NodeField() + reporter = graphene.Field(ReporterNodeType) + + def resolve_reporter(self, *args, **kwargs): + return ReporterNodeType(Reporter(id=1, first_name='ABA', last_name='X')) + + query = ''' + query ReporterQuery { + reporter { + id, + first_name, + last_name, + email + } + aCustomNode: node(id:"UmVwb3J0ZXJOb2RlVHlwZToy") { + id + ... on ReporterNodeType { + first_name + } + } + } + ''' + expected = { + 'reporter': { + 'id': 'UmVwb3J0ZXJOb2RlVHlwZTox', + 'first_name': 'ABA', + 'last_name': 'X', + 'email': '' + }, + 'aCustomNode': { + 'id': 'UmVwb3J0ZXJOb2RlVHlwZToy', + 'first_name': 'Cookie Monster' + } + } + Schema = graphene.Schema(query=Query) + result = Schema.execute(query) + assert not result.errors + assert result.data == expected From 76147d7c26f83a3c6c9ad2230c44e2ea9f33ee1b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 28 Sep 2015 01:51:51 -0700 Subject: [PATCH 14/77] Improved Django model conversion --- graphene/contrib/django/converter.py | 28 +++++++++++++++++++++------- graphene/contrib/django/fields.py | 23 +++++++++++++++++++++++ graphene/contrib/django/types.py | 13 +++++++++++-- graphene/core/fields.py | 4 ++-- graphene/core/options.py | 8 ++++++++ graphene/core/schema.py | 4 ++++ tests/contrib_django/test_schema.py | 8 ++++++++ 7 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 graphene/contrib/django/fields.py diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index 590d9064..6611d356 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -7,34 +7,48 @@ from graphene.core.fields import ( IntField, BooleanField, FloatField, + ListField ) +from graphene.contrib.django.fields import DjangoModelField @singledispatch -def convert_django_field(field): - raise Exception("Don't know how to convert the Django field %s"%field) +def convert_django_field(field, cls): + raise Exception("Don't know how to convert the Django field %s (%s)" % (field, field.__class__)) +@convert_django_field.register(models.DateField) @convert_django_field.register(models.CharField) -def _(field): +@convert_django_field.register(models.TextField) +def _(field, cls): return StringField(description=field.help_text) @convert_django_field.register(models.AutoField) -def _(field): +def _(field, cls): return IDField(description=field.help_text) @convert_django_field.register(models.BigIntegerField) @convert_django_field.register(models.IntegerField) -def _(field): +def _(field, cls): return IntField(description=field.help_text) @convert_django_field.register(models.BooleanField) -def _(field): +def _(field, cls): return BooleanField(description=field.help_text) @convert_django_field.register(models.FloatField) -def _(field): +def _(field, cls): return FloatField(description=field.help_text) + + +@convert_django_field.register(models.ManyToOneRel) +def _(field, cls): + return ListField(DjangoModelField(field.related_model)) + + +@convert_django_field.register(models.ForeignKey) +def _(field, cls): + return DjangoModelField(field.related_model) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py new file mode 100644 index 00000000..683f245a --- /dev/null +++ b/graphene/contrib/django/fields.py @@ -0,0 +1,23 @@ +from graphene.core.fields import Field +from graphene.utils import cached_property + +from graphene.env import get_global_schema + + +def get_type_for_model(schema, model): + schema = schema or get_global_schema() + types = schema.types.values() + for _type in types: + type_model = getattr(_type._meta, 'model', None) + if model == type_model: + return _type._meta.type + + +class DjangoModelField(Field): + def __init__(self, model): + super(DjangoModelField, self).__init__(None) + self.model = model + + @cached_property + def type(self): + return get_type_for_model(self.schema, self.model) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index daa7c6b7..dbde3aa9 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -1,4 +1,5 @@ import six +from django.db import models from graphene.core.types import ObjectTypeMeta, ObjectType from graphene.contrib.django.options import DjangoOptions @@ -6,6 +7,13 @@ from graphene.contrib.django.converter import convert_django_field from graphene.relay import Node +def get_reverse_fields(model): + for name, attr in model.__dict__.items(): + related = getattr(attr, 'related', None) + if isinstance(related, models.ManyToOneRel): + yield related + + class DjangoObjectTypeMeta(ObjectTypeMeta): options_cls = DjangoOptions def add_extra_fields(cls): @@ -14,10 +22,11 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): only_fields = cls._meta.only_fields # print cls._meta.model._meta._get_fields(forward=False, reverse=True, include_hidden=True) - for field in cls._meta.model._meta.fields: + reverse_fields = tuple(get_reverse_fields(cls._meta.model)) + for field in cls._meta.model._meta.fields + reverse_fields: if only_fields and field.name not in only_fields: continue - converted_field = convert_django_field(field) + converted_field = convert_django_field(field, cls) cls.add_to_class(field.name, converted_field) diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 12258c6a..254c2f02 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -76,8 +76,8 @@ class Field(object): @cached_property def field(self): - if not self.field_type: - raise Exception('Must specify a field GraphQL type for the field %s'%self.field_name) + # if not self.field_type: + # raise Exception('Must specify a field GraphQL type for the field %s'%self.field_name) if not self.object_type: raise Exception('Field could not be constructed in a non graphene.Type or graphene.Interface') diff --git a/graphene/core/options.py b/graphene/core/options.py index c0d48fc1..101c8a22 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -16,6 +16,14 @@ class Options(object): self.parents = [] self.valid_attrs = DEFAULT_NAMES + # @property + # def schema(self): + # return self._schema or get_global_schema() + + # @schema.setter + # def schema(self, schema): + # self._schema = schema + def contribute_to_class(self, cls, name): cls._meta = self self.parent = cls diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 75f11eae..63cf2318 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -42,6 +42,10 @@ class Schema(object): raise Exception('Type %s not found in %r' % (type_name, self)) return self._types[type_name] + @property + def types(self): + return self._types + def execute(self, request='', root=None, vars=None, operation_name=None): root = root or object() return graphql( diff --git a/tests/contrib_django/test_schema.py b/tests/contrib_django/test_schema.py index cbfea539..5dbf093d 100644 --- a/tests/contrib_django/test_schema.py +++ b/tests/contrib_django/test_schema.py @@ -74,6 +74,14 @@ def test_should_node(): def get_node(cls, id): return ReporterNodeType(Reporter(id=2, first_name='Cookie Monster')) + class ArticleNodeType(DjangoNode): + class Meta: + model = Article + + @classmethod + def get_node(cls, id): + return ArticleNodeType(None) + class Query(graphene.ObjectType): node = relay.NodeField() reporter = graphene.Field(ReporterNodeType) From 16d80b88f000559862d5f74a1e77677740377bc7 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 28 Sep 2015 01:54:45 -0700 Subject: [PATCH 15/77] Fixed dependency installation --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0435df42..6d43f6d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,8 @@ sudo: false python: - 2.7 install: -- pip install pytest pytest-cov coveralls flake8 six blinker +- pip install pytest pytest-cov coveralls flake8 six blinker singledispatch +- pip install -e .[django] - pip install git+https://github.com/dittos/graphqllib.git # Last version of graphqllib - pip install graphql-relay - python setup.py develop From d0285278acb80172f7cbde5008560d8bc098b5a0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 28 Sep 2015 01:56:15 -0700 Subject: [PATCH 16/77] Fixed Django install --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6d43f6d2..d9cf9eb9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,7 @@ sudo: false python: - 2.7 install: -- pip install pytest pytest-cov coveralls flake8 six blinker singledispatch -- pip install -e .[django] +- pip install pytest pytest-cov coveralls flake8 six blinker singledispatch django - pip install git+https://github.com/dittos/graphqllib.git # Last version of graphqllib - pip install graphql-relay - python setup.py develop From ac940b93090e75d884390b35eacdfd729d860584 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 28 Sep 2015 23:29:10 -0700 Subject: [PATCH 17/77] Improved Django integration with relations --- graphene/contrib/django/converter.py | 6 ++- graphene/contrib/django/fields.py | 13 +++++- graphene/contrib/django/options.py | 1 + graphene/contrib/django/types.py | 4 +- graphene/core/fields.py | 2 + graphene/core/options.py | 10 +---- graphene/relay/__init__.py | 2 + graphene/relay/connections.py | 4 +- graphene/relay/utils.py | 8 +++- tests/contrib_django/test_schema.py | 59 +++++++++++++++++++++++----- tests/starwars_relay/schema.py | 9 +++-- 11 files changed, 88 insertions(+), 30 deletions(-) diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index 6611d356..663cd11c 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -46,7 +46,11 @@ def _(field, cls): @convert_django_field.register(models.ManyToOneRel) def _(field, cls): - return ListField(DjangoModelField(field.related_model)) + schema = cls._meta.schema + model_field = DjangoModelField(field.related_model) + if issubclass(cls, schema.relay.Node): + return schema.relay.ConnectionField(model_field) + return ListField(model_field) @convert_django_field.register(models.ForeignKey) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 683f245a..992614c1 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -10,7 +10,7 @@ def get_type_for_model(schema, model): for _type in types: type_model = getattr(_type._meta, 'model', None) if model == type_model: - return _type._meta.type + return _type class DjangoModelField(Field): @@ -20,4 +20,13 @@ class DjangoModelField(Field): @cached_property def type(self): - return get_type_for_model(self.schema, self.model) + _type = self.get_object_type() + return _type and _type._meta.type + + def get_object_type(self): + _type = get_type_for_model(self.schema, self.model) + if not _type and self.object_type._meta.only_fields: + # We will only raise the exception if the related field is specified in only_fields + raise Exception("Field %s (%s) model not mapped in current schema" % (self, self.model._meta.object_name)) + + return _type diff --git a/graphene/contrib/django/options.py b/graphene/contrib/django/options.py index 381914f0..4560080a 100644 --- a/graphene/contrib/django/options.py +++ b/graphene/contrib/django/options.py @@ -5,6 +5,7 @@ from graphene.core.options import Options VALID_ATTRS = ('model', 'only_fields') + class DjangoOptions(Options): def __init__(self, *args, **kwargs): self.model = None diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index dbde3aa9..cd6831be 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -7,6 +7,7 @@ from graphene.contrib.django.converter import convert_django_field from graphene.relay import Node + def get_reverse_fields(model): for name, attr in model.__dict__.items(): related = getattr(attr, 'related', None) @@ -16,12 +17,11 @@ def get_reverse_fields(model): class DjangoObjectTypeMeta(ObjectTypeMeta): options_cls = DjangoOptions + def add_extra_fields(cls): if not cls._meta.model: return - only_fields = cls._meta.only_fields - # print cls._meta.model._meta._get_fields(forward=False, reverse=True, include_hidden=True) reverse_fields = tuple(get_reverse_fields(cls._meta.model)) for field in cls._meta.model._meta.fields + reverse_fields: if only_fields and field.name not in only_fields: diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 254c2f02..bd27aaaf 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -48,6 +48,8 @@ class Field(object): def get_object_type(self): field_type = self.field_type _is_class = inspect.isclass(field_type) + if isinstance(field_type, Field): + return field_type.get_object_type() if _is_class and issubclass(field_type, ObjectType): return field_type elif isinstance(field_type, basestring): diff --git a/graphene/core/options.py b/graphene/core/options.py index 101c8a22..6799e517 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -11,18 +11,10 @@ class Options(object): self.local_fields = [] self.interface = False self.proxy = False - self.schema = schema + self.schema = schema or get_global_schema() self.interfaces = [] self.parents = [] self.valid_attrs = DEFAULT_NAMES - - # @property - # def schema(self): - # return self._schema or get_global_schema() - - # @schema.setter - # def schema(self, schema): - # self._schema = schema def contribute_to_class(self, cls, name): cls._meta = self diff --git a/graphene/relay/__init__.py b/graphene/relay/__init__.py index 4e353804..76020a69 100644 --- a/graphene/relay/__init__.py +++ b/graphene/relay/__init__.py @@ -13,8 +13,10 @@ from graphene.relay.relay import ( ) from graphene.env import get_global_schema +from graphene.relay.utils import setup schema = get_global_schema() +setup(schema) relay = schema.relay Node, NodeField = relay.Node, relay.NodeField diff --git a/graphene/relay/connections.py b/graphene/relay/connections.py index 83d73af5..db241bb8 100644 --- a/graphene/relay/connections.py +++ b/graphene/relay/connections.py @@ -7,7 +7,7 @@ from graphql_relay.connection.connection import ( from graphene import signals from graphene.core.fields import NativeField -from graphene.relay.utils import get_relay +from graphene.relay.utils import get_relay, setup from graphene.relay.relay import Relay @@ -28,4 +28,4 @@ def object_type_created(object_type): @signals.init_schema.connect def schema_created(schema): - setattr(schema, 'relay', Relay(schema)) + setup(schema) diff --git a/graphene/relay/utils.py b/graphene/relay/utils.py index cd23632d..2974fbab 100644 --- a/graphene/relay/utils.py +++ b/graphene/relay/utils.py @@ -1,3 +1,9 @@ - def get_relay(schema): return getattr(schema, 'relay', None) + + +def setup(schema): + from graphene.relay.relay import Relay + if not hasattr(schema, 'relay'): + return setattr(schema, 'relay', Relay(schema)) + return schema diff --git a/tests/contrib_django/test_schema.py b/tests/contrib_django/test_schema.py index 5dbf093d..34617ddf 100644 --- a/tests/contrib_django/test_schema.py +++ b/tests/contrib_django/test_schema.py @@ -25,12 +25,31 @@ def test_should_raise_if_model_is_invalid(): assert 'not a Django model' in str(excinfo.value) +def test_should_raise_if_model_is_invalid(): + with raises(Exception) as excinfo: + class ReporterTypeError(DjangoObjectType): + class Meta: + model = Reporter + only_fields = ('articles', ) + + schema = graphene.Schema(query=ReporterTypeError) + query = ''' + query ReporterQuery { + articles + } + ''' + result = schema.execute(query) + assert not result.errors + + assert 'articles (Article) model not mapped in current schema' in str(excinfo.value) + + def test_should_map_fields(): class ReporterType(DjangoObjectType): class Meta: model = Reporter - class Query(graphene.ObjectType): + class Query2(graphene.ObjectType): reporter = graphene.Field(ReporterType) def resolve_reporter(self, *args, **kwargs): @@ -52,7 +71,7 @@ def test_should_map_fields(): 'email': '' } } - Schema = graphene.Schema(query=Query) + Schema = graphene.Schema(query=Query2) result = Schema.execute(query) assert not result.errors assert result.data == expected @@ -74,15 +93,18 @@ def test_should_node(): def get_node(cls, id): return ReporterNodeType(Reporter(id=2, first_name='Cookie Monster')) + def resolve_articles(self, *args, **kwargs): + return [ArticleNodeType(Article(headline='Hi!'))] + class ArticleNodeType(DjangoNode): class Meta: model = Article @classmethod def get_node(cls, id): - return ArticleNodeType(None) + return ArticleNodeType(Article(id=1, headline='Article node')) - class Query(graphene.ObjectType): + class Query1(graphene.ObjectType): node = relay.NodeField() reporter = graphene.Field(ReporterNodeType) @@ -94,14 +116,24 @@ def test_should_node(): reporter { id, first_name, + articles { + edges { + node { + headline + } + } + } last_name, email } - aCustomNode: node(id:"UmVwb3J0ZXJOb2RlVHlwZToy") { + my_article: node(id:"QXJ0aWNsZU5vZGVUeXBlOjE=") { id ... on ReporterNodeType { first_name } + ... on ArticleNodeType { + headline + } } } ''' @@ -110,14 +142,21 @@ def test_should_node(): 'id': 'UmVwb3J0ZXJOb2RlVHlwZTox', 'first_name': 'ABA', 'last_name': 'X', - 'email': '' + 'email': '', + 'articles': { + 'edges': [{ + 'node': { + 'headline': 'Hi!' + } + }] + }, }, - 'aCustomNode': { - 'id': 'UmVwb3J0ZXJOb2RlVHlwZToy', - 'first_name': 'Cookie Monster' + 'my_article': { + 'id': 'QXJ0aWNsZU5vZGVUeXBlOjE=', + 'headline': 'Article node' } } - Schema = graphene.Schema(query=Query) + Schema = graphene.Schema(query=Query1) result = Schema.execute(query) assert not result.errors assert result.data == expected diff --git a/tests/starwars_relay/schema.py b/tests/starwars_relay/schema.py index 8731f712..4bcdb87d 100644 --- a/tests/starwars_relay/schema.py +++ b/tests/starwars_relay/schema.py @@ -1,5 +1,5 @@ import graphene -from graphene import resolve_only_args, relay +from graphene import resolve_only_args from .data import ( getFaction, @@ -8,6 +8,9 @@ from .data import ( getEmpire, ) +schema = graphene.Schema(name='Starwars Relay Schema') +relay = schema.relay + class Ship(relay.Node): '''A ship in the Star Wars saga''' name = graphene.StringField(description='The name of the ship.') @@ -31,7 +34,7 @@ class Faction(relay.Node): return Faction(getFaction(id)) -class Query(graphene.ObjectType): +class Query(schema.ObjectType): rebels = graphene.Field(Faction) empire = graphene.Field(Faction) node = relay.NodeField() @@ -45,4 +48,4 @@ class Query(graphene.ObjectType): return Faction(getEmpire()) -schema = graphene.Schema(query=Query, name='Starwars Relay Schema') +schema.query = Query From 18e3ef8698104f8466a756fa407c247f40ae9fd1 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 28 Sep 2015 23:50:42 -0700 Subject: [PATCH 18/77] Created LazyField. Abstracted the Django connection into it. --- graphene/contrib/django/converter.py | 6 ++---- graphene/contrib/django/fields.py | 20 ++++++++++++++++++-- graphene/core/fields.py | 13 +++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index 663cd11c..b253de15 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -9,7 +9,7 @@ from graphene.core.fields import ( FloatField, ListField ) -from graphene.contrib.django.fields import DjangoModelField +from graphene.contrib.django.fields import ConnectionOrListField, DjangoModelField @singledispatch def convert_django_field(field, cls): @@ -48,9 +48,7 @@ def _(field, cls): def _(field, cls): schema = cls._meta.schema model_field = DjangoModelField(field.related_model) - if issubclass(cls, schema.relay.Node): - return schema.relay.ConnectionField(model_field) - return ListField(model_field) + return ConnectionOrListField(model_field) @convert_django_field.register(models.ForeignKey) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 992614c1..80925e4c 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -1,6 +1,9 @@ -from graphene.core.fields import Field -from graphene.utils import cached_property +from graphene.core.fields import ( + ListField +) +from graphene.core.fields import Field, LazyField +from graphene.utils import cached_property from graphene.env import get_global_schema @@ -13,6 +16,19 @@ def get_type_for_model(schema, model): return _type +class ConnectionOrListField(LazyField): + def get_field(self): + schema = self.schema + model_field = self.field_type + field_object_type = model_field.get_object_type() + if field_object_type and issubclass(field_object_type, schema.relay.Node): + field = schema.relay.ConnectionField(model_field) + else: + field = ListField(model_field) + field.contribute_to_class(self.object_type, self.field_name) + return field + + class DjangoModelField(Field): def __init__(self, model): super(DjangoModelField, self).__init__(None) diff --git a/graphene/core/fields.py b/graphene/core/fields.py index bd27aaaf..1c008422 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -125,6 +125,19 @@ class NativeField(Field): self.field = field or getattr(self, 'field') +class LazyField(Field): + @cached_property + def inner_field(self): + return self.get_field() + + @cached_property + def type(self): + return self.inner_field.type + + @cached_property + def field(self): + return self.inner_field.field + class TypeField(Field): def __init__(self, *args, **kwargs): super(TypeField, self).__init__(self.field_type, *args, **kwargs) From b3ecbb300f45bdab4621d6fdc68b92813e43b414 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 29 Sep 2015 00:31:59 -0700 Subject: [PATCH 19/77] Fixed errors with latest build of graphqllib --- tests/starwars_relay/test_connections.py | 2 +- tests/starwars_relay/test_objectidentification.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/starwars_relay/test_connections.py b/tests/starwars_relay/test_connections.py index 7d385514..5f115e49 100644 --- a/tests/starwars_relay/test_connections.py +++ b/tests/starwars_relay/test_connections.py @@ -33,5 +33,5 @@ def test_correct_fetch_first_ship_rebels(): } } result = schema.execute(query) - assert result.errors == None + assert not result.errors assert result.data == expected diff --git a/tests/starwars_relay/test_objectidentification.py b/tests/starwars_relay/test_objectidentification.py index 1c4a0ba2..ced7daa7 100644 --- a/tests/starwars_relay/test_objectidentification.py +++ b/tests/starwars_relay/test_objectidentification.py @@ -19,7 +19,7 @@ def test_correctly_fetches_id_name_rebels(): } } result = schema.execute(query) - assert result.errors == None + assert not result.errors assert result.data == expected def test_correctly_refetches_rebels(): @@ -40,7 +40,7 @@ def test_correctly_refetches_rebels(): } } result = schema.execute(query) - assert result.errors == None + assert not result.errors assert result.data == expected def test_correctly_fetches_id_name_empire(): @@ -59,7 +59,7 @@ def test_correctly_fetches_id_name_empire(): } } result = schema.execute(query) - assert result.errors == None + assert not result.errors assert result.data == expected def test_correctly_refetches_empire(): @@ -80,7 +80,7 @@ def test_correctly_refetches_empire(): } } result = schema.execute(query) - assert result.errors == None + assert not result.errors assert result.data == expected def test_correctly_refetches_xwing(): @@ -101,5 +101,5 @@ def test_correctly_refetches_xwing(): } } result = schema.execute(query) - assert result.errors == None + assert not result.errors assert result.data == expected From 2faa8223e8df7f5ad992fc0554a6869c0c32bef8 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 29 Sep 2015 01:18:32 -0700 Subject: [PATCH 20/77] Used LazyNativeField for NodeField --- graphene/core/fields.py | 10 ++++++++++ graphene/relay/nodes.py | 7 ++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 1c008422..97a60fbe 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -138,6 +138,16 @@ class LazyField(Field): def field(self): return self.inner_field.field + +class LazyNativeField(LazyField): + def __init__(self, *args, **kwargs): + super(LazyNativeField, self).__init__(None, *args, **kwargs) + + @cached_property + def field(self): + return self.inner_field + + class TypeField(Field): def __init__(self, *args, **kwargs): super(TypeField, self).__init__(self.field_type, *args, **kwargs) diff --git a/graphene/relay/nodes.py b/graphene/relay/nodes.py index 11e1b6da..e2cb9c5f 100644 --- a/graphene/relay/nodes.py +++ b/graphene/relay/nodes.py @@ -4,7 +4,7 @@ from graphql_relay.node.node import ( ) from graphene.env import get_global_schema from graphene.core.types import Interface -from graphene.core.fields import Field, NativeField +from graphene.core.fields import Field, LazyNativeField def getSchemaNode(schema=None): @@ -36,7 +36,8 @@ def create_node_definitions(getNode=None, getNodeType=getNodeType, schema=None): return super(Node, cls).get_graphql_type() - class NodeField(NativeField): - field = _nodeDefinitions.nodeField + class NodeField(LazyNativeField): + def get_field(self): + return _nodeDefinitions.nodeField return Node, NodeField From 80094f45c26fe443e2d060edfb6263654845b348 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 29 Sep 2015 02:29:38 -0700 Subject: [PATCH 21/77] Refactored basic schema code. Make it faster and cleaner --- graphene/contrib/django/fields.py | 5 ++-- graphene/core/schema.py | 3 ++ graphene/core/types.py | 17 ----------- graphene/relay/__init__.py | 18 ++--------- graphene/relay/connections.py | 11 ++----- graphene/relay/fields.py | 11 ++++--- graphene/relay/nodes.py | 50 ++++++++++++++----------------- graphene/relay/relay.py | 14 --------- graphene/relay/utils.py | 9 ------ tests/relay/test_relay.py | 1 - tests/starwars_relay/schema.py | 5 ++-- 11 files changed, 42 insertions(+), 102 deletions(-) delete mode 100644 graphene/relay/relay.py delete mode 100644 graphene/relay/utils.py diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 80925e4c..2bd173a7 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -1,6 +1,7 @@ from graphene.core.fields import ( ListField ) +from graphene import relay from graphene.core.fields import Field, LazyField from graphene.utils import cached_property @@ -21,8 +22,8 @@ class ConnectionOrListField(LazyField): schema = self.schema model_field = self.field_type field_object_type = model_field.get_object_type() - if field_object_type and issubclass(field_object_type, schema.relay.Node): - field = schema.relay.ConnectionField(model_field) + if field_object_type and issubclass(field_object_type, schema.Node): + field = relay.ConnectionField(model_field) else: field = ListField(model_field) field.contribute_to_class(self.object_type, self.field_name) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 63cf2318..225fffda 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -42,6 +42,9 @@ class Schema(object): raise Exception('Type %s not found in %r' % (type_name, self)) return self._types[type_name] + def __getattr__(self, name): + return self.get_type(name) + @property def types(self): return self._types diff --git a/graphene/core/types.py b/graphene/core/types.py index fcafa0d6..432bf2a9 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -153,20 +153,3 @@ class Interface(ObjectType): class Meta: interface = True proxy = True - - -@signals.init_schema.connect -def add_types_to_schema(schema): - own_schema = schema - class _Interface(Interface): - class Meta: - schema = own_schema - proxy = True - - class _ObjectType(ObjectType): - class Meta: - schema = own_schema - proxy = True - - setattr(own_schema, 'Interface', _Interface) - setattr(own_schema, 'ObjectType', _ObjectType) diff --git a/graphene/relay/__init__.py b/graphene/relay/__init__.py index 76020a69..ec44068e 100644 --- a/graphene/relay/__init__.py +++ b/graphene/relay/__init__.py @@ -1,22 +1,10 @@ -from graphene.relay.nodes import ( - create_node_definitions -) - from graphene.relay.fields import ( ConnectionField, + NodeField ) import graphene.relay.connections -from graphene.relay.relay import ( - Relay +from graphene.relay.nodes import ( + Node ) - -from graphene.env import get_global_schema -from graphene.relay.utils import setup - -schema = get_global_schema() -setup(schema) -relay = schema.relay - -Node, NodeField = relay.Node, relay.NodeField diff --git a/graphene/relay/connections.py b/graphene/relay/connections.py index db241bb8..ba2053ec 100644 --- a/graphene/relay/connections.py +++ b/graphene/relay/connections.py @@ -7,14 +7,12 @@ from graphql_relay.connection.connection import ( from graphene import signals from graphene.core.fields import NativeField -from graphene.relay.utils import get_relay, setup -from graphene.relay.relay import Relay @signals.class_prepared.connect def object_type_created(object_type): - relay = get_relay(object_type._meta.schema) - if relay and issubclass(object_type, relay.Node): + schema = object_type._meta.schema + if issubclass(object_type, schema.Node) and object_type != schema.Node: if object_type._meta.proxy: return type_name = object_type._meta.type_name @@ -24,8 +22,3 @@ def object_type_created(object_type): connection = connectionDefinitions(type_name, object_type._meta.type).connectionType object_type.add_to_class('connection', connection) - - -@signals.init_schema.connect -def schema_created(schema): - setup(schema) diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index acdeb2f5..2a35763a 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -6,9 +6,8 @@ from graphql_relay.connection.arrayconnection import ( from graphql_relay.connection.connection import ( connectionArgs ) -from graphene.core.fields import Field +from graphene.core.fields import Field, LazyNativeField from graphene.utils import cached_property -from graphene.relay.utils import get_relay class ConnectionField(Field): @@ -25,6 +24,10 @@ class ConnectionField(Field): @cached_property def type(self): object_type = self.get_object_type() - relay = get_relay(object_type._meta.schema) - assert issubclass(object_type, relay.Node), 'Only nodes have connections.' + assert issubclass(object_type, self.schema.Node), 'Only nodes have connections.' return object_type.connection + + +class NodeField(LazyNativeField): + def get_field(self): + return self.schema.Node._definitions.nodeField diff --git a/graphene/relay/nodes.py b/graphene/relay/nodes.py index e2cb9c5f..1b0c51bc 100644 --- a/graphene/relay/nodes.py +++ b/graphene/relay/nodes.py @@ -4,40 +4,34 @@ from graphql_relay.node.node import ( ) from graphene.env import get_global_schema from graphene.core.types import Interface -from graphene.core.fields import Field, LazyNativeField +from graphene.core.fields import LazyNativeField -def getSchemaNode(schema=None): - def getNode(globalId, *args): - _schema = schema or get_global_schema() - resolvedGlobalId = fromGlobalId(globalId) - _type, _id = resolvedGlobalId.type, resolvedGlobalId.id - object_type = schema.get_type(_type) - return object_type.get_node(_id) - return getNode - - -def getNodeType(obj): +def get_node_type(obj): return obj._meta.type -def create_node_definitions(getNode=None, getNodeType=getNodeType, schema=None): - getNode = getNode or getSchemaNode(schema) - _nodeDefinitions = nodeDefinitions(getNode, getNodeType) +def get_node(schema, globalId, *args): + resolvedGlobalId = fromGlobalId(globalId) + _type, _id = resolvedGlobalId.type, resolvedGlobalId.id + object_type = schema.get_type(_type) + return object_type.get_node(_id) - _Interface = getattr(schema, 'Interface', Interface) +class Node(Interface): + _definitions = None - 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() + @classmethod + def contribute_to_schema(cls, schema): + if cls._definitions: + return + schema = cls._meta.schema + cls._definitions = nodeDefinitions(lambda *args: get_node(schema, *args), get_node_type) + @classmethod + def get_graphql_type(cls): + if cls is cls._meta.schema.Node: + # Return only nodeInterface when is the Node Inerface + cls.contribute_to_schema(cls._meta.schema) + return cls._definitions.nodeInterface + return super(Node, cls).get_graphql_type() - class NodeField(LazyNativeField): - def get_field(self): - return _nodeDefinitions.nodeField - - return Node, NodeField diff --git a/graphene/relay/relay.py b/graphene/relay/relay.py deleted file mode 100644 index 2ff4eb9b..00000000 --- a/graphene/relay/relay.py +++ /dev/null @@ -1,14 +0,0 @@ -from graphene.relay.nodes import ( - create_node_definitions -) - -from graphene.relay.fields import ( - ConnectionField, -) - - -class Relay(object): - def __init__(self, schema): - self.schema = schema - self.Node, self.NodeField = create_node_definitions(schema=self.schema) - self.ConnectionField = ConnectionField diff --git a/graphene/relay/utils.py b/graphene/relay/utils.py deleted file mode 100644 index 2974fbab..00000000 --- a/graphene/relay/utils.py +++ /dev/null @@ -1,9 +0,0 @@ -def get_relay(schema): - return getattr(schema, 'relay', None) - - -def setup(schema): - from graphene.relay.relay import Relay - if not hasattr(schema, 'relay'): - return setattr(schema, 'relay', Relay(schema)) - return schema diff --git a/tests/relay/test_relay.py b/tests/relay/test_relay.py index 7314c096..2bcdc836 100644 --- a/tests/relay/test_relay.py +++ b/tests/relay/test_relay.py @@ -4,7 +4,6 @@ import graphene from graphene import relay schema = graphene.Schema() -relay = schema.relay class OtherNode(relay.Node): name = graphene.StringField() diff --git a/tests/starwars_relay/schema.py b/tests/starwars_relay/schema.py index 4bcdb87d..8c9bc494 100644 --- a/tests/starwars_relay/schema.py +++ b/tests/starwars_relay/schema.py @@ -1,5 +1,5 @@ import graphene -from graphene import resolve_only_args +from graphene import resolve_only_args, relay from .data import ( getFaction, @@ -9,7 +9,6 @@ from .data import ( ) schema = graphene.Schema(name='Starwars Relay Schema') -relay = schema.relay class Ship(relay.Node): '''A ship in the Star Wars saga''' @@ -34,7 +33,7 @@ class Faction(relay.Node): return Faction(getFaction(id)) -class Query(schema.ObjectType): +class Query(graphene.ObjectType): rebels = graphene.Field(Faction) empire = graphene.Field(Faction) node = relay.NodeField() From 8274fcc5d919afad5342990d0636e5b5d1758f54 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 29 Sep 2015 02:38:50 -0700 Subject: [PATCH 22/77] Added introspect method to the schema --- graphene/core/schema.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 75f11eae..4a7ae74f 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -52,6 +52,9 @@ class Schema(object): operation_name=operation_name ) + def introspect(self): + return self._schema.get_type_map() + @signals.class_prepared.connect def object_type_created(object_type): From e89eb3456e0614c5c45306e3cf0da3aed4212c60 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 29 Sep 2015 18:25:56 -0700 Subject: [PATCH 23/77] Improved Django mapping --- graphene/contrib/django/converter.py | 26 ++++++++++++++++++++------ graphene/contrib/django/fields.py | 4 ++-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index b253de15..5544f205 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -19,31 +19,44 @@ def convert_django_field(field, cls): @convert_django_field.register(models.DateField) @convert_django_field.register(models.CharField) @convert_django_field.register(models.TextField) +@convert_django_field.register(models.EmailField) +@convert_django_field.register(models.SlugField) def _(field, cls): - return StringField(description=field.help_text) + return StringField(description=field.description) @convert_django_field.register(models.AutoField) def _(field, cls): - return IDField(description=field.help_text) + return IDField(description=field.description) +@convert_django_field.register(models.PositiveIntegerField) +@convert_django_field.register(models.PositiveSmallIntegerField) +@convert_django_field.register(models.SmallIntegerField) @convert_django_field.register(models.BigIntegerField) +@convert_django_field.register(models.URLField) +@convert_django_field.register(models.UUIDField) @convert_django_field.register(models.IntegerField) def _(field, cls): - return IntField(description=field.help_text) + return IntField(description=field.description) @convert_django_field.register(models.BooleanField) def _(field, cls): - return BooleanField(description=field.help_text) + return BooleanField(description=field.description, null=False) + + +@convert_django_field.register(models.NullBooleanField) +def _(field, cls): + return BooleanField(description=field.description) @convert_django_field.register(models.FloatField) def _(field, cls): - return FloatField(description=field.help_text) + return FloatField(description=field.description) +@convert_django_field.register(models.ManyToManyField) @convert_django_field.register(models.ManyToOneRel) def _(field, cls): schema = cls._meta.schema @@ -51,6 +64,7 @@ def _(field, cls): return ConnectionOrListField(model_field) +@convert_django_field.register(models.OneToOneField) @convert_django_field.register(models.ForeignKey) def _(field, cls): - return DjangoModelField(field.related_model) + return DjangoModelField(field.related_model, description=field.description) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 2bd173a7..d87734bb 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -31,8 +31,8 @@ class ConnectionOrListField(LazyField): class DjangoModelField(Field): - def __init__(self, model): - super(DjangoModelField, self).__init__(None) + def __init__(self, model, *args, **kwargs): + super(DjangoModelField, self).__init__(None, *args, **kwargs) self.model = model @cached_property From a7774f0be4fcdceb7873f6206d50bf8d61f59e63 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 29 Sep 2015 23:34:59 -0700 Subject: [PATCH 24/77] Fixed issues with relay and django models --- graphene/contrib/django/fields.py | 13 +++++++++++-- graphene/relay/connections.py | 2 +- graphene/relay/fields.py | 4 ++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index d87734bb..a6d54f62 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -7,23 +7,32 @@ from graphene.core.fields import Field, LazyField from graphene.utils import cached_property from graphene.env import get_global_schema +from django.db.models.query import QuerySet def get_type_for_model(schema, model): schema = schema or get_global_schema() types = schema.types.values() for _type in types: - type_model = getattr(_type._meta, 'model', None) + type_model = hasattr(_type,'_meta') and getattr(_type._meta, 'model', None) if model == type_model: return _type +class DjangoConnectionField(relay.ConnectionField): + def wrap_resolved(self, value, instance, args, info): + if isinstance(value, QuerySet): + cls = instance.__class__ + value = [cls(s) for s in value] + return value + + class ConnectionOrListField(LazyField): def get_field(self): schema = self.schema model_field = self.field_type field_object_type = model_field.get_object_type() if field_object_type and issubclass(field_object_type, schema.Node): - field = relay.ConnectionField(model_field) + field = DjangoConnectionField(model_field) else: field = ListField(model_field) field.contribute_to_class(self.object_type, self.field_name) diff --git a/graphene/relay/connections.py b/graphene/relay/connections.py index ba2053ec..5cda4705 100644 --- a/graphene/relay/connections.py +++ b/graphene/relay/connections.py @@ -12,7 +12,7 @@ from graphene.core.fields import NativeField @signals.class_prepared.connect def object_type_created(object_type): schema = object_type._meta.schema - if issubclass(object_type, schema.Node) and object_type != schema.Node: + if hasattr(schema, 'Node') and issubclass(object_type, schema.Node) and object_type != schema.Node: if object_type._meta.proxy: return type_name = object_type._meta.type_name diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index 2a35763a..a888919d 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -15,10 +15,14 @@ class ConnectionField(Field): super(ConnectionField, self).__init__(field_type, resolve=resolve, args=connectionArgs, description=description) + def wrap_resolved(self, value, instance, args, info): + return value + 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' + resolved = self.wrap_resolved(resolved, instance, args, info) return connectionFromArray(resolved, args) @cached_property From 35ec78750170aa9bfaa977686a041ce64ea7fcfd Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 29 Sep 2015 23:40:40 -0700 Subject: [PATCH 25/77] Improved testing --- .travis.yml | 3 +- setup.py | 1 + tests/__init__.py | 0 tests/contrib_django/test_schema.py | 7 ++ tests/django_settings.py | 13 +++ tests/starwars_django/__init__.py | 0 tests/starwars_django/data.py | 98 ++++++++++++++++ tests/starwars_django/models.py | 17 +++ tests/starwars_django/schema.py | 48 ++++++++ tests/starwars_django/test_connections.py | 43 +++++++ .../test_objectidentification.py | 105 ++++++++++++++++++ tox.ini | 4 + 12 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 tests/__init__.py create mode 100644 tests/django_settings.py create mode 100644 tests/starwars_django/__init__.py create mode 100644 tests/starwars_django/data.py create mode 100644 tests/starwars_django/models.py create mode 100644 tests/starwars_django/schema.py create mode 100644 tests/starwars_django/test_connections.py create mode 100644 tests/starwars_django/test_objectidentification.py diff --git a/.travis.yml b/.travis.yml index d9cf9eb9..83d35e1a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,8 @@ sudo: false python: - 2.7 install: -- pip install pytest pytest-cov coveralls flake8 six blinker singledispatch django +- pip install pytest pytest-cov coveralls flake8 six blinker +- pip install -e .[django] - pip install git+https://github.com/dittos/graphqllib.git # Last version of graphqllib - pip install graphql-relay - python setup.py develop diff --git a/setup.py b/setup.py index 739f729f..04d1d37e 100644 --- a/setup.py +++ b/setup.py @@ -56,6 +56,7 @@ setup( extras_require={ 'django': [ 'Django>=1.8.0,<1.9', + 'pytest-django', 'singledispatch>=3.4.0.3', ], }, diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/contrib_django/test_schema.py b/tests/contrib_django/test_schema.py index 34617ddf..669c8a7f 100644 --- a/tests/contrib_django/test_schema.py +++ b/tests/contrib_django/test_schema.py @@ -44,6 +44,13 @@ def test_should_raise_if_model_is_invalid(): assert 'articles (Article) model not mapped in current schema' in str(excinfo.value) + +def test_should_map_fields_correctly(): + class ReporterType2(DjangoObjectType): + class Meta: + model = Reporter + assert ReporterType2._meta.fields_map.keys() == ['articles', 'first_name', 'last_name', 'id', 'email'] + def test_should_map_fields(): class ReporterType(DjangoObjectType): class Meta: diff --git a/tests/django_settings.py b/tests/django_settings.py new file mode 100644 index 00000000..4f1bf8ce --- /dev/null +++ b/tests/django_settings.py @@ -0,0 +1,13 @@ +SECRET_KEY = 1 + +INSTALLED_APPS = [ + 'graphql.contrib.django', + 'tests.starwars_django', +] + +DATABASES={ + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'tests/django.sqlite', + } +} diff --git a/tests/starwars_django/__init__.py b/tests/starwars_django/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/starwars_django/data.py b/tests/starwars_django/data.py new file mode 100644 index 00000000..552c2ebe --- /dev/null +++ b/tests/starwars_django/data.py @@ -0,0 +1,98 @@ +from collections import namedtuple + +from .models import Ship, Faction + +def initialize(): + rebels = Faction( + id='1', + name='Alliance to Restore the Republic', + ) + rebels.save() + + empire = Faction( + id='2', + name='Galactic Empire', + ) + empire.save() + + + xwing = Ship( + id='1', + name='X-Wing', + faction=rebels, + ) + xwing.save() + + ywing = Ship( + id='2', + name='Y-Wing', + faction=rebels, + ) + ywing.save() + + awing = Ship( + id='3', + name='A-Wing', + faction=rebels, + ) + awing.save() + + # 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', + faction=rebels, + ) + falcon.save() + + homeOne = Ship( + id='5', + name='Home One', + faction=rebels, + ) + homeOne.save() + + tieFighter = Ship( + id='6', + name='TIE Fighter', + faction=empire, + ) + tieFighter.save() + + tieInterceptor = Ship( + id='7', + name='TIE Interceptor', + faction=empire, + ) + tieInterceptor.save() + + executor = Ship( + id='8', + name='Executor', + faction=empire, + ) + executor.save() + + +def createShip(shipName, factionId): + nextShip = len(data['Ship'].keys())+1 + newShip = Ship( + id=str(nextShip), + name=shipName + ) + newShip.save() + return newShip + + +def getShip(_id): + return Ship.objects.get(id=_id) + +def getFaction(_id): + return Faction.objects.get(id=_id) + +def getRebels(): + return getFaction(1) + +def getEmpire(): + return getFaction(2) diff --git a/tests/starwars_django/models.py b/tests/starwars_django/models.py new file mode 100644 index 00000000..6afa152e --- /dev/null +++ b/tests/starwars_django/models.py @@ -0,0 +1,17 @@ +from __future__ import absolute_import +from django.db import models + + +class Faction(models.Model): + name = models.CharField(max_length=50) + + def __str__(self): + return self.name + + +class Ship(models.Model): + name = models.CharField(max_length=50) + faction = models.ForeignKey(Faction, related_name='ships') + + def __str__(self): + return self.name diff --git a/tests/starwars_django/schema.py b/tests/starwars_django/schema.py new file mode 100644 index 00000000..63f499b7 --- /dev/null +++ b/tests/starwars_django/schema.py @@ -0,0 +1,48 @@ +import graphene +from graphene import resolve_only_args, relay +from graphene.contrib.django import ( + DjangoObjectType, + DjangoNode +) +from .models import Ship as ShipModel, Faction as FactionModel +from .data import ( + getFaction, + getShip, + getRebels, + getEmpire, +) + +schema = graphene.Schema(name='Starwars Django Relay Schema') + +class Ship(DjangoNode): + class Meta: + model = ShipModel + + @classmethod + def get_node(cls, id): + return Ship(getShip(id)) + +class Faction(DjangoNode): + class Meta: + model = FactionModel + + @classmethod + def get_node(cls, id): + return Faction(getFaction(id)) + + +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.query = Query diff --git a/tests/starwars_django/test_connections.py b/tests/starwars_django/test_connections.py new file mode 100644 index 00000000..5c033216 --- /dev/null +++ b/tests/starwars_django/test_connections.py @@ -0,0 +1,43 @@ +# import pytest +# from graphql.core import graphql + +# from .models import * +# from .schema import schema +# from .data import initialize, getFaction + +# pytestmark = pytest.mark.django_db + +# def test_correct_fetch_first_ship_rebels(): +# initialize() +# print schema.Faction._meta.fields_map +# 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 not result.errors +# assert result.data == expected diff --git a/tests/starwars_django/test_objectidentification.py b/tests/starwars_django/test_objectidentification.py new file mode 100644 index 00000000..3040bd92 --- /dev/null +++ b/tests/starwars_django/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 not result.errors +# 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 not result.errors +# 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 not result.errors +# 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 not result.errors +# 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 not result.errors +# assert result.data == expected diff --git a/tox.ini b/tox.ini index 735a042d..cd4b45a4 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py27 deps= pytest>=2.7.2 django>=1.8.0,<1.9 + pytest-django flake8 six blinker @@ -12,3 +13,6 @@ deps= commands= py.test flake8 + +[pytest] +DJANGO_SETTINGS_MODULE = tests.django_settings From 1e8746830e7ee05f86a6bdc059120997b4f35bfa Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 29 Sep 2015 23:50:23 -0700 Subject: [PATCH 26/77] Fixed tests with django starwars --- graphene/contrib/django/fields.py | 6 +- graphene/relay/fields.py | 3 +- tests/starwars_django/test_connections.py | 79 ++++--- .../test_objectidentification.py | 207 +++++++++--------- 4 files changed, 153 insertions(+), 142 deletions(-) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index a6d54f62..9b81ca1e 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -8,6 +8,8 @@ from graphene.utils import cached_property from graphene.env import get_global_schema from django.db.models.query import QuerySet +from django.db.models.manager import Manager + def get_type_for_model(schema, model): schema = schema or get_global_schema() @@ -20,9 +22,9 @@ def get_type_for_model(schema, model): class DjangoConnectionField(relay.ConnectionField): def wrap_resolved(self, value, instance, args, info): - if isinstance(value, QuerySet): + if isinstance(value, (QuerySet, Manager)): cls = instance.__class__ - value = [cls(s) for s in value] + value = [cls(s) for s in value.all()] return value diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index a888919d..963f9827 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -21,8 +21,9 @@ class ConnectionField(Field): 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' resolved = self.wrap_resolved(resolved, instance, args, info) + print resolved + assert isinstance(resolved, collections.Iterable), 'Resolved value from the connection field have to be iterable' return connectionFromArray(resolved, args) @cached_property diff --git a/tests/starwars_django/test_connections.py b/tests/starwars_django/test_connections.py index 5c033216..42fd81b6 100644 --- a/tests/starwars_django/test_connections.py +++ b/tests/starwars_django/test_connections.py @@ -1,43 +1,42 @@ -# import pytest -# from graphql.core import graphql +import pytest +from graphql.core import graphql -# from .models import * -# from .schema import schema -# from .data import initialize, getFaction +from .models import * +from .schema import schema +from .data import initialize -# pytestmark = pytest.mark.django_db +pytestmark = pytest.mark.django_db -# def test_correct_fetch_first_ship_rebels(): -# initialize() -# print schema.Faction._meta.fields_map -# 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 not result.errors -# assert result.data == expected +def test_correct_fetch_first_ship_rebels(): + initialize() + 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 not result.errors + assert result.data == expected diff --git a/tests/starwars_django/test_objectidentification.py b/tests/starwars_django/test_objectidentification.py index 3040bd92..93bf1b71 100644 --- a/tests/starwars_django/test_objectidentification.py +++ b/tests/starwars_django/test_objectidentification.py @@ -1,105 +1,114 @@ -# from pytest import raises -# from graphql.core import graphql +import pytest +from pytest import raises +from graphql.core import graphql +from .data import initialize -# from .schema import schema +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 not result.errors -# assert result.data == expected +pytestmark = pytest.mark.django_db -# 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 not result.errors -# assert result.data == expected +def test_correctly_fetches_id_name_rebels(): + initialize() + query = ''' + query RebelsQuery { + rebels { + id + name + } + } + ''' + expected = { + 'rebels': { + 'id': 'RmFjdGlvbjox', + 'name': 'Alliance to Restore the Republic' + } + } + result = schema.execute(query) + assert not result.errors + 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 not result.errors -# assert result.data == expected +def test_correctly_refetches_rebels(): + initialize() + 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 not result.errors + 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 not result.errors -# assert result.data == expected +def test_correctly_fetches_id_name_empire(): + initialize() + query = ''' + query EmpireQuery { + empire { + id + name + } + } + ''' + expected = { + 'empire': { + 'id': 'RmFjdGlvbjoy', + 'name': 'Galactic Empire' + } + } + result = schema.execute(query) + assert not result.errors + 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 not result.errors -# assert result.data == expected +def test_correctly_refetches_empire(): + initialize() + query = ''' + query EmpireRefetchQuery { + node(id: "RmFjdGlvbjoy") { + id + ... on Faction { + name + } + } + } + ''' + expected = { + 'node': { + 'id': 'RmFjdGlvbjoy', + 'name': 'Galactic Empire' + } + } + result = schema.execute(query) + assert not result.errors + assert result.data == expected + +def test_correctly_refetches_xwing(): + initialize() + query = ''' + query XWingRefetchQuery { + node(id: "U2hpcDox") { + id + ... on Ship { + name + } + } + } + ''' + expected = { + 'node': { + 'id': 'U2hpcDox', + 'name': 'X-Wing' + } + } + result = schema.execute(query) + assert not result.errors + assert result.data == expected From e14f1fdd34b5baa59c1100774a193494b2178379 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 29 Sep 2015 23:52:36 -0700 Subject: [PATCH 27/77] Removed resolved printing. --- graphene/relay/fields.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index 963f9827..48f252f7 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -22,7 +22,6 @@ class ConnectionField(Field): resolved = super(ConnectionField, self).resolve(instance, args, info) if resolved: resolved = self.wrap_resolved(resolved, instance, args, info) - print resolved assert isinstance(resolved, collections.Iterable), 'Resolved value from the connection field have to be iterable' return connectionFromArray(resolved, args) From 6673582e9dbed9fc618bb287e079b38e67c9df04 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 30 Sep 2015 00:11:40 -0700 Subject: [PATCH 28/77] Improved Django tests --- tests/contrib_django/models.py | 22 ++-------------------- tests/django_settings.py | 3 ++- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/tests/contrib_django/models.py b/tests/contrib_django/models.py index ab37eaf8..dac0258a 100644 --- a/tests/contrib_django/models.py +++ b/tests/contrib_django/models.py @@ -1,19 +1,4 @@ from __future__ import absolute_import -import django -from django.conf import settings - -settings.configure( - DATABASES={ - 'INSTALLED_APPS': [ - 'graphql.contrib.django', - ], - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'db_test.sqlite', - } - } -) - from django.db import models class Reporter(models.Model): @@ -25,7 +10,7 @@ class Reporter(models.Model): return "%s %s" % (self.first_name, self.last_name) class Meta: - app_label = 'graphql' + app_label = 'contrib_django' class Article(models.Model): headline = models.CharField(max_length=100) @@ -37,7 +22,4 @@ class Article(models.Model): class Meta: ordering = ('headline',) - app_label = 'graphql' - - -django.setup() + app_label = 'contrib_django' diff --git a/tests/django_settings.py b/tests/django_settings.py index 4f1bf8ce..faa4c15d 100644 --- a/tests/django_settings.py +++ b/tests/django_settings.py @@ -1,8 +1,9 @@ SECRET_KEY = 1 INSTALLED_APPS = [ - 'graphql.contrib.django', + 'graphene.contrib.django', 'tests.starwars_django', + 'tests.contrib_django', ] DATABASES={ From 72c88a19e596c00fef6a2bad91eb2e4a05e58b91 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 30 Sep 2015 01:09:37 -0700 Subject: [PATCH 29/77] Removed unused schema --- graphene/contrib/django/converter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index 5544f205..d8e5bb5f 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -59,7 +59,6 @@ def _(field, cls): @convert_django_field.register(models.ManyToManyField) @convert_django_field.register(models.ManyToOneRel) def _(field, cls): - schema = cls._meta.schema model_field = DjangoModelField(field.related_model) return ConnectionOrListField(model_field) From c945df606467437e620abdd4ffa7a8727f5c95ab Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 1 Oct 2015 01:54:52 -0700 Subject: [PATCH 30/77] Completed Django support. Improved tests. Changed schema behavior --- .travis.yml | 3 +- graphene/__init__.py | 2 +- graphene/contrib/django/fields.py | 24 ++++--- graphene/contrib/django/options.py | 5 +- graphene/contrib/django/types.py | 21 +++--- graphene/core/fields.py | 94 ++++++++++++++----------- graphene/core/options.py | 10 +-- graphene/core/schema.py | 50 +++++++------- graphene/core/types.py | 52 +++++++------- graphene/relay/__init__.py | 2 +- graphene/relay/connections.py | 17 ++--- graphene/relay/fields.py | 26 +++++-- graphene/relay/nodes.py | 37 ---------- graphene/relay/types.py | 49 +++++++++++++ graphene/utils.py | 16 +++++ tests/contrib_django/test_schema.py | 1 + tests/contrib_django/test_types.py | 65 ++++++++++++++++++ tests/core/test_fields.py | 49 ++++++++++--- tests/core/test_query.py | 68 ++++++++++++++++++ tests/core/test_schema.py | 103 ++++++++++++++++++++++++++++ tests/core/test_types.py | 24 +++++-- tests/relay/test_relay.py | 8 ++- tests/starwars_django/data.py | 3 + tests/starwars_django/schema.py | 6 ++ 24 files changed, 543 insertions(+), 192 deletions(-) delete mode 100644 graphene/relay/nodes.py create mode 100644 graphene/relay/types.py create mode 100644 tests/contrib_django/test_types.py create mode 100644 tests/core/test_query.py create mode 100644 tests/core/test_schema.py diff --git a/.travis.yml b/.travis.yml index 83d35e1a..208ed4e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,8 @@ python: - 2.7 install: - pip install pytest pytest-cov coveralls flake8 six blinker -- pip install -e .[django] +# - pip install -e .[django] # TODO: Commented until graphqllib is in pypi +- pip install Django>=1.8.0 pytest-django singledispatch>=3.4.0.3 - 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 61996539..4c16032c 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -35,4 +35,4 @@ from graphene.decorators import ( resolve_only_args ) -import graphene.relay +# import graphene.relay diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 9b81ca1e..84a5f3a5 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -4,9 +4,11 @@ from graphene.core.fields import ( from graphene import relay from graphene.core.fields import Field, LazyField -from graphene.utils import cached_property +from graphene.utils import cached_property, memoize from graphene.env import get_global_schema +from graphene.relay.types import BaseNode + from django.db.models.query import QuerySet from django.db.models.manager import Manager @@ -29,11 +31,11 @@ class DjangoConnectionField(relay.ConnectionField): class ConnectionOrListField(LazyField): - def get_field(self): - schema = self.schema + @memoize + def get_field(self, schema): model_field = self.field_type - field_object_type = model_field.get_object_type() - if field_object_type and issubclass(field_object_type, schema.Node): + field_object_type = model_field.get_object_type(schema) + if field_object_type and issubclass(field_object_type, BaseNode): field = DjangoConnectionField(model_field) else: field = ListField(model_field) @@ -46,13 +48,13 @@ class DjangoModelField(Field): super(DjangoModelField, self).__init__(None, *args, **kwargs) self.model = model - @cached_property - def type(self): - _type = self.get_object_type() - return _type and _type._meta.type + @memoize + def internal_type(self, schema): + _type = self.get_object_type(schema) + return _type and _type.internal_type(schema) - def get_object_type(self): - _type = get_type_for_model(self.schema, self.model) + def get_object_type(self, schema): + _type = get_type_for_model(schema, self.model) if not _type and self.object_type._meta.only_fields: # We will only raise the exception if the related field is specified in only_fields raise Exception("Field %s (%s) model not mapped in current schema" % (self, self.model._meta.object_name)) diff --git a/graphene/contrib/django/options.py b/graphene/contrib/django/options.py index 4560080a..75b0738f 100644 --- a/graphene/contrib/django/options.py +++ b/graphene/contrib/django/options.py @@ -5,6 +5,7 @@ from graphene.core.options import Options VALID_ATTRS = ('model', 'only_fields') +from graphene.relay.types import Node, BaseNode class DjangoOptions(Options): def __init__(self, *args, **kwargs): @@ -15,9 +16,9 @@ class DjangoOptions(Options): def contribute_to_class(self, cls, name): super(DjangoOptions, self).contribute_to_class(cls, name) - if self.proxy: + if cls.__name__ == 'DjangoNode': return if not self.model: - raise Exception('Django ObjectType %s must have a model in the Meta attr' % cls) + raise Exception('Django ObjectType %s must have a model in the Meta class attr' % cls) elif not inspect.isclass(self.model) or not issubclass(self.model, models.Model): raise Exception('Provided model in %s is not a Django model' % cls) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index cd6831be..24f7612c 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -1,11 +1,11 @@ import six from django.db import models -from graphene.core.types import ObjectTypeMeta, ObjectType +from graphene.core.types import ObjectTypeMeta, BaseObjectType from graphene.contrib.django.options import DjangoOptions from graphene.contrib.django.converter import convert_django_field -from graphene.relay import Node +from graphene.relay.types import Node, BaseNode def get_reverse_fields(model): @@ -18,6 +18,9 @@ def get_reverse_fields(model): class DjangoObjectTypeMeta(ObjectTypeMeta): options_cls = DjangoOptions + def is_interface(cls, parents): + return DjangoInterface in parents + def add_extra_fields(cls): if not cls._meta.model: return @@ -30,11 +33,13 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): cls.add_to_class(field.name, converted_field) -class DjangoObjectType(six.with_metaclass(DjangoObjectTypeMeta, ObjectType)): - class Meta: - proxy = True +class DjangoObjectType(six.with_metaclass(DjangoObjectTypeMeta, BaseObjectType)): + pass -class DjangoNode(six.with_metaclass(DjangoObjectTypeMeta, Node)): - class Meta: - proxy = True +class DjangoInterface(six.with_metaclass(DjangoObjectTypeMeta, BaseObjectType)): + pass + + +class DjangoNode(BaseNode, DjangoInterface): + pass \ No newline at end of file diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 97a60fbe..f11c951a 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -10,8 +10,8 @@ from graphql.core.type import ( GraphQLArgument, GraphQLFloat, ) -from graphene.utils import cached_property -from graphene.core.types import ObjectType +from graphene.utils import cached_property, memoize +from graphene.core.types import BaseObjectType class Field(object): def __init__(self, field_type, resolve=None, null=True, args=None, description='', **extra_args): @@ -27,7 +27,6 @@ class Field(object): def contribute_to_class(self, cls, name): self.field_name = name self.object_type = cls - self.schema = cls._meta.schema if isinstance(self.field_type, Field) and not self.field_type.object_type: self.field_type.contribute_to_class(cls, name) cls._meta.add_field(self) @@ -45,42 +44,39 @@ class Field(object): resolve_fn = lambda root, args, info: root.resolve(self.field_name, args, info) return resolve_fn(instance, args, info) - def get_object_type(self): + def get_object_type(self, schema): field_type = self.field_type _is_class = inspect.isclass(field_type) if isinstance(field_type, Field): - return field_type.get_object_type() - if _is_class and issubclass(field_type, ObjectType): + return field_type.get_object_type(schema) + if _is_class and issubclass(field_type, BaseObjectType): return field_type elif isinstance(field_type, basestring): if field_type == 'self': return self.object_type - elif self.schema: - return self.schema.get_type(field_type) - - @cached_property - def type(self): - field_type = self.field_type - if isinstance(field_type, Field): - field_type = self.field_type.type - else: - object_type = self.get_object_type() - if object_type: - field_type = object_type._meta.type - - field_type = self.type_wrapper(field_type) - return field_type + else: + return schema.get_type(field_type) def type_wrapper(self, field_type): if not self.null: field_type = GraphQLNonNull(field_type) return field_type - @cached_property - def field(self): - # if not self.field_type: - # raise Exception('Must specify a field GraphQL type for the field %s'%self.field_name) + @memoize + def internal_type(self, schema): + field_type = self.field_type + if isinstance(field_type, Field): + field_type = self.field_type.internal_type(schema) + else: + object_type = self.get_object_type(schema) + if object_type: + field_type = object_type.internal_type(schema) + field_type = self.type_wrapper(field_type) + return field_type + + @memoize + def internal_field(self, schema): if not self.object_type: raise Exception('Field could not be constructed in a non graphene.Type or graphene.Interface') @@ -97,8 +93,10 @@ class Field(object): ','.join(meta_attrs.keys()) )) + internal_type = self.internal_type(schema) + return GraphQLField( - self.type, + internal_type, description=self.description, args=self.args, resolver=self.resolver, @@ -122,30 +120,46 @@ class Field(object): class NativeField(Field): def __init__(self, field=None): super(NativeField, self).__init__(None) - self.field = field or getattr(self, 'field') + self.field = field + + def get_field(self, schema): + return self.field + + @memoize + def internal_field(self, schema): + return self.get_field(schema) + + @memoize + def internal_type(self, schema): + return self.internal_field(schema).type class LazyField(Field): - @cached_property - def inner_field(self): - return self.get_field() + @memoize + def inner_field(self, schema): + return self.get_field(schema) - @cached_property - def type(self): - return self.inner_field.type + def internal_type(self, schema): + return self.inner_field(schema).internal_type(schema) - @cached_property - def field(self): - return self.inner_field.field + def internal_field(self, schema): + return self.inner_field(schema).internal_field(schema) -class LazyNativeField(LazyField): +class LazyNativeField(NativeField): def __init__(self, *args, **kwargs): super(LazyNativeField, self).__init__(None, *args, **kwargs) - @cached_property - def field(self): - return self.inner_field + def get_field(self, schema): + raise NotImplementedError("get_field function not implemented for %s LazyField" % self.__class__) + + @memoize + def internal_field(self, schema): + return self.get_field(schema) + + @memoize + def internal_type(self, schema): + return self.internal_field(schema).type class TypeField(Field): diff --git a/graphene/core/options.py b/graphene/core/options.py index 6799e517..aab8efc6 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -1,17 +1,15 @@ -from graphene.env import get_global_schema from graphene.utils import cached_property -DEFAULT_NAMES = ('description', 'name', 'interface', 'schema', +DEFAULT_NAMES = ('description', 'name', 'interface', 'type_name', 'interfaces', 'proxy') class Options(object): - def __init__(self, meta=None, schema=None): + def __init__(self, meta=None): self.meta = meta self.local_fields = [] self.interface = False self.proxy = False - self.schema = schema or get_global_schema() self.interfaces = [] self.parents = [] self.valid_attrs = DEFAULT_NAMES @@ -71,7 +69,3 @@ class Options(object): @cached_property def fields_map(self): return {f.field_name: f for f in self.fields} - - @cached_property - def type(self): - return self.parent.get_graphql_type() diff --git a/graphene/core/schema.py b/graphene/core/schema.py index db4f0345..47180b12 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -1,3 +1,5 @@ +from functools import wraps + from graphql.core import graphql from graphql.core.type import ( GraphQLSchema @@ -10,10 +12,10 @@ class Schema(object): _query = None def __init__(self, query=None, mutation=None, name='Schema'): + self._internal_types = {} self.mutation = mutation self.query = query self.name = name - self._types = {} signals.init_schema.send(self) def __repr__(self): @@ -25,34 +27,33 @@ class Schema(object): @query.setter def query(self, query): - if not query: - return self._query = query - self._query_type = query._meta.type - self._schema = GraphQLSchema(query=self._query_type, mutation=self.mutation) + self._query_type = query and query.internal_type(self) - def register_type(self, type): - type_name = type._meta.type_name - if type_name in self._types: - raise Exception('Type name %s already registered in %r' % (type_name, self)) - self._types[type_name] = type + @cached_property + def schema(self): + if not self._query_type: + raise Exception('You have to define a base query type') + return GraphQLSchema(query=self._query_type, mutation=self.mutation) + + def associate_internal_type(self, internal_type, object_type): + self._internal_types[internal_type.name] = object_type def get_type(self, type_name): - if type_name not in self._types: + # print 'get_type' + # _type = self.schema.get_type(type_name) + if type_name not in self._internal_types: raise Exception('Type %s not found in %r' % (type_name, self)) - return self._types[type_name] - - def __getattr__(self, name): - return self.get_type(name) + return self._internal_types[type_name] @property def types(self): - return self._types - + return self._internal_types + def execute(self, request='', root=None, vars=None, operation_name=None): root = root or object() return graphql( - self._schema, + self.schema, request=request, root=self.query(root), vars=vars, @@ -62,9 +63,12 @@ class Schema(object): def introspect(self): return self._schema.get_type_map() +def register_internal_type(fun): + @wraps(fun) + def wrapper(cls, schema): + internal_type = fun(cls, schema) + if isinstance(schema, Schema): + schema.associate_internal_type(internal_type, cls) + return internal_type -@signals.class_prepared.connect -def object_type_created(object_type): - schema = object_type._meta.schema - if schema: - schema.register_type(object_type) + return wrapper diff --git a/graphene/core/types.py b/graphene/core/types.py index 432bf2a9..1b695cc1 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -8,11 +8,15 @@ from graphql.core.type import ( from graphene import signals from graphene.core.options import Options - +from graphene.utils import memoize +from graphene.core.schema import register_internal_type class ObjectTypeMeta(type): options_cls = Options + def is_interface(cls, parents): + return Interface in parents + def __new__(cls, name, bases, attrs): super_new = super(ObjectTypeMeta, cls).__new__ parents = [b for b in bases if isinstance(b, cls)] @@ -27,7 +31,6 @@ class ObjectTypeMeta(type): '__doc__': doc }) attr_meta = attrs.pop('Meta', None) - proxy = None if not attr_meta: meta = None # meta = getattr(new_class, 'Meta', None) @@ -36,13 +39,9 @@ class ObjectTypeMeta(type): base_meta = getattr(new_class, '_meta', None) - schema = (base_meta and base_meta.schema) - - new_class.add_to_class('_meta', new_class.options_cls(meta, schema)) - - if base_meta and base_meta.proxy: - new_class._meta.interface = base_meta.interface - + new_class.add_to_class('_meta', new_class.options_cls(meta)) + + new_class._meta.interface = new_class.is_interface(parents) # Add all attributes to the class. for obj_name, obj in attrs.items(): new_class.add_to_class(obj_name, obj) @@ -93,13 +92,13 @@ class ObjectTypeMeta(type): setattr(cls, name, value) -class ObjectType(six.with_metaclass(ObjectTypeMeta)): +class BaseObjectType(object): def __new__(cls, instance=None, *args, **kwargs): if cls._meta.interface: raise Exception("An interface cannot be initialized") if instance == None: return None - return super(ObjectType, cls).__new__(cls, instance, *args, **kwargs) + return super(BaseObjectType, cls).__new__(cls, instance, *args, **kwargs) def __init__(self, instance=None): signals.pre_init.send(self.__class__, instance=instance) @@ -128,28 +127,35 @@ class ObjectType(six.with_metaclass(ObjectTypeMeta)): return True @classmethod - def resolve_type(cls, instance, *_): - return instance._meta.type + def resolve_type(cls, schema, instance, *_): + return instance.internal_type(schema) @classmethod - def get_graphql_type(cls): - fields = cls._meta.fields_map + @memoize + @register_internal_type + def internal_type(cls, schema): + fields_map = cls._meta.fields_map + fields = lambda: { + name: field.internal_field(schema) + for name, field in fields_map.items() + } if cls._meta.interface: return GraphQLInterfaceType( cls._meta.type_name, description=cls._meta.description, - resolve_type=cls.resolve_type, - fields=lambda: {name: field.field for name, field in fields.items()} + resolve_type=lambda *args, **kwargs: cls.resolve_type(schema, *args, **kwargs), + fields=fields ) 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()} + interfaces=[i.internal_type(schema) for i in cls._meta.interfaces], + fields=fields ) -class Interface(ObjectType): - class Meta: - interface = True - proxy = True +class ObjectType(six.with_metaclass(ObjectTypeMeta, BaseObjectType)): + pass + +class Interface(six.with_metaclass(ObjectTypeMeta, BaseObjectType)): + pass diff --git a/graphene/relay/__init__.py b/graphene/relay/__init__.py index ec44068e..67180b18 100644 --- a/graphene/relay/__init__.py +++ b/graphene/relay/__init__.py @@ -5,6 +5,6 @@ from graphene.relay.fields import ( import graphene.relay.connections -from graphene.relay.nodes import ( +from graphene.relay.types import ( Node ) diff --git a/graphene/relay/connections.py b/graphene/relay/connections.py index 5cda4705..815f3617 100644 --- a/graphene/relay/connections.py +++ b/graphene/relay/connections.py @@ -1,24 +1,15 @@ from graphql_relay.node.node import ( globalIdField ) -from graphql_relay.connection.connection import ( - connectionDefinitions -) from graphene import signals -from graphene.core.fields import NativeField - +from graphene.relay.fields import NodeIDField +from graphene.relay.types import BaseNode, Node @signals.class_prepared.connect def object_type_created(object_type): - schema = object_type._meta.schema - if hasattr(schema, 'Node') and issubclass(object_type, schema.Node) and object_type != schema.Node: - if object_type._meta.proxy: - return + if issubclass(object_type, BaseNode) and BaseNode not in object_type.__bases__: type_name = object_type._meta.type_name - field = NativeField(globalIdField(type_name)) + field = NodeIDField() 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/graphene/relay/fields.py b/graphene/relay/fields.py index 48f252f7..b078af78 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -6,8 +6,13 @@ from graphql_relay.connection.arrayconnection import ( from graphql_relay.connection.connection import ( connectionArgs ) +from graphql_relay.node.node import ( + globalIdField +) + from graphene.core.fields import Field, LazyNativeField from graphene.utils import cached_property +from graphene.utils import memoize class ConnectionField(Field): @@ -25,13 +30,20 @@ class ConnectionField(Field): 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 = self.get_object_type() - assert issubclass(object_type, self.schema.Node), 'Only nodes have connections.' - return object_type.connection + @memoize + def internal_type(self, schema): + from graphene.relay.types import BaseNode + object_type = self.get_object_type(schema) + assert issubclass(object_type, BaseNode), 'Only nodes have connections.' + return object_type.get_connection(schema) class NodeField(LazyNativeField): - def get_field(self): - return self.schema.Node._definitions.nodeField + def get_field(self, schema): + from graphene.relay.types import BaseNode + return BaseNode.get_definitions(schema).nodeField + + +class NodeIDField(LazyNativeField): + def get_field(self, schema): + return globalIdField(self.object_type._meta.type_name) diff --git a/graphene/relay/nodes.py b/graphene/relay/nodes.py deleted file mode 100644 index 1b0c51bc..00000000 --- a/graphene/relay/nodes.py +++ /dev/null @@ -1,37 +0,0 @@ -from graphql_relay.node.node import ( - nodeDefinitions, - fromGlobalId -) -from graphene.env import get_global_schema -from graphene.core.types import Interface -from graphene.core.fields import LazyNativeField - - -def get_node_type(obj): - return obj._meta.type - - -def get_node(schema, globalId, *args): - resolvedGlobalId = fromGlobalId(globalId) - _type, _id = resolvedGlobalId.type, resolvedGlobalId.id - object_type = schema.get_type(_type) - return object_type.get_node(_id) - -class Node(Interface): - _definitions = None - - @classmethod - def contribute_to_schema(cls, schema): - if cls._definitions: - return - schema = cls._meta.schema - cls._definitions = nodeDefinitions(lambda *args: get_node(schema, *args), get_node_type) - - @classmethod - def get_graphql_type(cls): - if cls is cls._meta.schema.Node: - # Return only nodeInterface when is the Node Inerface - cls.contribute_to_schema(cls._meta.schema) - return cls._definitions.nodeInterface - return super(Node, cls).get_graphql_type() - diff --git a/graphene/relay/types.py b/graphene/relay/types.py new file mode 100644 index 00000000..53919f1f --- /dev/null +++ b/graphene/relay/types.py @@ -0,0 +1,49 @@ +from graphql_relay.node.node import ( + nodeDefinitions, + fromGlobalId +) +from graphql_relay.connection.connection import ( + connectionDefinitions +) + +from graphene.env import get_global_schema +from graphene.core.types import Interface +from graphene.core.fields import LazyNativeField +from graphene.utils import memoize + + +def get_node_type(schema, obj): + return obj.internal_type(schema) + + +def get_node(schema, globalId, *args): + resolvedGlobalId = fromGlobalId(globalId) + _type, _id = resolvedGlobalId.type, resolvedGlobalId.id + object_type = schema.get_type(_type) + return object_type.get_node(_id) + + +class BaseNode(object): + @classmethod + @memoize + def get_definitions(cls, schema): + return nodeDefinitions(lambda *args: get_node(schema, *args), lambda *args: get_node_type(schema, *args)) + + @classmethod + @memoize + def get_connection(cls, schema): + _type = cls.internal_type(schema) + type_name = cls._meta.type_name + connection = connectionDefinitions(type_name, _type).connectionType + return connection + + @classmethod + def internal_type(cls, schema): + if cls is Node or BaseNode in cls.__bases__: + # Return only nodeInterface when is the Node Inerface + return BaseNode.get_definitions(schema).nodeInterface + return super(BaseNode, cls).internal_type(schema) + + +class Node(BaseNode, Interface): + pass diff --git a/graphene/utils.py b/graphene/utils.py index 709b74a3..c944df9e 100644 --- a/graphene/utils.py +++ b/graphene/utils.py @@ -1,3 +1,5 @@ +from functools import wraps + class cached_property(object): """ A property that is only computed once per instance and then replaces itself @@ -14,3 +16,17 @@ class cached_property(object): return self value = obj.__dict__[self.func.__name__] = self.func(obj) return value + + +def memoize(fun): + """A simple memoize decorator for functions supporting positional args.""" + @wraps(fun) + def wrapper(*args, **kwargs): + key = (args, frozenset(sorted(kwargs.items()))) + try: + return cache[key] + except KeyError: + ret = cache[key] = fun(*args, **kwargs) + return ret + cache = {} + return wrapper diff --git a/tests/contrib_django/test_schema.py b/tests/contrib_django/test_schema.py index 669c8a7f..6605f994 100644 --- a/tests/contrib_django/test_schema.py +++ b/tests/contrib_django/test_schema.py @@ -114,6 +114,7 @@ def test_should_node(): class Query1(graphene.ObjectType): node = relay.NodeField() reporter = graphene.Field(ReporterNodeType) + article = graphene.Field(ArticleNodeType) def resolve_reporter(self, *args, **kwargs): return ReporterNodeType(Reporter(id=1, first_name='ABA', last_name='X')) diff --git a/tests/contrib_django/test_types.py b/tests/contrib_django/test_types.py new file mode 100644 index 00000000..4aaa3e0b --- /dev/null +++ b/tests/contrib_django/test_types.py @@ -0,0 +1,65 @@ +from py.test import raises +from collections import namedtuple +from pytest import raises +from graphene.core.fields import ( + Field, + StringField, +) +from graphql.core.type import ( + GraphQLObjectType, + GraphQLInterfaceType +) + +from graphene import Schema +from graphene.contrib.django.types import ( + DjangoNode, + DjangoInterface +) + +from .models import Reporter, Article + + +class Character(DjangoInterface): + '''Character description''' + class Meta: + model = Reporter + + +class Human(DjangoNode): + '''Human description''' + def get_node(self, id): + pass + + class Meta: + model = Article + +schema = Schema() + + +def test_django_interface(): + assert DjangoNode._meta.interface == True + +def test_pseudo_interface(): + object_type = Character.internal_type(schema) + assert Character._meta.interface == True + assert isinstance(object_type, GraphQLInterfaceType) + assert Character._meta.model == Reporter + assert object_type.get_fields().keys() == ['articles', 'first_name', 'last_name', 'id', 'email'] + + +def test_interface_resolve_type(): + resolve_type = Character.resolve_type(schema, Human(object())) + assert isinstance(resolve_type, GraphQLObjectType) + + +def test_object_type(): + object_type = Human.internal_type(schema) + assert Human._meta.interface == False + assert isinstance(object_type, GraphQLObjectType) + assert object_type.get_fields() == { + 'headline': Human._meta.fields_map['headline'].internal_field(schema), + 'id': Human._meta.fields_map['id'].internal_field(schema), + 'reporter': Human._meta.fields_map['reporter'].internal_field(schema), + 'pub_date': Human._meta.fields_map['pub_date'].internal_field(schema), + } + assert object_type.get_interfaces() == [DjangoNode.internal_type(schema)] diff --git a/tests/core/test_fields.py b/tests/core/test_fields.py index 18cb75e1..eb11af45 100644 --- a/tests/core/test_fields.py +++ b/tests/core/test_fields.py @@ -28,34 +28,65 @@ ot = ObjectType() ObjectType._meta.contribute_to_class(ObjectType, '_meta') +class Schema(object): + pass + +schema = Schema() + def test_field_no_contributed_raises_error(): f = Field(GraphQLString) with raises(Exception) as excinfo: - f.field + f.internal_field(schema) def test_field_type(): f = Field(GraphQLString) f.contribute_to_class(ot, 'field_name') - assert isinstance(f.field, GraphQLField) - assert f.type == GraphQLString + assert isinstance(f.internal_field(schema), GraphQLField) + assert f.internal_type(schema) == GraphQLString def test_stringfield_type(): f = StringField() f.contribute_to_class(ot, 'field_name') - assert f.type == GraphQLString + assert f.internal_type(schema) == GraphQLString def test_stringfield_type_null(): f = StringField(null=False) f.contribute_to_class(ot, 'field_name') - assert isinstance(f.field, GraphQLField) - assert isinstance(f.type, GraphQLNonNull) + assert isinstance(f.internal_field(schema), GraphQLField) + assert isinstance(f.internal_type(schema), GraphQLNonNull) def test_field_resolve(): - f = StringField(null=False) + f = StringField(null=False, resolve=lambda *args:'RESOLVED') f.contribute_to_class(ot, 'field_name') - field_type = f.field - field_type.resolver(ot,2,3) + field_type = f.internal_field(schema) + assert 'RESOLVED' == field_type.resolver(ot,2,3) + + +def test_field_resolve_type_custom(): + class MyCustomType(object): + pass + + class Schema(object): + def get_type(self, name): + if name == 'MyCustomType': + return MyCustomType + + s = Schema() + + f = Field('MyCustomType') + f.contribute_to_class(ot, 'field_name') + field_type = f.get_object_type(s) + assert field_type == MyCustomType + + +def test_field_resolve_type_custom(): + s = Schema() + + f = Field('self') + f.contribute_to_class(ot, 'field_name') + field_type = f.get_object_type(s) + assert field_type == ot diff --git a/tests/core/test_query.py b/tests/core/test_query.py new file mode 100644 index 00000000..1ab947d1 --- /dev/null +++ b/tests/core/test_query.py @@ -0,0 +1,68 @@ +from py.test import raises +from collections import namedtuple +from pytest import raises +from graphql.core import graphql +from graphene.core.fields import ( + Field, + StringField, + ListField, +) +from graphql.core.type import ( + GraphQLObjectType, + GraphQLSchema, + GraphQLInterfaceType +) + +from graphene.core.types import ( + Interface, + ObjectType +) + + +class Character(Interface): + name = StringField() + + +class Pet(ObjectType): + type = StringField(resolve=lambda *_:'Dog') + + +class Human(Character): + friends = ListField(Character) + pet = Field(Pet) + + def resolve_name(self, *args): + return 'Peter' + + def resolve_friend(self, *args): + return Human(object()) + + def resolve_pet(self, *args): + return Pet(object()) + # def resolve_friends(self, *args, **kwargs): + # return 'HEY YOU!' + +schema = object() + +Human_type = Human.internal_type(schema) + + +def test_query(): + schema = GraphQLSchema(query=Human_type) + query = ''' + { + name + pet { + type + } + } + ''' + expected = { + 'name': 'Peter', + 'pet': { + 'type':'Dog' + } + } + result = graphql(schema, query, root=Human(object())) + assert not result.errors + assert result.data == expected diff --git a/tests/core/test_schema.py b/tests/core/test_schema.py new file mode 100644 index 00000000..3a11c90f --- /dev/null +++ b/tests/core/test_schema.py @@ -0,0 +1,103 @@ +from py.test import raises +from collections import namedtuple +from pytest import raises +from graphql.core import graphql +from graphene.core.fields import ( + Field, + StringField, + ListField, +) +from graphql.core.type import ( + GraphQLObjectType, + GraphQLSchema, + GraphQLInterfaceType +) + +from graphene import ( + Interface, + ObjectType, + Schema +) + + +schema = Schema(name='My own schema') + + +class Character(Interface): + name = StringField() + + +class Pet(ObjectType): + type = StringField(resolve=lambda *_:'Dog') + + +class Human(Character): + friends = ListField(Character) + pet = Field(Pet) + + def resolve_name(self, *args): + return 'Peter' + + def resolve_friend(self, *args): + return Human(object()) + + def resolve_pet(self, *args): + return Pet(object()) + +schema.query = Human + +def test_get_registered_type(): + assert schema.get_type('Character') == Character + +def test_get_unregistered_type(): + with raises(Exception) as excinfo: + schema.get_type('NON_EXISTENT_MODEL') + assert 'not found' in str(excinfo.value) + +def test_schema_query(): + assert schema.query == Human + +def test_query_schema_graphql(): + a = object() + query = ''' + { + name + pet { + type + } + } + ''' + expected = { + 'name': 'Peter', + 'pet': { + 'type':'Dog' + } + } + result = graphql(schema.schema, query, root=Human(object())) + assert not result.errors + assert result.data == expected + + +def test_query_schema_execute(): + a = object() + query = ''' + { + name + pet { + type + } + } + ''' + expected = { + 'name': 'Peter', + 'pet': { + 'type':'Dog' + } + } + result = schema.execute(query, root=object()) + assert not result.errors + assert result.data == expected + + +def test_schema_get_type_map(): + assert schema.schema.get_type_map().keys() == ['__Field', 'String', 'Pet', 'Character', '__InputValue', '__Directive', '__TypeKind', '__Schema', '__Type', 'Human', '__EnumValue', 'Boolean'] diff --git a/tests/core/test_types.py b/tests/core/test_types.py index 0a83398a..c79d0c74 100644 --- a/tests/core/test_types.py +++ b/tests/core/test_types.py @@ -15,31 +15,43 @@ from graphene.core.types import ( ObjectType ) + class Character(Interface): '''Character description''' name = StringField() class Meta: type_name = 'core.Character' + class Human(Character): '''Human description''' friends = StringField() + class Meta: type_name = 'core.Human' +schema = object() + + def test_interface(): - object_type = Character._meta.type + object_type = Character.internal_type(schema) assert Character._meta.interface == True - assert Character._meta.type_name == 'core.Character' assert isinstance(object_type, GraphQLInterfaceType) + assert Character._meta.type_name == 'core.Character' assert object_type.description == 'Character description' - assert object_type.get_fields() == {'name': Character._meta.fields_map['name'].field} + assert object_type.get_fields() == {'name': Character._meta.fields_map['name'].internal_field(schema)} + + +def test_interface_resolve_type(): + resolve_type = Character.resolve_type(schema, Human(object())) + assert isinstance(resolve_type, GraphQLObjectType) + def test_object_type(): - object_type = Human._meta.type + object_type = Human.internal_type(schema) assert Human._meta.interface == False assert Human._meta.type_name == 'core.Human' assert isinstance(object_type, GraphQLObjectType) assert object_type.description == 'Human description' - 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] + assert object_type.get_fields() == {'name': Character._meta.fields_map['name'].internal_field(schema), 'friends': Human._meta.fields_map['friends'].internal_field(schema)} + assert object_type.get_interfaces() == [Character.internal_type(schema)] diff --git a/tests/relay/test_relay.py b/tests/relay/test_relay.py index 2bcdc836..5b16bc49 100644 --- a/tests/relay/test_relay.py +++ b/tests/relay/test_relay.py @@ -21,8 +21,12 @@ def test_field_no_contributed_raises_error(): assert 'get_node' in str(excinfo.value) -def test_node_should_have_connection(): - assert OtherNode.connection +def test_node_should_have_same_connection_always(): + s = object() + connection1 = OtherNode.get_connection(s) + connection2 = OtherNode.get_connection(s) + + assert connection1 == connection2 def test_node_should_have_id_field(): diff --git a/tests/starwars_django/data.py b/tests/starwars_django/data.py index 552c2ebe..690627b1 100644 --- a/tests/starwars_django/data.py +++ b/tests/starwars_django/data.py @@ -88,6 +88,9 @@ def createShip(shipName, factionId): def getShip(_id): return Ship.objects.get(id=_id) +def getShips(): + return Ship.objects.all() + def getFaction(_id): return Faction.objects.get(id=_id) diff --git a/tests/starwars_django/schema.py b/tests/starwars_django/schema.py index 63f499b7..c0ae9652 100644 --- a/tests/starwars_django/schema.py +++ b/tests/starwars_django/schema.py @@ -8,6 +8,7 @@ from .models import Ship as ShipModel, Faction as FactionModel from .data import ( getFaction, getShip, + getShips, getRebels, getEmpire, ) @@ -35,6 +36,11 @@ class Query(graphene.ObjectType): rebels = graphene.Field(Faction) empire = graphene.Field(Faction) node = relay.NodeField() + ships = relay.ConnectionField(Ship, description='All the ships.') + + @resolve_only_args + def resolve_ships(self): + return [Ship(s) for s in getShips()] @resolve_only_args def resolve_rebels(self): From 311e1048d34d01cf85c1844227e1040286885059 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 1 Oct 2015 02:08:00 -0700 Subject: [PATCH 31/77] =?UTF-8?q?Simplified=20README.=20Add=20Django=20exa?= =?UTF-8?q?mple=20=F0=9F=92=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 77 +++++++++++++------------------------------------------ 1 file changed, 18 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 9e6b9e28..688a6795 100644 --- a/README.md +++ b/README.md @@ -20,44 +20,17 @@ 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] + return [Human(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) - ) + human = graphene.Field(Human) - @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) +schema = graphene.Schema(query=Query) ``` ### Querying @@ -72,17 +45,14 @@ query = ''' } } ''' -result = Schema.execute(query) +result = schema.execute(query) ``` ### Relay Schema -Graphene also supports Relay, check the (Starwars Relay example)[/tests/starwars_relay]! +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.') @@ -92,39 +62,28 @@ class Ship(relay.Node): return Ship(getShip(id)) -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): - return Faction(getFaction(id) - - class Query(graphene.ObjectType): - rebels = graphene.Field(Faction) - empire = graphene.Field(Faction) + ships = relay.ConnectionField(Ship, description='The ships used by the faction.') node = relay.NodeField() @resolve_only_args - def resolve_rebels(self): - return Faction(getRebels()) + def resolve_ships(self): + return [Ship(s) for s in getShips()] - @resolve_only_args - def resolve_empire(self): - return Faction(getEmpire()) +``` +### Django+Relay Schema -Schema = graphene.Schema(query=Query) +Graphene also supports Relay, check the (Starwars Django example)[tests/starwars_django]! -# Later on, for querying -Schema.execute('''rebels { name }''') +```python +class Ship(DjangoNode): + class Meta: + model = YourDjangoModelHere + # only_fields = ('id', 'name') # Only map this fields from the model +class Query(graphene.ObjectType): + node = relay.NodeField() ``` ## Contributing From 2ba0a62a6c7153acdb722055ce63dd7e6eb5b075 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 1 Oct 2015 02:40:21 -0700 Subject: [PATCH 32/77] Refactored is_node into relay utils --- graphene/contrib/django/options.py | 10 ++++++++-- graphene/relay/__init__.py | 2 ++ graphene/relay/connections.py | 5 +++-- graphene/relay/types.py | 3 ++- graphene/relay/utils.py | 7 +++++++ 5 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 graphene/relay/utils.py diff --git a/graphene/contrib/django/options.py b/graphene/contrib/django/options.py index 75b0738f..50f0710a 100644 --- a/graphene/contrib/django/options.py +++ b/graphene/contrib/django/options.py @@ -2,10 +2,16 @@ import inspect from django.db import models from graphene.core.options import Options +from graphene.core.types import BaseObjectType +from graphene.relay.utils import is_node VALID_ATTRS = ('model', 'only_fields') -from graphene.relay.types import Node, BaseNode + +def is_base(cls): + from graphene.contrib.django.types import DjangoObjectType + return DjangoObjectType in cls.__bases__ + class DjangoOptions(Options): def __init__(self, *args, **kwargs): @@ -16,7 +22,7 @@ class DjangoOptions(Options): def contribute_to_class(self, cls, name): super(DjangoOptions, self).contribute_to_class(cls, name) - if cls.__name__ == 'DjangoNode': + if not is_node(cls) and not is_base(cls): return if not self.model: raise Exception('Django ObjectType %s must have a model in the Meta class attr' % cls) diff --git a/graphene/relay/__init__.py b/graphene/relay/__init__.py index 67180b18..dc636f67 100644 --- a/graphene/relay/__init__.py +++ b/graphene/relay/__init__.py @@ -8,3 +8,5 @@ import graphene.relay.connections from graphene.relay.types import ( Node ) + +from graphene.relay.utils import is_node diff --git a/graphene/relay/connections.py b/graphene/relay/connections.py index 815f3617..af3968c4 100644 --- a/graphene/relay/connections.py +++ b/graphene/relay/connections.py @@ -4,11 +4,12 @@ from graphql_relay.node.node import ( from graphene import signals from graphene.relay.fields import NodeIDField -from graphene.relay.types import BaseNode, Node +from graphene.relay.utils import is_node + @signals.class_prepared.connect def object_type_created(object_type): - if issubclass(object_type, BaseNode) and BaseNode not in object_type.__bases__: + if is_node(object_type): type_name = object_type._meta.type_name field = NodeIDField() object_type.add_to_class('id', field) diff --git a/graphene/relay/types.py b/graphene/relay/types.py index 53919f1f..763168b4 100644 --- a/graphene/relay/types.py +++ b/graphene/relay/types.py @@ -39,7 +39,8 @@ class BaseNode(object): @classmethod def internal_type(cls, schema): - if cls is Node or BaseNode in cls.__bases__: + from graphene.relay.utils import is_node_type + if is_node_type(cls): # Return only nodeInterface when is the Node Inerface return BaseNode.get_definitions(schema).nodeInterface return super(BaseNode, cls).internal_type(schema) diff --git a/graphene/relay/utils.py b/graphene/relay/utils.py new file mode 100644 index 00000000..1c3a1bc3 --- /dev/null +++ b/graphene/relay/utils.py @@ -0,0 +1,7 @@ +from graphene.relay.types import BaseNode + +def is_node(object_type): + return issubclass(object_type, BaseNode) and not is_node_type(object_type) + +def is_node_type(object_type): + return BaseNode in object_type.__bases__ From 0aba62514014d5c6e04145cfd11b6944f2a40000 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 1 Oct 2015 09:22:29 -0700 Subject: [PATCH 33/77] Fixed Django field convert bug pointed by @jhgg --- graphene/contrib/django/converter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index d8e5bb5f..91c7e756 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -21,6 +21,8 @@ def convert_django_field(field, cls): @convert_django_field.register(models.TextField) @convert_django_field.register(models.EmailField) @convert_django_field.register(models.SlugField) +@convert_django_field.register(models.URLField) +@convert_django_field.register(models.UUIDField) def _(field, cls): return StringField(description=field.description) @@ -34,8 +36,6 @@ def _(field, cls): @convert_django_field.register(models.PositiveSmallIntegerField) @convert_django_field.register(models.SmallIntegerField) @convert_django_field.register(models.BigIntegerField) -@convert_django_field.register(models.URLField) -@convert_django_field.register(models.UUIDField) @convert_django_field.register(models.IntegerField) def _(field, cls): return IntField(description=field.description) From 587f05e1d7d73add43ed31f8f263b471b2503879 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 1 Oct 2015 09:34:32 -0700 Subject: [PATCH 34/77] Removed unnecessary get_global_schema legacy code --- graphene/contrib/django/fields.py | 3 +-- graphene/relay/types.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 84a5f3a5..013bc157 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -5,7 +5,6 @@ from graphene import relay from graphene.core.fields import Field, LazyField from graphene.utils import cached_property, memoize -from graphene.env import get_global_schema from graphene.relay.types import BaseNode @@ -14,7 +13,7 @@ from django.db.models.manager import Manager def get_type_for_model(schema, model): - schema = schema or get_global_schema() + schema = schema types = schema.types.values() for _type in types: type_model = hasattr(_type,'_meta') and getattr(_type._meta, 'model', None) diff --git a/graphene/relay/types.py b/graphene/relay/types.py index 763168b4..a9eae9c0 100644 --- a/graphene/relay/types.py +++ b/graphene/relay/types.py @@ -6,7 +6,6 @@ from graphql_relay.connection.connection import ( connectionDefinitions ) -from graphene.env import get_global_schema from graphene.core.types import Interface from graphene.core.fields import LazyNativeField from graphene.utils import memoize From 9a3f11b802fefeb316169d49507717b3571a6e27 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 1 Oct 2015 09:40:51 -0700 Subject: [PATCH 35/77] =?UTF-8?q?Added=20extra=20info=20to=20README=20?= =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index be5f9ccc..7af681f5 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # Graphene: Python DSL for GraphQL This is a library to use GraphQL in Python in a easy way. -It will map the models/fields to internal GraphQL-py objects without effort. +It will map the models/fields to internal GraphQL-py objects without effort. Including automatic [Django models](#djangorelay-schema) conversion. [![Build Status](https://travis-ci.org/syrusakbary/graphene.svg?branch=master)](https://travis-ci.org/syrusakbary/graphene) [![Coverage Status](https://coveralls.io/repos/syrusakbary/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/syrusakbary/graphene?branch=master) +*Note: This library requires installing [graphqllib](https://github.com/dittos/graphqllib) and [graphql-relay](https://github.com/syrusakbary/graphql-relay-py) external libraries.* + ## Usage Example code of a GraphQL schema using Graphene: From 176696c1ac59447f0bd1c5bf4ae3d32195875c54 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 2 Oct 2015 22:17:51 -0700 Subject: [PATCH 36/77] Improved syntax using pep8 style guide --- graphene/contrib/django/__init__.py | 4 +- graphene/contrib/django/converter.py | 4 +- graphene/contrib/django/fields.py | 14 ++++-- graphene/contrib/django/options.py | 4 +- graphene/contrib/django/types.py | 2 +- graphene/core/fields.py | 17 +++++-- graphene/core/options.py | 6 ++- graphene/core/schema.py | 3 +- graphene/core/types.py | 11 +++-- graphene/decorators.py | 4 +- graphene/env.py | 9 ++-- graphene/relay/__init__.py | 6 +-- graphene/relay/connections.py | 3 +- graphene/relay/fields.py | 11 +++-- graphene/relay/types.py | 1 + graphene/relay/utils.py | 6 ++- graphene/utils.py | 2 + setup.py | 2 +- tests/contrib_django/data.py | 6 ++- tests/contrib_django/models.py | 2 + tests/contrib_django/test_schema.py | 26 +++++++---- tests/contrib_django/test_types.py | 7 ++- tests/core/test_fields.py | 10 ++++- tests/core/test_options.py | 8 ++++ tests/core/test_query.py | 6 +-- tests/core/test_schema.py | 15 ++++--- tests/core/test_types.py | 9 +++- tests/django_settings.py | 2 +- tests/relay/test_relay.py | 1 + tests/starwars/data.py | 29 ++++++------ tests/starwars/schema.py | 20 ++++----- tests/starwars/test_query.py | 14 +++--- tests/starwars_django/data.py | 6 ++- tests/starwars_django/schema.py | 8 +++- tests/starwars_django/test_connections.py | 21 ++++----- .../test_objectidentification.py | 45 ++++++++++--------- tests/starwars_relay/data.py | 10 +++-- tests/starwars_relay/schema.py | 6 ++- tests/starwars_relay/schema_other.py | 1 + tests/starwars_relay/test_connections.py | 21 ++++----- .../test_objectidentification.py | 45 ++++++++++--------- 41 files changed, 271 insertions(+), 156 deletions(-) diff --git a/graphene/contrib/django/__init__.py b/graphene/contrib/django/__init__.py index fab7622f..96edc3d3 100644 --- a/graphene/contrib/django/__init__.py +++ b/graphene/contrib/django/__init__.py @@ -1,4 +1,4 @@ from graphene.contrib.django.types import ( - DjangoObjectType, - DjangoNode + DjangoObjectType, + DjangoNode ) diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index 91c7e756..2595a68d 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -11,9 +11,11 @@ from graphene.core.fields import ( ) from graphene.contrib.django.fields import ConnectionOrListField, DjangoModelField + @singledispatch def convert_django_field(field, cls): - raise Exception("Don't know how to convert the Django field %s (%s)" % (field, field.__class__)) + raise Exception( + "Don't know how to convert the Django field %s (%s)" % (field, field.__class__)) @convert_django_field.register(models.DateField) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 013bc157..5bed67f5 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -16,12 +16,14 @@ def get_type_for_model(schema, model): schema = schema types = schema.types.values() for _type in types: - type_model = hasattr(_type,'_meta') and getattr(_type._meta, 'model', None) + type_model = hasattr(_type, '_meta') and getattr( + _type._meta, 'model', None) if model == type_model: return _type class DjangoConnectionField(relay.ConnectionField): + def wrap_resolved(self, value, instance, args, info): if isinstance(value, (QuerySet, Manager)): cls = instance.__class__ @@ -30,6 +32,7 @@ class DjangoConnectionField(relay.ConnectionField): class ConnectionOrListField(LazyField): + @memoize def get_field(self, schema): model_field = self.field_type @@ -43,6 +46,7 @@ class ConnectionOrListField(LazyField): class DjangoModelField(Field): + def __init__(self, model, *args, **kwargs): super(DjangoModelField, self).__init__(None, *args, **kwargs) self.model = model @@ -55,7 +59,9 @@ class DjangoModelField(Field): def get_object_type(self, schema): _type = get_type_for_model(schema, self.model) if not _type and self.object_type._meta.only_fields: - # We will only raise the exception if the related field is specified in only_fields - raise Exception("Field %s (%s) model not mapped in current schema" % (self, self.model._meta.object_name)) - + # We will only raise the exception if the related field is + # specified in only_fields + raise Exception("Field %s (%s) model not mapped in current schema" % ( + self, self.model._meta.object_name)) + return _type diff --git a/graphene/contrib/django/options.py b/graphene/contrib/django/options.py index 50f0710a..69415cc0 100644 --- a/graphene/contrib/django/options.py +++ b/graphene/contrib/django/options.py @@ -14,6 +14,7 @@ def is_base(cls): class DjangoOptions(Options): + def __init__(self, *args, **kwargs): self.model = None super(DjangoOptions, self).__init__(*args, **kwargs) @@ -25,6 +26,7 @@ class DjangoOptions(Options): if not is_node(cls) and not is_base(cls): return if not self.model: - raise Exception('Django ObjectType %s must have a model in the Meta class attr' % cls) + raise Exception( + 'Django ObjectType %s must have a model in the Meta class attr' % cls) elif not inspect.isclass(self.model) or not issubclass(self.model, models.Model): raise Exception('Provided model in %s is not a Django model' % cls) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index 24f7612c..6e23bd6d 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -42,4 +42,4 @@ class DjangoInterface(six.with_metaclass(DjangoObjectTypeMeta, BaseObjectType)): class DjangoNode(BaseNode, DjangoInterface): - pass \ No newline at end of file + pass diff --git a/graphene/core/fields.py b/graphene/core/fields.py index f11c951a..40edce9f 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -13,7 +13,9 @@ from graphql.core.type import ( from graphene.utils import cached_property, memoize from graphene.core.types import BaseObjectType + class Field(object): + def __init__(self, field_type, resolve=None, null=True, args=None, description='', **extra_args): self.field_type = field_type self.resolve_fn = resolve @@ -41,7 +43,8 @@ class Field(object): if self.resolve_fn: resolve_fn = self.resolve_fn else: - resolve_fn = lambda root, args, info: root.resolve(self.field_name, args, info) + resolve_fn = lambda root, args, info: root.resolve( + self.field_name, args, info) return resolve_fn(instance, args, info) def get_object_type(self, schema): @@ -78,7 +81,8 @@ class Field(object): @memoize def internal_field(self, schema): if not self.object_type: - raise Exception('Field could not be constructed in a non graphene.Type or graphene.Interface') + raise Exception( + 'Field could not be constructed in a non graphene.Type or graphene.Interface') extra_args = self.extra_args.copy() for arg_name, arg_value in extra_args.items(): @@ -118,6 +122,7 @@ class Field(object): class NativeField(Field): + def __init__(self, field=None): super(NativeField, self).__init__(None) self.field = field @@ -135,6 +140,7 @@ class NativeField(Field): class LazyField(Field): + @memoize def inner_field(self, schema): return self.get_field(schema) @@ -147,11 +153,13 @@ class LazyField(Field): class LazyNativeField(NativeField): + def __init__(self, *args, **kwargs): super(LazyNativeField, self).__init__(None, *args, **kwargs) def get_field(self, schema): - raise NotImplementedError("get_field function not implemented for %s LazyField" % self.__class__) + raise NotImplementedError( + "get_field function not implemented for %s LazyField" % self.__class__) @memoize def internal_field(self, schema): @@ -163,6 +171,7 @@ class LazyNativeField(NativeField): class TypeField(Field): + def __init__(self, *args, **kwargs): super(TypeField, self).__init__(self.field_type, *args, **kwargs) @@ -188,10 +197,12 @@ class FloatField(TypeField): class ListField(Field): + def type_wrapper(self, field_type): return GraphQLList(field_type) class NonNullField(Field): + def type_wrapper(self, field_type): return GraphQLNonNull(field_type) diff --git a/graphene/core/options.py b/graphene/core/options.py index aab8efc6..e0abe144 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -5,6 +5,7 @@ DEFAULT_NAMES = ('description', 'name', 'interface', class Options(object): + def __init__(self, meta=None): self.meta = meta self.local_fields = [] @@ -13,7 +14,7 @@ class Options(object): self.interfaces = [] self.parents = [] self.valid_attrs = DEFAULT_NAMES - + def contribute_to_class(self, cls, name): cls._meta = self self.parent = cls @@ -47,7 +48,8 @@ class Options(object): # Any leftover attributes must be invalid. if meta_attrs != {}: - raise TypeError("'class Meta' got invalid attribute(s): %s" % ','.join(meta_attrs.keys())) + raise TypeError( + "'class Meta' got invalid attribute(s): %s" % ','.join(meta_attrs.keys())) else: self.proxy = False diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 47180b12..da26e7d0 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -49,7 +49,7 @@ class Schema(object): @property def types(self): return self._internal_types - + def execute(self, request='', root=None, vars=None, operation_name=None): root = root or object() return graphql( @@ -63,6 +63,7 @@ class Schema(object): def introspect(self): return self._schema.get_type_map() + def register_internal_type(fun): @wraps(fun) def wrapper(cls, schema): diff --git a/graphene/core/types.py b/graphene/core/types.py index 1b695cc1..48ba3c5a 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -11,6 +11,7 @@ from graphene.core.options import Options from graphene.utils import memoize from graphene.core.schema import register_internal_type + class ObjectTypeMeta(type): options_cls = Options @@ -40,7 +41,7 @@ class ObjectTypeMeta(type): base_meta = getattr(new_class, '_meta', None) new_class.add_to_class('_meta', new_class.options_cls(meta)) - + new_class._meta.interface = new_class.is_interface(parents) # Add all attributes to the class. for obj_name, obj in attrs.items(): @@ -68,7 +69,8 @@ class ObjectTypeMeta(type): raise Exception( 'Local field %r in class %r clashes ' 'with field of similar name from ' - 'base class %r' % (field.field_name, name, base.__name__) + 'base class %r' % ( + field.field_name, name, base.__name__) ) new_class._meta.parents.append(base) if base._meta.interface: @@ -93,6 +95,7 @@ class ObjectTypeMeta(type): class BaseObjectType(object): + def __new__(cls, instance=None, *args, **kwargs): if cls._meta.interface: raise Exception("An interface cannot be initialized") @@ -143,7 +146,8 @@ class BaseObjectType(object): return GraphQLInterfaceType( cls._meta.type_name, description=cls._meta.description, - resolve_type=lambda *args, **kwargs: cls.resolve_type(schema, *args, **kwargs), + resolve_type=lambda * + args, **kwargs: cls.resolve_type(schema, *args, **kwargs), fields=fields ) return GraphQLObjectType( @@ -157,5 +161,6 @@ class BaseObjectType(object): class ObjectType(six.with_metaclass(ObjectTypeMeta, BaseObjectType)): pass + class Interface(six.with_metaclass(ObjectTypeMeta, BaseObjectType)): pass diff --git a/graphene/decorators.py b/graphene/decorators.py index a3e6b335..bf71aef1 100644 --- a/graphene/decorators.py +++ b/graphene/decorators.py @@ -4,5 +4,5 @@ from functools import wraps def resolve_only_args(func): @wraps(func) def inner(self, args, info): - return func(self, **args) - return inner \ No newline at end of file + return func(self, **args) + return inner diff --git a/graphene/env.py b/graphene/env.py index 604dbc2c..a1d43ec5 100644 --- a/graphene/env.py +++ b/graphene/env.py @@ -2,8 +2,9 @@ from graphene.core.schema import Schema _global_schema = None + def get_global_schema(): - global _global_schema - if not _global_schema: - _global_schema = Schema(name='Global Schema') - return _global_schema + global _global_schema + if not _global_schema: + _global_schema = Schema(name='Global Schema') + return _global_schema diff --git a/graphene/relay/__init__.py b/graphene/relay/__init__.py index dc636f67..e33fcd3d 100644 --- a/graphene/relay/__init__.py +++ b/graphene/relay/__init__.py @@ -1,12 +1,12 @@ from graphene.relay.fields import ( - ConnectionField, - NodeField + ConnectionField, + NodeField ) import graphene.relay.connections from graphene.relay.types import ( - Node + Node ) from graphene.relay.utils import is_node diff --git a/graphene/relay/connections.py b/graphene/relay/connections.py index af3968c4..4a074634 100644 --- a/graphene/relay/connections.py +++ b/graphene/relay/connections.py @@ -13,4 +13,5 @@ def object_type_created(object_type): type_name = object_type._meta.type_name field = NodeIDField() object_type.add_to_class('id', field) - assert hasattr(object_type, 'get_node'), 'get_node classmethod not found in %s Node' % type_name + assert hasattr( + object_type, 'get_node'), 'get_node classmethod not found in %s Node' % type_name diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index b078af78..7cc840f3 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -16,8 +16,9 @@ from graphene.utils import memoize class ConnectionField(Field): + def __init__(self, field_type, resolve=None, description=''): - super(ConnectionField, self).__init__(field_type, resolve=resolve, + super(ConnectionField, self).__init__(field_type, resolve=resolve, args=connectionArgs, description=description) def wrap_resolved(self, value, instance, args, info): @@ -27,23 +28,27 @@ class ConnectionField(Field): resolved = super(ConnectionField, self).resolve(instance, args, info) if resolved: resolved = self.wrap_resolved(resolved, instance, args, info) - assert isinstance(resolved, collections.Iterable), 'Resolved value from the connection field have to be iterable' + assert isinstance( + resolved, collections.Iterable), 'Resolved value from the connection field have to be iterable' return connectionFromArray(resolved, args) @memoize def internal_type(self, schema): from graphene.relay.types import BaseNode object_type = self.get_object_type(schema) - assert issubclass(object_type, BaseNode), 'Only nodes have connections.' + assert issubclass( + object_type, BaseNode), 'Only nodes have connections.' return object_type.get_connection(schema) class NodeField(LazyNativeField): + def get_field(self, schema): from graphene.relay.types import BaseNode return BaseNode.get_definitions(schema).nodeField class NodeIDField(LazyNativeField): + def get_field(self, schema): return globalIdField(self.object_type._meta.type_name) diff --git a/graphene/relay/types.py b/graphene/relay/types.py index a9eae9c0..4898c78d 100644 --- a/graphene/relay/types.py +++ b/graphene/relay/types.py @@ -23,6 +23,7 @@ def get_node(schema, globalId, *args): class BaseNode(object): + @classmethod @memoize def get_definitions(cls, schema): diff --git a/graphene/relay/utils.py b/graphene/relay/utils.py index 1c3a1bc3..3fd127b3 100644 --- a/graphene/relay/utils.py +++ b/graphene/relay/utils.py @@ -1,7 +1,9 @@ from graphene.relay.types import BaseNode + def is_node(object_type): - return issubclass(object_type, BaseNode) and not is_node_type(object_type) + return issubclass(object_type, BaseNode) and not is_node_type(object_type) + def is_node_type(object_type): - return BaseNode in object_type.__bases__ + return BaseNode in object_type.__bases__ diff --git a/graphene/utils.py b/graphene/utils.py index c944df9e..020c6d3e 100644 --- a/graphene/utils.py +++ b/graphene/utils.py @@ -1,6 +1,8 @@ from functools import wraps + class cached_property(object): + """ A property that is only computed once per instance and then replaces itself with an ordinary attribute. Deleting the attribute resets the property. diff --git a/setup.py b/setup.py index 04d1d37e..441a1069 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ class PyTest(TestCommand): self.test_suite = True def run_tests(self): - #import here, cause outside the eggs aren't loaded + # import here, cause outside the eggs aren't loaded import pytest errno = pytest.main(self.pytest_args) sys.exit(errno) diff --git a/tests/contrib_django/data.py b/tests/contrib_django/data.py index 107b146c..a9fa13f5 100644 --- a/tests/contrib_django/data.py +++ b/tests/contrib_django/data.py @@ -8,10 +8,12 @@ r.save() r2 = Reporter(first_name='Paul', last_name='Jones', email='paul@example.com') r2.save() -a = Article(id=None, headline="This is a test", pub_date=date(2005, 7, 27), reporter=r) +a = Article(id=None, headline="This is a test", + pub_date=date(2005, 7, 27), reporter=r) a.save() -new_article = r.articles.create(headline="John's second story", pub_date=date(2005, 7, 29)) +new_article = r.articles.create( + headline="John's second story", pub_date=date(2005, 7, 29)) new_article2 = Article(headline="Paul's story", pub_date=date(2006, 1, 17)) r.articles.add(new_article2) diff --git a/tests/contrib_django/models.py b/tests/contrib_django/models.py index dac0258a..7ded5f4f 100644 --- a/tests/contrib_django/models.py +++ b/tests/contrib_django/models.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from django.db import models + class Reporter(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) @@ -12,6 +13,7 @@ class Reporter(models.Model): class Meta: app_label = 'contrib_django' + class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateField() diff --git a/tests/contrib_django/test_schema.py b/tests/contrib_django/test_schema.py index 6605f994..a8f4d2fd 100644 --- a/tests/contrib_django/test_schema.py +++ b/tests/contrib_django/test_schema.py @@ -20,6 +20,7 @@ def test_should_raise_if_no_model(): def test_should_raise_if_model_is_invalid(): with raises(Exception) as excinfo: class Character2(DjangoObjectType): + class Meta: model = 1 assert 'not a Django model' in str(excinfo.value) @@ -28,6 +29,7 @@ def test_should_raise_if_model_is_invalid(): def test_should_raise_if_model_is_invalid(): with raises(Exception) as excinfo: class ReporterTypeError(DjangoObjectType): + class Meta: model = Reporter only_fields = ('articles', ) @@ -41,18 +43,22 @@ def test_should_raise_if_model_is_invalid(): result = schema.execute(query) assert not result.errors - assert 'articles (Article) model not mapped in current schema' in str(excinfo.value) - + assert 'articles (Article) model not mapped in current schema' in str( + excinfo.value) def test_should_map_fields_correctly(): class ReporterType2(DjangoObjectType): + class Meta: model = Reporter - assert ReporterType2._meta.fields_map.keys() == ['articles', 'first_name', 'last_name', 'id', 'email'] + assert ReporterType2._meta.fields_map.keys( + ) == ['articles', 'first_name', 'last_name', 'id', 'email'] + def test_should_map_fields(): class ReporterType(DjangoObjectType): + class Meta: model = Reporter @@ -86,13 +92,16 @@ def test_should_map_fields(): def test_should_map_only_few_fields(): class Reporter2(DjangoObjectType): + class Meta: model = Reporter only_fields = ('id', 'email') assert Reporter2._meta.fields_map.keys() == ['id', 'email'] + def test_should_node(): class ReporterNodeType(DjangoNode): + class Meta: model = Reporter @@ -104,6 +113,7 @@ def test_should_node(): return [ArticleNodeType(Article(headline='Hi!'))] class ArticleNodeType(DjangoNode): + class Meta: model = Article @@ -152,11 +162,11 @@ def test_should_node(): 'last_name': 'X', 'email': '', 'articles': { - 'edges': [{ - 'node': { - 'headline': 'Hi!' - } - }] + 'edges': [{ + 'node': { + 'headline': 'Hi!' + } + }] }, }, 'my_article': { diff --git a/tests/contrib_django/test_types.py b/tests/contrib_django/test_types.py index 4aaa3e0b..a0ac08c7 100644 --- a/tests/contrib_django/test_types.py +++ b/tests/contrib_django/test_types.py @@ -20,13 +20,16 @@ from .models import Reporter, Article class Character(DjangoInterface): + '''Character description''' class Meta: model = Reporter class Human(DjangoNode): + '''Human description''' + def get_node(self, id): pass @@ -39,12 +42,14 @@ schema = Schema() def test_django_interface(): assert DjangoNode._meta.interface == True + def test_pseudo_interface(): object_type = Character.internal_type(schema) assert Character._meta.interface == True assert isinstance(object_type, GraphQLInterfaceType) assert Character._meta.model == Reporter - assert object_type.get_fields().keys() == ['articles', 'first_name', 'last_name', 'id', 'email'] + assert object_type.get_fields().keys() == [ + 'articles', 'first_name', 'last_name', 'id', 'email'] def test_interface_resolve_type(): diff --git a/tests/core/test_fields.py b/tests/core/test_fields.py index eb11af45..31b08bc0 100644 --- a/tests/core/test_fields.py +++ b/tests/core/test_fields.py @@ -17,10 +17,13 @@ from graphql.core.type import ( GraphQLID, ) + class ObjectType(object): _meta = Options() + def resolve(self, *args, **kwargs): return None + def can_resolve(self, *args): return True @@ -28,11 +31,13 @@ ot = ObjectType() ObjectType._meta.contribute_to_class(ObjectType, '_meta') + class Schema(object): pass schema = Schema() + def test_field_no_contributed_raises_error(): f = Field(GraphQLString) with raises(Exception) as excinfo: @@ -60,10 +65,10 @@ def test_stringfield_type_null(): def test_field_resolve(): - f = StringField(null=False, resolve=lambda *args:'RESOLVED') + f = StringField(null=False, resolve=lambda *args: 'RESOLVED') f.contribute_to_class(ot, 'field_name') field_type = f.internal_field(schema) - assert 'RESOLVED' == field_type.resolver(ot,2,3) + assert 'RESOLVED' == field_type.resolver(ot, 2, 3) def test_field_resolve_type_custom(): @@ -71,6 +76,7 @@ def test_field_resolve_type_custom(): pass class Schema(object): + def get_type(self, name): if name == 'MyCustomType': return MyCustomType diff --git a/tests/core/test_options.py b/tests/core/test_options.py index cb27eba1..a4dd045b 100644 --- a/tests/core/test_options.py +++ b/tests/core/test_options.py @@ -8,13 +8,16 @@ from graphene.core.fields import ( from graphene.core.options import Options + class Meta: interface = True type_name = 'Character' + class InvalidMeta: other_value = True + def test_field_added_in_meta(): opt = Options(Meta) @@ -27,6 +30,7 @@ def test_field_added_in_meta(): opt.add_field(f) assert f in opt.fields + def test_options_contribute(): opt = Options(Meta) @@ -36,6 +40,7 @@ def test_options_contribute(): opt.contribute_to_class(ObjectType, '_meta') assert ObjectType._meta == opt + def test_options_typename(): opt = Options(Meta) @@ -45,16 +50,19 @@ def test_options_typename(): opt.contribute_to_class(ObjectType, '_meta') assert opt.type_name == 'Character' + def test_options_description(): opt = Options(Meta) class ObjectType(object): + '''False description''' pass opt.contribute_to_class(ObjectType, '_meta') assert opt.description == 'False description' + def test_field_no_contributed_raises_error(): opt = Options(InvalidMeta) diff --git a/tests/core/test_query.py b/tests/core/test_query.py index 1ab947d1..66269e01 100644 --- a/tests/core/test_query.py +++ b/tests/core/test_query.py @@ -24,7 +24,7 @@ class Character(Interface): class Pet(ObjectType): - type = StringField(resolve=lambda *_:'Dog') + type = StringField(resolve=lambda *_: 'Dog') class Human(Character): @@ -33,7 +33,7 @@ class Human(Character): def resolve_name(self, *args): return 'Peter' - + def resolve_friend(self, *args): return Human(object()) @@ -60,7 +60,7 @@ def test_query(): expected = { 'name': 'Peter', 'pet': { - 'type':'Dog' + 'type': 'Dog' } } result = graphql(schema, query, root=Human(object())) diff --git a/tests/core/test_schema.py b/tests/core/test_schema.py index 3a11c90f..fa08f944 100644 --- a/tests/core/test_schema.py +++ b/tests/core/test_schema.py @@ -28,7 +28,7 @@ class Character(Interface): class Pet(ObjectType): - type = StringField(resolve=lambda *_:'Dog') + type = StringField(resolve=lambda *_: 'Dog') class Human(Character): @@ -37,7 +37,7 @@ class Human(Character): def resolve_name(self, *args): return 'Peter' - + def resolve_friend(self, *args): return Human(object()) @@ -46,17 +46,21 @@ class Human(Character): schema.query = Human + def test_get_registered_type(): assert schema.get_type('Character') == Character + def test_get_unregistered_type(): with raises(Exception) as excinfo: schema.get_type('NON_EXISTENT_MODEL') assert 'not found' in str(excinfo.value) + def test_schema_query(): assert schema.query == Human + def test_query_schema_graphql(): a = object() query = ''' @@ -70,7 +74,7 @@ def test_query_schema_graphql(): expected = { 'name': 'Peter', 'pet': { - 'type':'Dog' + 'type': 'Dog' } } result = graphql(schema.schema, query, root=Human(object())) @@ -91,7 +95,7 @@ def test_query_schema_execute(): expected = { 'name': 'Peter', 'pet': { - 'type':'Dog' + 'type': 'Dog' } } result = schema.execute(query, root=object()) @@ -100,4 +104,5 @@ def test_query_schema_execute(): def test_schema_get_type_map(): - assert schema.schema.get_type_map().keys() == ['__Field', 'String', 'Pet', 'Character', '__InputValue', '__Directive', '__TypeKind', '__Schema', '__Type', 'Human', '__EnumValue', 'Boolean'] + assert schema.schema.get_type_map().keys() == [ + '__Field', 'String', 'Pet', 'Character', '__InputValue', '__Directive', '__TypeKind', '__Schema', '__Type', 'Human', '__EnumValue', 'Boolean'] diff --git a/tests/core/test_types.py b/tests/core/test_types.py index c79d0c74..b4bc9cf7 100644 --- a/tests/core/test_types.py +++ b/tests/core/test_types.py @@ -17,13 +17,16 @@ from graphene.core.types import ( class Character(Interface): + '''Character description''' name = StringField() + class Meta: type_name = 'core.Character' class Human(Character): + '''Human description''' friends = StringField() @@ -39,7 +42,8 @@ def test_interface(): assert isinstance(object_type, GraphQLInterfaceType) assert Character._meta.type_name == 'core.Character' assert object_type.description == 'Character description' - assert object_type.get_fields() == {'name': Character._meta.fields_map['name'].internal_field(schema)} + assert object_type.get_fields() == { + 'name': Character._meta.fields_map['name'].internal_field(schema)} def test_interface_resolve_type(): @@ -53,5 +57,6 @@ def test_object_type(): assert Human._meta.type_name == 'core.Human' assert isinstance(object_type, GraphQLObjectType) assert object_type.description == 'Human description' - assert object_type.get_fields() == {'name': Character._meta.fields_map['name'].internal_field(schema), 'friends': Human._meta.fields_map['friends'].internal_field(schema)} + assert object_type.get_fields() == {'name': Character._meta.fields_map['name'].internal_field( + schema), 'friends': Human._meta.fields_map['friends'].internal_field(schema)} assert object_type.get_interfaces() == [Character.internal_type(schema)] diff --git a/tests/django_settings.py b/tests/django_settings.py index faa4c15d..f9c41629 100644 --- a/tests/django_settings.py +++ b/tests/django_settings.py @@ -6,7 +6,7 @@ INSTALLED_APPS = [ 'tests.contrib_django', ] -DATABASES={ +DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'tests/django.sqlite', diff --git a/tests/relay/test_relay.py b/tests/relay/test_relay.py index 5b16bc49..2f5c61c9 100644 --- a/tests/relay/test_relay.py +++ b/tests/relay/test_relay.py @@ -5,6 +5,7 @@ from graphene import relay schema = graphene.Schema() + class OtherNode(relay.Node): name = graphene.StringField() diff --git a/tests/starwars/data.py b/tests/starwars/data.py index 8b30cac6..51f29a59 100644 --- a/tests/starwars/data.py +++ b/tests/starwars/data.py @@ -5,40 +5,40 @@ Human = namedtuple('Human', 'id name friends appearsIn homePlanet') luke = Human( id='1000', name='Luke Skywalker', - friends=[ '1002', '1003', '2000', '2001' ], - appearsIn=[ 4, 5, 6 ], + friends=['1002', '1003', '2000', '2001'], + appearsIn=[4, 5, 6], homePlanet='Tatooine', ) vader = Human( id='1001', name='Darth Vader', - friends=[ '1004' ], - appearsIn=[ 4, 5, 6 ], + friends=['1004'], + appearsIn=[4, 5, 6], homePlanet='Tatooine', ) han = Human( id='1002', name='Han Solo', - friends=[ '1000', '1003', '2001' ], - appearsIn=[ 4, 5, 6 ], + friends=['1000', '1003', '2001'], + appearsIn=[4, 5, 6], homePlanet=None, ) leia = Human( id='1003', name='Leia Organa', - friends=[ '1000', '1002', '2000', '2001' ], - appearsIn=[ 4, 5, 6 ], + friends=['1000', '1002', '2000', '2001'], + appearsIn=[4, 5, 6], homePlanet='Alderaan', ) tarkin = Human( id='1004', name='Wilhuff Tarkin', - friends=[ '1001' ], - appearsIn=[ 4 ], + friends=['1001'], + appearsIn=[4], homePlanet=None, ) @@ -55,16 +55,16 @@ Droid = namedtuple('Droid', 'id name friends appearsIn primaryFunction') threepio = Droid( id='2000', name='C-3PO', - friends=[ '1000', '1002', '1003', '2001' ], - appearsIn=[ 4, 5, 6 ], + friends=['1000', '1002', '1003', '2001'], + appearsIn=[4, 5, 6], primaryFunction='Protocol', ) artoo = Droid( id='2001', name='R2-D2', - friends=[ '1000', '1002', '1003' ], - appearsIn=[ 4, 5, 6 ], + friends=['1000', '1002', '1003'], + appearsIn=[4, 5, 6], primaryFunction='Astromech', ) @@ -73,6 +73,7 @@ droidData = { '2001': artoo, } + def getCharacter(id): return humanData.get(id) or droidData.get(id) diff --git a/tests/starwars/schema.py b/tests/starwars/schema.py index d3527a83..71bbb92c 100644 --- a/tests/starwars/schema.py +++ b/tests/starwars/schema.py @@ -4,14 +4,14 @@ 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 + 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) @@ -37,14 +37,14 @@ class Droid(Character): 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) + ) class Meta: type_name = 'core.Query' diff --git a/tests/starwars/test_query.py b/tests/starwars/test_query.py index 500178e4..214030ef 100644 --- a/tests/starwars/test_query.py +++ b/tests/starwars/test_query.py @@ -69,7 +69,7 @@ def test_nested_query(): 'friends': [ { 'name': 'Luke Skywalker', - 'appearsIn': [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + 'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'], 'friends': [ { 'name': 'Han Solo', @@ -87,7 +87,7 @@ def test_nested_query(): }, { 'name': 'Han Solo', - 'appearsIn': [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + 'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'], 'friends': [ { 'name': 'Luke Skywalker', @@ -102,7 +102,7 @@ def test_nested_query(): }, { 'name': 'Leia Organa', - 'appearsIn': [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + 'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'], 'friends': [ { 'name': 'Luke Skywalker', @@ -264,11 +264,11 @@ def test_duplicate_fields(): 'luke': { 'name': 'Luke Skywalker', 'homePlanet': 'Tatooine', - }, + }, 'leia': { 'name': 'Leia Organa', 'homePlanet': 'Alderaan', - } + } } result = Schema.execute(query) assert not result.errors @@ -294,11 +294,11 @@ def test_use_fragment(): 'luke': { 'name': 'Luke Skywalker', 'homePlanet': 'Tatooine', - }, + }, 'leia': { 'name': 'Leia Organa', 'homePlanet': 'Alderaan', - } + } } result = Schema.execute(query) assert not result.errors diff --git a/tests/starwars_django/data.py b/tests/starwars_django/data.py index 690627b1..cb43c29b 100644 --- a/tests/starwars_django/data.py +++ b/tests/starwars_django/data.py @@ -2,6 +2,7 @@ from collections import namedtuple from .models import Ship, Faction + def initialize(): rebels = Faction( id='1', @@ -15,7 +16,6 @@ def initialize(): ) empire.save() - xwing = Ship( id='1', name='X-Wing', @@ -88,14 +88,18 @@ def createShip(shipName, factionId): def getShip(_id): return Ship.objects.get(id=_id) + def getShips(): return Ship.objects.all() + def getFaction(_id): return Faction.objects.get(id=_id) + def getRebels(): return getFaction(1) + def getEmpire(): return getFaction(2) diff --git a/tests/starwars_django/schema.py b/tests/starwars_django/schema.py index c0ae9652..0bde6826 100644 --- a/tests/starwars_django/schema.py +++ b/tests/starwars_django/schema.py @@ -15,17 +15,21 @@ from .data import ( schema = graphene.Schema(name='Starwars Django Relay Schema') + class Ship(DjangoNode): + class Meta: - model = ShipModel + model = ShipModel @classmethod def get_node(cls, id): return Ship(getShip(id)) + class Faction(DjangoNode): + class Meta: - model = FactionModel + model = FactionModel @classmethod def get_node(cls, id): diff --git a/tests/starwars_django/test_connections.py b/tests/starwars_django/test_connections.py index 42fd81b6..e7181dc9 100644 --- a/tests/starwars_django/test_connections.py +++ b/tests/starwars_django/test_connections.py @@ -7,6 +7,7 @@ from .data import initialize pytestmark = pytest.mark.django_db + def test_correct_fetch_first_ship_rebels(): initialize() query = ''' @@ -24,18 +25,18 @@ def test_correct_fetch_first_ship_rebels(): } ''' expected = { - 'rebels': { - 'name': 'Alliance to Restore the Republic', - 'ships': { - 'edges': [ - { - 'node': { - 'name': 'X-Wing' - } + 'rebels': { + 'name': 'Alliance to Restore the Republic', + 'ships': { + 'edges': [ + { + 'node': { + 'name': 'X-Wing' + } + } + ] } - ] } - } } result = schema.execute(query) assert not result.errors diff --git a/tests/starwars_django/test_objectidentification.py b/tests/starwars_django/test_objectidentification.py index 93bf1b71..84fb5413 100644 --- a/tests/starwars_django/test_objectidentification.py +++ b/tests/starwars_django/test_objectidentification.py @@ -7,6 +7,7 @@ from .schema import schema pytestmark = pytest.mark.django_db + def test_correctly_fetches_id_name_rebels(): initialize() query = ''' @@ -18,15 +19,16 @@ def test_correctly_fetches_id_name_rebels(): } ''' expected = { - 'rebels': { - 'id': 'RmFjdGlvbjox', - 'name': 'Alliance to Restore the Republic' - } + 'rebels': { + 'id': 'RmFjdGlvbjox', + 'name': 'Alliance to Restore the Republic' + } } result = schema.execute(query) assert not result.errors assert result.data == expected + def test_correctly_refetches_rebels(): initialize() query = ''' @@ -40,15 +42,16 @@ def test_correctly_refetches_rebels(): } ''' expected = { - 'node': { - 'id': 'RmFjdGlvbjox', - 'name': 'Alliance to Restore the Republic' - } + 'node': { + 'id': 'RmFjdGlvbjox', + 'name': 'Alliance to Restore the Republic' + } } result = schema.execute(query) assert not result.errors assert result.data == expected + def test_correctly_fetches_id_name_empire(): initialize() query = ''' @@ -60,15 +63,16 @@ def test_correctly_fetches_id_name_empire(): } ''' expected = { - 'empire': { - 'id': 'RmFjdGlvbjoy', - 'name': 'Galactic Empire' - } + 'empire': { + 'id': 'RmFjdGlvbjoy', + 'name': 'Galactic Empire' + } } result = schema.execute(query) assert not result.errors assert result.data == expected + def test_correctly_refetches_empire(): initialize() query = ''' @@ -82,15 +86,16 @@ def test_correctly_refetches_empire(): } ''' expected = { - 'node': { - 'id': 'RmFjdGlvbjoy', - 'name': 'Galactic Empire' - } + 'node': { + 'id': 'RmFjdGlvbjoy', + 'name': 'Galactic Empire' + } } result = schema.execute(query) assert not result.errors assert result.data == expected + def test_correctly_refetches_xwing(): initialize() query = ''' @@ -104,10 +109,10 @@ def test_correctly_refetches_xwing(): } ''' expected = { - 'node': { - 'id': 'U2hpcDox', - 'name': 'X-Wing' - } + 'node': { + 'id': 'U2hpcDox', + 'name': 'X-Wing' + } } result = schema.execute(query) assert not result.errors diff --git a/tests/starwars_relay/data.py b/tests/starwars_relay/data.py index 31ac591d..a706dd53 100644 --- a/tests/starwars_relay/data.py +++ b/tests/starwars_relay/data.py @@ -1,7 +1,7 @@ from collections import namedtuple -Ship = namedtuple('Ship',['id', 'name']) -Faction = namedtuple('Faction',['id', 'name', 'ships']) +Ship = namedtuple('Ship', ['id', 'name']) +Faction = namedtuple('Faction', ['id', 'name', 'ships']) xwing = Ship( id='1', @@ -54,7 +54,7 @@ rebels = Faction( empire = Faction( id='2', name='Galactic Empire', - ships= ['6', '7', '8'] + ships=['6', '7', '8'] ) data = { @@ -74,6 +74,7 @@ data = { } } + def createShip(shipName, factionId): nextShip = len(data['Ship'].keys())+1 newShip = Ship( @@ -88,11 +89,14 @@ def createShip(shipName, factionId): 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 8c9bc494..83aa673b 100644 --- a/tests/starwars_relay/schema.py +++ b/tests/starwars_relay/schema.py @@ -10,7 +10,9 @@ from .data import ( schema = graphene.Schema(name='Starwars Relay Schema') + class Ship(relay.Node): + '''A ship in the Star Wars saga''' name = graphene.StringField(description='The name of the ship.') @@ -20,9 +22,11 @@ class Ship(relay.Node): 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.') + ships = relay.ConnectionField( + Ship, description='The ships used by the faction.') @resolve_only_args def resolve_ships(self, **kwargs): diff --git a/tests/starwars_relay/schema_other.py b/tests/starwars_relay/schema_other.py index dd33bba7..5cf4f0a9 100644 --- a/tests/starwars_relay/schema_other.py +++ b/tests/starwars_relay/schema_other.py @@ -11,6 +11,7 @@ Episode = graphene.Enum('Episode', dict( JEDI=6 )) + def wrap_character(character): if isinstance(character, _Human): return Human(character) diff --git a/tests/starwars_relay/test_connections.py b/tests/starwars_relay/test_connections.py index 5f115e49..303f5230 100644 --- a/tests/starwars_relay/test_connections.py +++ b/tests/starwars_relay/test_connections.py @@ -3,6 +3,7 @@ from graphql.core import graphql from .schema import schema + def test_correct_fetch_first_ship_rebels(): query = ''' query RebelsShipsQuery { @@ -19,18 +20,18 @@ def test_correct_fetch_first_ship_rebels(): } ''' expected = { - 'rebels': { - 'name': 'Alliance to Restore the Republic', - 'ships': { - 'edges': [ - { - 'node': { - 'name': 'X-Wing' - } + 'rebels': { + 'name': 'Alliance to Restore the Republic', + 'ships': { + 'edges': [ + { + 'node': { + 'name': 'X-Wing' + } + } + ] } - ] } - } } result = schema.execute(query) assert not result.errors diff --git a/tests/starwars_relay/test_objectidentification.py b/tests/starwars_relay/test_objectidentification.py index ced7daa7..eabaa785 100644 --- a/tests/starwars_relay/test_objectidentification.py +++ b/tests/starwars_relay/test_objectidentification.py @@ -3,6 +3,7 @@ from graphql.core import graphql from .schema import schema + def test_correctly_fetches_id_name_rebels(): query = ''' query RebelsQuery { @@ -13,15 +14,16 @@ def test_correctly_fetches_id_name_rebels(): } ''' expected = { - 'rebels': { - 'id': 'RmFjdGlvbjox', - 'name': 'Alliance to Restore the Republic' - } + 'rebels': { + 'id': 'RmFjdGlvbjox', + 'name': 'Alliance to Restore the Republic' + } } result = schema.execute(query) assert not result.errors assert result.data == expected + def test_correctly_refetches_rebels(): query = ''' query RebelsRefetchQuery { @@ -34,15 +36,16 @@ def test_correctly_refetches_rebels(): } ''' expected = { - 'node': { - 'id': 'RmFjdGlvbjox', - 'name': 'Alliance to Restore the Republic' - } + 'node': { + 'id': 'RmFjdGlvbjox', + 'name': 'Alliance to Restore the Republic' + } } result = schema.execute(query) assert not result.errors assert result.data == expected + def test_correctly_fetches_id_name_empire(): query = ''' query EmpireQuery { @@ -53,15 +56,16 @@ def test_correctly_fetches_id_name_empire(): } ''' expected = { - 'empire': { - 'id': 'RmFjdGlvbjoy', - 'name': 'Galactic Empire' - } + 'empire': { + 'id': 'RmFjdGlvbjoy', + 'name': 'Galactic Empire' + } } result = schema.execute(query) assert not result.errors assert result.data == expected + def test_correctly_refetches_empire(): query = ''' query EmpireRefetchQuery { @@ -74,15 +78,16 @@ def test_correctly_refetches_empire(): } ''' expected = { - 'node': { - 'id': 'RmFjdGlvbjoy', - 'name': 'Galactic Empire' - } + 'node': { + 'id': 'RmFjdGlvbjoy', + 'name': 'Galactic Empire' + } } result = schema.execute(query) assert not result.errors assert result.data == expected + def test_correctly_refetches_xwing(): query = ''' query XWingRefetchQuery { @@ -95,10 +100,10 @@ def test_correctly_refetches_xwing(): } ''' expected = { - 'node': { - 'id': 'U2hpcDox', - 'name': 'X-Wing' - } + 'node': { + 'id': 'U2hpcDox', + 'name': 'X-Wing' + } } result = schema.execute(query) assert not result.errors From 701c49db26fbb5f12585bda2d9c1520855e5dd11 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 2 Oct 2015 22:45:13 -0700 Subject: [PATCH 37/77] Added automatic snake casing to camel casing conversion in field names --- graphene/contrib/django/fields.py | 2 +- graphene/core/fields.py | 11 +++++++---- graphene/core/options.py | 4 ++++ graphene/core/types.py | 10 +++++----- graphene/utils.py | 9 +++++++++ tests/contrib_django/test_schema.py | 22 +++++++++++----------- tests/contrib_django/test_types.py | 17 +++++++++-------- 7 files changed, 46 insertions(+), 29 deletions(-) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 5bed67f5..b47aaf0d 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -41,7 +41,7 @@ class ConnectionOrListField(LazyField): field = DjangoConnectionField(model_field) else: field = ListField(model_field) - field.contribute_to_class(self.object_type, self.field_name) + field.contribute_to_class(self.object_type, self.name) return field diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 40edce9f..4e95b6d2 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -10,23 +10,26 @@ from graphql.core.type import ( GraphQLArgument, GraphQLFloat, ) -from graphene.utils import cached_property, memoize +from graphene.utils import memoize, to_camel_case from graphene.core.types import BaseObjectType class Field(object): - def __init__(self, field_type, resolve=None, null=True, args=None, description='', **extra_args): + def __init__(self, field_type, name=None, resolve=None, null=True, args=None, description='', **extra_args): self.field_type = field_type self.resolve_fn = resolve self.null = null self.args = args or {} self.extra_args = extra_args self._type = None + self.name = name self.description = description or self.__doc__ self.object_type = None def contribute_to_class(self, cls, name): + if not self.name: + self.name = to_camel_case(name) self.field_name = name self.object_type = cls if isinstance(self.field_type, Field) and not self.field_type.object_type: @@ -94,7 +97,7 @@ class Field(object): raise TypeError("Field %s.%s initiated with invalid args: %s" % ( self.object_type, self.field_name, - ','.join(meta_attrs.keys()) + ','.join(extra_args.keys()) )) internal_type = self.internal_type(schema) @@ -107,7 +110,7 @@ class Field(object): ) def __str__(self): - """ Return "object_type.field_name". """ + """ Return "object_type.name". """ return '%s.%s' % (self.object_type, self.field_name) def __repr__(self): diff --git a/graphene/core/options.py b/graphene/core/options.py index e0abe144..6ff03b3d 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -71,3 +71,7 @@ class Options(object): @cached_property def fields_map(self): return {f.field_name: f for f in self.fields} + + @cached_property + def internal_fields_map(self): + return {f.name: f for f in self.fields} diff --git a/graphene/core/types.py b/graphene/core/types.py index 48ba3c5a..193a65a4 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -49,7 +49,7 @@ class ObjectTypeMeta(type): new_class.add_extra_fields() new_fields = new_class._meta.local_fields - field_names = {f.field_name for f in new_fields} + field_names = {f.name for f in new_fields} for base in parents: original_base = base @@ -65,12 +65,12 @@ class ObjectTypeMeta(type): # on the base classes (we cannot handle shadowed fields at the # moment). for field in parent_fields: - if field.field_name in field_names: + if field.name in field_names: raise Exception( 'Local field %r in class %r clashes ' 'with field of similar name from ' 'base class %r' % ( - field.field_name, name, base.__name__) + field.name, name, base.__name__) ) new_class._meta.parents.append(base) if base._meta.interface: @@ -99,7 +99,7 @@ class BaseObjectType(object): def __new__(cls, instance=None, *args, **kwargs): if cls._meta.interface: raise Exception("An interface cannot be initialized") - if instance == None: + if instance is None: return None return super(BaseObjectType, cls).__new__(cls, instance, *args, **kwargs) @@ -137,7 +137,7 @@ class BaseObjectType(object): @memoize @register_internal_type def internal_type(cls, schema): - fields_map = cls._meta.fields_map + fields_map = cls._meta.internal_fields_map fields = lambda: { name: field.internal_field(schema) for name, field in fields_map.items() diff --git a/graphene/utils.py b/graphene/utils.py index 020c6d3e..364e3ba1 100644 --- a/graphene/utils.py +++ b/graphene/utils.py @@ -32,3 +32,12 @@ def memoize(fun): return ret cache = {} return wrapper + + +# From this response in Stackoverflow +# http://stackoverflow.com/a/19053800/1072990 +def to_camel_case(snake_str): + components = snake_str.split('_') + # We capitalize the first letter of each component except the first one + # with the 'title' method and join them together. + return components[0] + "".join(x.title() for x in components[1:]) diff --git a/tests/contrib_django/test_schema.py b/tests/contrib_django/test_schema.py index a8f4d2fd..6fc51074 100644 --- a/tests/contrib_django/test_schema.py +++ b/tests/contrib_django/test_schema.py @@ -71,16 +71,16 @@ def test_should_map_fields(): query = ''' query ReporterQuery { reporter { - first_name, - last_name, + firstName, + lastName, email } } ''' expected = { 'reporter': { - 'first_name': 'ABA', - 'last_name': 'X', + 'firstName': 'ABA', + 'lastName': 'X', 'email': '' } } @@ -133,7 +133,7 @@ def test_should_node(): query ReporterQuery { reporter { id, - first_name, + firstName, articles { edges { node { @@ -141,13 +141,13 @@ def test_should_node(): } } } - last_name, + lastName, email } - my_article: node(id:"QXJ0aWNsZU5vZGVUeXBlOjE=") { + myArticle: node(id:"QXJ0aWNsZU5vZGVUeXBlOjE=") { id ... on ReporterNodeType { - first_name + firstName } ... on ArticleNodeType { headline @@ -158,8 +158,8 @@ def test_should_node(): expected = { 'reporter': { 'id': 'UmVwb3J0ZXJOb2RlVHlwZTox', - 'first_name': 'ABA', - 'last_name': 'X', + 'firstName': 'ABA', + 'lastName': 'X', 'email': '', 'articles': { 'edges': [{ @@ -169,7 +169,7 @@ def test_should_node(): }] }, }, - 'my_article': { + 'myArticle': { 'id': 'QXJ0aWNsZU5vZGVUeXBlOjE=', 'headline': 'Article node' } diff --git a/tests/contrib_django/test_types.py b/tests/contrib_django/test_types.py index a0ac08c7..1ab8bd47 100644 --- a/tests/contrib_django/test_types.py +++ b/tests/contrib_django/test_types.py @@ -40,16 +40,16 @@ schema = Schema() def test_django_interface(): - assert DjangoNode._meta.interface == True + assert DjangoNode._meta.interface is True def test_pseudo_interface(): object_type = Character.internal_type(schema) - assert Character._meta.interface == True + assert Character._meta.interface is True assert isinstance(object_type, GraphQLInterfaceType) assert Character._meta.model == Reporter assert object_type.get_fields().keys() == [ - 'articles', 'first_name', 'last_name', 'id', 'email'] + 'lastName', 'email', 'id', 'firstName', 'articles'] def test_interface_resolve_type(): @@ -59,12 +59,13 @@ def test_interface_resolve_type(): def test_object_type(): object_type = Human.internal_type(schema) - assert Human._meta.interface == False + internal_fields_map = Human._meta.internal_fields_map + assert Human._meta.interface is False assert isinstance(object_type, GraphQLObjectType) assert object_type.get_fields() == { - 'headline': Human._meta.fields_map['headline'].internal_field(schema), - 'id': Human._meta.fields_map['id'].internal_field(schema), - 'reporter': Human._meta.fields_map['reporter'].internal_field(schema), - 'pub_date': Human._meta.fields_map['pub_date'].internal_field(schema), + 'headline': internal_fields_map['headline'].internal_field(schema), + 'id': internal_fields_map['id'].internal_field(schema), + 'reporter': internal_fields_map['reporter'].internal_field(schema), + 'pubDate': internal_fields_map['pubDate'].internal_field(schema), } assert object_type.get_interfaces() == [DjangoNode.internal_type(schema)] From 1eee2912d81e62d36babe0796718251c0be769de Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 2 Oct 2015 22:56:37 -0700 Subject: [PATCH 38/77] Added two more tests on field naming --- tests/core/test_fields.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/core/test_fields.py b/tests/core/test_fields.py index 31b08bc0..dce05ad2 100644 --- a/tests/core/test_fields.py +++ b/tests/core/test_fields.py @@ -51,6 +51,18 @@ def test_field_type(): assert f.internal_type(schema) == GraphQLString +def test_field_name_automatic_camelcase(): + f = Field(GraphQLString) + f.contribute_to_class(ot, 'field_name') + assert f.name == 'fieldName' + + +def test_field_name_use_name_if_exists(): + f = Field(GraphQLString, name='my_custom_name') + f.contribute_to_class(ot, 'field_name') + assert f.name == 'my_custom_name' + + def test_stringfield_type(): f = StringField() f.contribute_to_class(ot, 'field_name') From 52cb1715d3fafa8f42066ef05e77fff3c482a8c1 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 3 Oct 2015 00:03:38 -0700 Subject: [PATCH 39/77] Added LazyMap for django resolvers --- graphene/contrib/django/fields.py | 26 +++++++++++------- graphene/core/types.py | 7 ++++- graphene/utils.py | 44 +++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 10 deletions(-) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index b47aaf0d..4cccc9f6 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -4,7 +4,7 @@ from graphene.core.fields import ( from graphene import relay from graphene.core.fields import Field, LazyField -from graphene.utils import cached_property, memoize +from graphene.utils import cached_property, memoize, LazyMap from graphene.relay.types import BaseNode @@ -22,17 +22,26 @@ def get_type_for_model(schema, model): return _type -class DjangoConnectionField(relay.ConnectionField): +def lazy_map(value, func): + if isinstance(value, Manager): + value = value.get_queryset() + if isinstance(value, QuerySet): + return LazyMap(value, func) + return value + +class DjangoConnectionField(relay.ConnectionField): def wrap_resolved(self, value, instance, args, info): - if isinstance(value, (QuerySet, Manager)): - cls = instance.__class__ - value = [cls(s) for s in value.all()] - return value + return lazy_map(value, instance.__class__) + + +class LazyListField(ListField): + def resolve(self, value, instance, args, info): + resolved = super(LazyListField, self).resolve(value, instance, args, info) + return lazy_map(resolved, instance.__class__) class ConnectionOrListField(LazyField): - @memoize def get_field(self, schema): model_field = self.field_type @@ -40,13 +49,12 @@ class ConnectionOrListField(LazyField): if field_object_type and issubclass(field_object_type, BaseNode): field = DjangoConnectionField(model_field) else: - field = ListField(model_field) + field = LazyListField(model_field) field.contribute_to_class(self.object_type, self.name) return field class DjangoModelField(Field): - def __init__(self, model, *args, **kwargs): super(DjangoModelField, self).__init__(None, *args, **kwargs) self.model = model diff --git a/graphene/core/types.py b/graphene/core/types.py index 193a65a4..2734803f 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -101,6 +101,8 @@ class BaseObjectType(object): raise Exception("An interface cannot be initialized") if instance is None: return None + elif type(instance) is cls: + instance = instance.instance return super(BaseObjectType, cls).__new__(cls, instance, *args, **kwargs) def __init__(self, instance=None): @@ -115,9 +117,12 @@ class BaseObjectType(object): def get_field(self, field): return getattr(self.instance, field, None) + def __eq__(self, other): + return self.instance.__eq__(other) + 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) + raise Exception('Field %s not found in model %s' % (field_name, self._meta.type_name)) custom_resolve_fn = 'resolve_%s' % field_name if hasattr(self, custom_resolve_fn): resolve_fn = getattr(self, custom_resolve_fn) diff --git a/graphene/utils.py b/graphene/utils.py index 364e3ba1..b7a7864e 100644 --- a/graphene/utils.py +++ b/graphene/utils.py @@ -41,3 +41,47 @@ def to_camel_case(snake_str): # We capitalize the first letter of each component except the first one # with the 'title' method and join them together. return components[0] + "".join(x.title() for x in components[1:]) + + +class LazyMap(object): + def __init__(self, origin, _map, state=None): + self._origin = origin + self._origin_iter = origin.__iter__() + self._state = state or [] + self._finished = False + self._map = _map + + def __iter__(self): + return self if not self._finished else iter(self._state) + + def iter(self): + return self.__iter__() + + def __len__(self): + return self._origin.__len__() + + def __next__(self): + try: + n = next(self._origin_iter) + n = self._map(n) + except StopIteration as e: + self._finished = True + raise e + else: + self._state.append(n) + return n + + def next(self): + return self.__next__() + + def __getitem__(self, key): + item = self._origin.__getitem__(key) + if isinstance(key, slice): + return LazyMap(item, self._map) + return self._map(item) + + def __getattr__(self, name): + return getattr(self._origin, name) + + def __repr__(self): + return "" % repr(self._origin) From dcd8edb59af9eba9ee9c4fda6b59ff8268a17a9e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 3 Oct 2015 01:47:55 -0700 Subject: [PATCH 40/77] Improved Django Connection resolver. Added exclude_fields option to Django Types --- graphene/contrib/django/__init__.py | 4 +++ graphene/contrib/django/fields.py | 4 +-- graphene/contrib/django/options.py | 3 ++- graphene/contrib/django/types.py | 6 ++++- graphene/contrib/django/views.py | 40 +++++++++++++++++++++++++++++ graphene/utils.py | 1 - 6 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 graphene/contrib/django/views.py diff --git a/graphene/contrib/django/__init__.py b/graphene/contrib/django/__init__.py index 96edc3d3..9e4c8aea 100644 --- a/graphene/contrib/django/__init__.py +++ b/graphene/contrib/django/__init__.py @@ -2,3 +2,7 @@ from graphene.contrib.django.types import ( DjangoObjectType, DjangoNode ) +from graphene.contrib.django.fields import ( + DjangoConnectionField, + DjangoModelField +) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 4cccc9f6..292e6333 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -32,13 +32,13 @@ def lazy_map(value, func): class DjangoConnectionField(relay.ConnectionField): def wrap_resolved(self, value, instance, args, info): - return lazy_map(value, instance.__class__) + return lazy_map(value, self.field_type) class LazyListField(ListField): def resolve(self, value, instance, args, info): resolved = super(LazyListField, self).resolve(value, instance, args, info) - return lazy_map(resolved, instance.__class__) + return lazy_map(resolved, self.field_type) class ConnectionOrListField(LazyField): diff --git a/graphene/contrib/django/options.py b/graphene/contrib/django/options.py index 69415cc0..3ee72a67 100644 --- a/graphene/contrib/django/options.py +++ b/graphene/contrib/django/options.py @@ -5,7 +5,7 @@ from graphene.core.options import Options from graphene.core.types import BaseObjectType from graphene.relay.utils import is_node -VALID_ATTRS = ('model', 'only_fields') +VALID_ATTRS = ('model', 'only_fields', 'exclude_fields') def is_base(cls): @@ -20,6 +20,7 @@ class DjangoOptions(Options): super(DjangoOptions, self).__init__(*args, **kwargs) self.valid_attrs += VALID_ATTRS self.only_fields = None + self.exclude_fields = [] def contribute_to_class(self, cls, name): super(DjangoOptions, self).contribute_to_class(cls, name) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index 6e23bd6d..d88a7f14 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -27,7 +27,11 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): only_fields = cls._meta.only_fields reverse_fields = tuple(get_reverse_fields(cls._meta.model)) for field in cls._meta.model._meta.fields + reverse_fields: - if only_fields and field.name not in only_fields: + is_not_in_only = only_fields and field.name not in only_fields + is_excluded = field.name in cls._meta.exclude_fields + if is_not_in_only or is_excluded: + # We skip this field if we specify only_fields and is not + # in there. Or when we excldue this field in exclude_fields continue converted_field = convert_django_field(field, cls) cls.add_to_class(field.name, converted_field) diff --git a/graphene/contrib/django/views.py b/graphene/contrib/django/views.py new file mode 100644 index 00000000..01f50387 --- /dev/null +++ b/graphene/contrib/django/views.py @@ -0,0 +1,40 @@ +import json + +from django.http import JsonResponse +from django.views.generic import View + +from graphql.core.error import GraphQLError, format_error + + +def form_error(error): + if isinstance(error, GraphQLError): + return format_error(error) + return error + + +class GraphQLView(View): + schema = None + @staticmethod + def format_result(result): + data = {'data': result.data} + if result.errors: + data['errors'] = map(form_error, result.errors) + + return data + + def execute_query(self, request, query): + result = self.schema.execute(query, root=object()) + data = self.format_result(result) + return JsonResponse(data) + + def get(self, request, *args, **kwargs): + query = request.GET.get('query') or request.GET.get('q') or '' + return self.execute_query(request, query) + + def post(self, request, *args, **kwargs): + if request.body: + received_json_data = json.loads(request.body) + query = received_json_data.get('query') or '' + else: + query = request.POST.get('query') or request.POST.get('q') + return self.execute_query(request, query) diff --git a/graphene/utils.py b/graphene/utils.py index b7a7864e..f551f57f 100644 --- a/graphene/utils.py +++ b/graphene/utils.py @@ -2,7 +2,6 @@ from functools import wraps class cached_property(object): - """ A property that is only computed once per instance and then replaces itself with an ordinary attribute. Deleting the attribute resets the property. From b0e3b3a3afcebd0273c42cfb38523f0ca7599e52 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 3 Oct 2015 02:10:03 -0700 Subject: [PATCH 41/77] Improved Django GraphQL view error handling --- graphene/contrib/django/views.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/graphene/contrib/django/views.py b/graphene/contrib/django/views.py index 01f50387..a9212e89 100644 --- a/graphene/contrib/django/views.py +++ b/graphene/contrib/django/views.py @@ -23,8 +23,21 @@ class GraphQLView(View): return data def execute_query(self, request, query): - result = self.schema.execute(query, root=object()) - data = self.format_result(result) + if not query: + data = { + "errors": [{ + "message": "Must provide query string." + }] + } + else: + try: + result = self.schema.execute(query, root=object()) + data = self.format_result(result) + except Exception, e: + data = { + "errors": [{"message": str(e)}] + } + return JsonResponse(data) def get(self, request, *args, **kwargs): From 855cee2f346dbc6a530ea166c81bfdd7d03e3e9f Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 3 Oct 2015 02:42:56 -0700 Subject: [PATCH 42/77] Added custom GraphQLSchema --- graphene/contrib/django/fields.py | 11 +++++++---- graphene/core/schema.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 292e6333..4d70b9b6 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -12,6 +12,7 @@ from django.db.models.query import QuerySet from django.db.models.manager import Manager +@memoize def get_type_for_model(schema, model): schema = schema types = schema.types.values() @@ -32,13 +33,15 @@ def lazy_map(value, func): class DjangoConnectionField(relay.ConnectionField): def wrap_resolved(self, value, instance, args, info): - return lazy_map(value, self.field_type) + schema = info.schema.graphene_schema + return lazy_map(value, self.get_object_type(schema)) class LazyListField(ListField): - def resolve(self, value, instance, args, info): - resolved = super(LazyListField, self).resolve(value, instance, args, info) - return lazy_map(resolved, self.field_type) + def resolve(self, instance, args, info): + schema = info.schema.graphene_schema + resolved = super(LazyListField, self).resolve(instance, args, info) + return lazy_map(resolved, self.get_object_type(schema)) class ConnectionOrListField(LazyField): diff --git a/graphene/core/schema.py b/graphene/core/schema.py index da26e7d0..6d979f76 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -2,12 +2,18 @@ from functools import wraps from graphql.core import graphql from graphql.core.type import ( - GraphQLSchema + GraphQLSchema as _GraphQLSchema ) from graphene import signals from graphene.utils import cached_property +class GraphQLSchema(_GraphQLSchema): + def __init__(self, schema, *args, **kwargs): + self.graphene_schema = schema + super(GraphQLSchema, self).__init__(*args, **kwargs) + + class Schema(object): _query = None @@ -34,7 +40,7 @@ class Schema(object): def schema(self): if not self._query_type: raise Exception('You have to define a base query type') - return GraphQLSchema(query=self._query_type, mutation=self.mutation) + return GraphQLSchema(self, query=self._query_type, mutation=self.mutation) def associate_internal_type(self, internal_type, object_type): self._internal_types[internal_type.name] = object_type From a1c7d3e3dea3b22aa76ad01ac42c9a63e317efed Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 3 Oct 2015 03:59:24 -0700 Subject: [PATCH 43/77] Simplified unused code --- graphene/core/fields.py | 8 +------- graphene/core/types.py | 7 ------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 4e95b6d2..c74b2ac4 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -36,12 +36,6 @@ class Field(object): self.field_type.contribute_to_class(cls, name) cls._meta.add_field(self) - def resolver(self, instance, args, info): - if self.object_type.can_resolve(self.field_name, instance, args, info): - return self.resolve(instance, args, info) - else: - return None - def resolve(self, instance, args, info): if self.resolve_fn: resolve_fn = self.resolve_fn @@ -106,7 +100,7 @@ class Field(object): internal_type, description=self.description, args=self.args, - resolver=self.resolver, + resolver=self.resolve, ) def __str__(self): diff --git a/graphene/core/types.py b/graphene/core/types.py index 2734803f..59c7221c 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -121,19 +121,12 @@ class BaseObjectType(object): return self.instance.__eq__(other) 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 %s' % (field_name, self._meta.type_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) return self.get_field(field_name) - @classmethod - def can_resolve(cls, field_name, instance, args, info): - # Useful for manage permissions in fields - return True - @classmethod def resolve_type(cls, schema, instance, *_): return instance.internal_type(schema) From 47bd3d392677e1be915acab6ff2b45b5dbf9ab65 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 3 Oct 2015 12:23:39 -0700 Subject: [PATCH 44/77] Removed global env from grapheme --- graphene/__init__.py | 4 ---- graphene/env.py | 10 ---------- 2 files changed, 14 deletions(-) delete mode 100644 graphene/env.py diff --git a/graphene/__init__.py b/graphene/__init__.py index 4c16032c..cfcd6177 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -12,10 +12,6 @@ from graphene.core.schema import ( Schema ) -from graphene.env import ( - get_global_schema -) - from graphene.core.types import ( ObjectType, Interface diff --git a/graphene/env.py b/graphene/env.py deleted file mode 100644 index a1d43ec5..00000000 --- a/graphene/env.py +++ /dev/null @@ -1,10 +0,0 @@ -from graphene.core.schema import Schema - -_global_schema = None - - -def get_global_schema(): - global _global_schema - if not _global_schema: - _global_schema = Schema(name='Global Schema') - return _global_schema From 5b415a1de66025a8cb6a7a74796dafb5529f6264 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 3 Oct 2015 12:24:51 -0700 Subject: [PATCH 45/77] Improved Django view exception handling --- graphene/contrib/django/views.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/graphene/contrib/django/views.py b/graphene/contrib/django/views.py index a9212e89..a2f813e1 100644 --- a/graphene/contrib/django/views.py +++ b/graphene/contrib/django/views.py @@ -2,6 +2,7 @@ import json from django.http import JsonResponse from django.views.generic import View +from django.conf import settings from graphql.core.error import GraphQLError, format_error @@ -14,6 +15,7 @@ def form_error(error): class GraphQLView(View): schema = None + @staticmethod def format_result(result): data = {'data': result.data} @@ -34,6 +36,8 @@ class GraphQLView(View): result = self.schema.execute(query, root=object()) data = self.format_result(result) except Exception, e: + if settings.DEBUG: + raise e data = { "errors": [{"message": str(e)}] } @@ -41,13 +45,14 @@ class GraphQLView(View): return JsonResponse(data) def get(self, request, *args, **kwargs): - query = request.GET.get('query') or request.GET.get('q') or '' - return self.execute_query(request, query) + query = request.GET.get('query') + return self.execute_query(request, query or '') def post(self, request, *args, **kwargs): if request.body: received_json_data = json.loads(request.body) - query = received_json_data.get('query') or '' + query = received_json_data.get('query') else: - query = request.POST.get('query') or request.POST.get('q') - return self.execute_query(request, query) + query = request.POST.get('query') or request.GET.get('query') + raise Exception(query) + return self.execute_query(request, query or '') From d47f1d544eba4dbf681cdac6e51e91858df6b291 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 3 Oct 2015 13:27:19 -0700 Subject: [PATCH 46/77] Improved django GraphQL view testing --- graphene/contrib/django/views.py | 33 +++++++---- tests/contrib_django/test_urls.py | 34 +++++++++++ tests/contrib_django/test_views.py | 94 ++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 tests/contrib_django/test_urls.py create mode 100644 tests/contrib_django/test_views.py diff --git a/graphene/contrib/django/views.py b/graphene/contrib/django/views.py index a2f813e1..b4482c62 100644 --- a/graphene/contrib/django/views.py +++ b/graphene/contrib/django/views.py @@ -24,13 +24,16 @@ class GraphQLView(View): return data + def response_errors(self, *errors): + return JsonResponse({ + "errors": [{ + "message": str(e) + } for e in errors] + }) + def execute_query(self, request, query): if not query: - data = { - "errors": [{ - "message": "Must provide query string." - }] - } + return self.response_errors(Exception("Must provide query string.")) else: try: result = self.schema.execute(query, root=object()) @@ -38,9 +41,7 @@ class GraphQLView(View): except Exception, e: if settings.DEBUG: raise e - data = { - "errors": [{"message": str(e)}] - } + return self.response_errors(e) return JsonResponse(data) @@ -48,11 +49,19 @@ class GraphQLView(View): query = request.GET.get('query') return self.execute_query(request, query or '') + @staticmethod + def get_content_type(request): + meta = request.META + return meta.get('CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE', '')) + def post(self, request, *args, **kwargs): - if request.body: - received_json_data = json.loads(request.body) - query = received_json_data.get('query') + content_type = self.get_content_type(request) + if content_type == 'application/json': + try: + received_json_data = json.loads(request.body) + query = received_json_data.get('query') + except ValueError, e: + return self.response_errors(ValueError("Malformed json body in the post data")) else: query = request.POST.get('query') or request.GET.get('query') - raise Exception(query) return self.execute_query(request, query or '') diff --git a/tests/contrib_django/test_urls.py b/tests/contrib_django/test_urls.py new file mode 100644 index 00000000..6d0bca57 --- /dev/null +++ b/tests/contrib_django/test_urls.py @@ -0,0 +1,34 @@ +from django.conf.urls import url + +from graphene.contrib.django.views import GraphQLView + +from graphene import Schema +from graphene.contrib.django.types import ( + DjangoNode, + DjangoInterface +) + +from .models import Reporter, Article + + +class Character(DjangoNode): + class Meta: + model = Reporter + + def get_node(self, id): + pass + + +class Human(DjangoNode): + class Meta: + model = Article + + def get_node(self, id): + pass + +schema = Schema(query=Human) + + +urlpatterns = [ + url(r'^graphql', GraphQLView.as_view(schema=schema)), +] diff --git a/tests/contrib_django/test_views.py b/tests/contrib_django/test_views.py new file mode 100644 index 00000000..bfd1efc3 --- /dev/null +++ b/tests/contrib_django/test_views.py @@ -0,0 +1,94 @@ +import json +import pytest +from py.test import raises +from collections import namedtuple +from pytest import raises +from graphene.core.fields import ( + Field, + StringField, +) +from graphql.core.type import ( + GraphQLObjectType, + GraphQLInterfaceType +) + +from graphene import Schema +from graphene.contrib.django.types import ( + DjangoNode, + DjangoInterface +) + + +def format_response(response): + return json.loads(response.content) + + +def test_client_get_no_query(settings, client): + settings.ROOT_URLCONF = 'tests.contrib_django.test_urls' + response = client.get('/graphql') + json_response = format_response(response) + assert json_response == {'errors': [{'message': 'Must provide query string.'}]} + + +def test_client_post_no_query(settings, client): + settings.ROOT_URLCONF = 'tests.contrib_django.test_urls' + response = client.post('/graphql', {}) + print response.content + json_response = format_response(response) + assert json_response == {'errors': [{'message': 'Must provide query string.'}]} + + +def test_client_post_malformed_json(settings, client): + settings.ROOT_URLCONF = 'tests.contrib_django.test_urls' + response = client.post('/graphql', 'MALFORMED', 'application/json') + json_response = format_response(response) + assert json_response == {'errors': [{'message': 'Malformed json body in the post data'}]} + + +def test_client_post_empty_query(settings, client): + settings.ROOT_URLCONF = 'tests.contrib_django.test_urls' + response = client.post('/graphql', json.dumps({'query': ''}), 'application/json') + json_response = format_response(response) + assert json_response == {'errors': [{'message': 'Must provide query string.'}]} + + +def test_client_post_bad_query(settings, client): + settings.ROOT_URLCONF = 'tests.contrib_django.test_urls' + response = client.post('/graphql', json.dumps({'query': '{ MALFORMED'}), 'application/json') + json_response = format_response(response) + assert 'errors' in json_response + assert len(json_response['errors']) == 1 + assert 'Syntax Error GraphQL' in json_response['errors'][0]['message'] + + +def test_client_get_good_query(settings, client): + settings.ROOT_URLCONF = 'tests.contrib_django.test_urls' + response = client.get('/graphql', {'query': '{ headline }'}) + json_response = format_response(response) + expected_json = { + 'data': { + 'headline': None + } + } + assert json_response == expected_json + + +def test_client_post_good_query(settings, client): + settings.ROOT_URLCONF = 'tests.contrib_django.test_urls' + response = client.post('/graphql', json.dumps({'query': '{ headline }'}), 'application/json') + json_response = format_response(response) + expected_json = { + 'data': { + 'headline': None + } + } + assert json_response == expected_json + + +# def test_client_get_bad_query(settings, client): +# settings.ROOT_URLCONF = 'tests.contrib_django.test_urls' +# response = client.get('/graphql') +# json_response = format_response(response) +# assert json_response == {'errors': [{'message': 'Must provide query string.'}]} + + From 2f3bfdf863ad6c9efbd32b0a961b1d59a4e3c57a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 3 Oct 2015 13:51:45 -0700 Subject: [PATCH 47/77] Improved Django fields testing --- graphene/__init__.py | 1 + graphene/contrib/django/converter.py | 20 ++--- graphene/contrib/django/types.py | 2 +- tests/contrib_django/test_converter.py | 105 +++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 tests/contrib_django/test_converter.py diff --git a/graphene/__init__.py b/graphene/__init__.py index cfcd6177..91d019d2 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -25,6 +25,7 @@ from graphene.core.fields import ( IDField, ListField, NonNullField, + FloatField, ) from graphene.decorators import ( diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index 2595a68d..098ab4c2 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -13,7 +13,7 @@ from graphene.contrib.django.fields import ConnectionOrListField, DjangoModelFie @singledispatch -def convert_django_field(field, cls): +def convert_django_field(field): raise Exception( "Don't know how to convert the Django field %s (%s)" % (field, field.__class__)) @@ -25,12 +25,12 @@ def convert_django_field(field, cls): @convert_django_field.register(models.SlugField) @convert_django_field.register(models.URLField) @convert_django_field.register(models.UUIDField) -def _(field, cls): +def _(field): return StringField(description=field.description) @convert_django_field.register(models.AutoField) -def _(field, cls): +def _(field): return IDField(description=field.description) @@ -39,33 +39,33 @@ def _(field, cls): @convert_django_field.register(models.SmallIntegerField) @convert_django_field.register(models.BigIntegerField) @convert_django_field.register(models.IntegerField) -def _(field, cls): +def _(field): return IntField(description=field.description) @convert_django_field.register(models.BooleanField) -def _(field, cls): +def _(field): return BooleanField(description=field.description, null=False) @convert_django_field.register(models.NullBooleanField) -def _(field, cls): - return BooleanField(description=field.description) +def _(field): + return BooleanField(description=field.description, null=True) @convert_django_field.register(models.FloatField) -def _(field, cls): +def _(field): return FloatField(description=field.description) @convert_django_field.register(models.ManyToManyField) @convert_django_field.register(models.ManyToOneRel) -def _(field, cls): +def _(field): model_field = DjangoModelField(field.related_model) return ConnectionOrListField(model_field) @convert_django_field.register(models.OneToOneField) @convert_django_field.register(models.ForeignKey) -def _(field, cls): +def _(field): return DjangoModelField(field.related_model, description=field.description) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index d88a7f14..0c5026dc 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -33,7 +33,7 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): # We skip this field if we specify only_fields and is not # in there. Or when we excldue this field in exclude_fields continue - converted_field = convert_django_field(field, cls) + converted_field = convert_django_field(field) cls.add_to_class(field.name, converted_field) diff --git a/tests/contrib_django/test_converter.py b/tests/contrib_django/test_converter.py new file mode 100644 index 00000000..37402608 --- /dev/null +++ b/tests/contrib_django/test_converter.py @@ -0,0 +1,105 @@ +from py.test import raises +from collections import namedtuple +from pytest import raises +import graphene +from graphene import relay +from graphene.contrib.django.converter import ( + convert_django_field +) +from graphene.contrib.django.fields import ( + ConnectionOrListField, + DjangoModelField +) +from django.db import models +from .models import Article, Reporter + + +def assert_conversion(django_field, graphene_field, *args): + field = django_field(*args) + graphene_type = convert_django_field(field) + assert isinstance(graphene_type, graphene_field) + return graphene_type + + +def test_should_unknown_django_field_raise_exception(): + with raises(Exception) as excinfo: + convert_django_field(None) + assert 'Don\'t know how to convert the Django field' in str(excinfo.value) + + +def test_should_date_convert_string(): + assert_conversion(models.DateField, graphene.StringField) + + +def test_should_char_convert_string(): + assert_conversion(models.CharField, graphene.StringField) + + +def test_should_text_convert_string(): + assert_conversion(models.TextField, graphene.StringField) + + +def test_should_email_convert_string(): + assert_conversion(models.EmailField, graphene.StringField) + + +def test_should_slug_convert_string(): + assert_conversion(models.SlugField, graphene.StringField) + + +def test_should_url_convert_string(): + assert_conversion(models.URLField, graphene.StringField) + + +def test_should_auto_convert_id(): + assert_conversion(models.AutoField, graphene.IDField) + + +def test_should_positive_integer_convert_int(): + assert_conversion(models.PositiveIntegerField, graphene.IntField) + + +def test_should_positive_small_convert_int(): + assert_conversion(models.PositiveSmallIntegerField, graphene.IntField) + + +def test_should_small_integer_convert_int(): + assert_conversion(models.SmallIntegerField, graphene.IntField) + + +def test_should_big_integer_convert_int(): + assert_conversion(models.BigIntegerField, graphene.IntField) + + +def test_should_integer_convert_int(): + assert_conversion(models.IntegerField, graphene.IntField) + + +def test_should_boolean_convert_boolean(): + assert_conversion(models.BooleanField, graphene.BooleanField) + + +def test_should_nullboolean_convert_boolean(): + field = assert_conversion(models.NullBooleanField, graphene.BooleanField) + assert field.null == True + + +def test_should_float_convert_float(): + assert_conversion(models.FloatField, graphene.FloatField) + + +def test_should_manytomany_convert_connectionorlist(): + field = assert_conversion(models.ManyToManyField, ConnectionOrListField, Article) + + +def test_should_manytoone_convert_connectionorlist(): + graphene_type = convert_django_field(Reporter.articles.related) + assert isinstance(graphene_type, ConnectionOrListField) + + +def test_should_onetoone_convert_model(): + assert_conversion(models.OneToOneField, DjangoModelField, Article) + + +def test_should_onetoone_convert_model(): + assert_conversion(models.ForeignKey, DjangoModelField, Article) From 2271d3f1c2b1b30510db45f761562ded7f67cdea Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 3 Oct 2015 13:55:20 -0700 Subject: [PATCH 48/77] Improved django field converter coverage --- tests/contrib_django/test_converter.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/contrib_django/test_converter.py b/tests/contrib_django/test_converter.py index 37402608..7e587fa9 100644 --- a/tests/contrib_django/test_converter.py +++ b/tests/contrib_django/test_converter.py @@ -81,7 +81,7 @@ def test_should_boolean_convert_boolean(): def test_should_nullboolean_convert_boolean(): field = assert_conversion(models.NullBooleanField, graphene.BooleanField) - assert field.null == True + assert field.null is True def test_should_float_convert_float(): @@ -90,16 +90,22 @@ def test_should_float_convert_float(): def test_should_manytomany_convert_connectionorlist(): field = assert_conversion(models.ManyToManyField, ConnectionOrListField, Article) + assert isinstance(field.field_type, DjangoModelField) + assert field.field_type.model == Article def test_should_manytoone_convert_connectionorlist(): graphene_type = convert_django_field(Reporter.articles.related) assert isinstance(graphene_type, ConnectionOrListField) + assert isinstance(graphene_type.field_type, DjangoModelField) + assert graphene_type.field_type.model == Article def test_should_onetoone_convert_model(): - assert_conversion(models.OneToOneField, DjangoModelField, Article) + field = assert_conversion(models.OneToOneField, DjangoModelField, Article) + assert field.model == Article -def test_should_onetoone_convert_model(): - assert_conversion(models.ForeignKey, DjangoModelField, Article) +def test_should_foreignkey_convert_model(): + field = assert_conversion(models.ForeignKey, DjangoModelField, Article) + assert field.model == Article From 3f9b94b73f620dc20b1d2343358091d3fbcf5c07 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 3 Oct 2015 14:27:54 -0700 Subject: [PATCH 49/77] Improved README text --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7af681f5..e518c422 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ -# Graphene: Python DSL for GraphQL +# Graphene: Pythonic GraphQL [![Build Status](https://travis-ci.org/syrusakbary/graphene.svg?branch=master)](https://travis-ci.org/syrusakbary/graphene) [![Coverage Status](https://coveralls.io/repos/syrusakbary/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/syrusakbary/graphene?branch=master) -This is a library to use GraphQL in Python in a easy way. -It will map the models/fields to internal GraphQL-py objects without effort. Including automatic [Django models](#djangorelay-schema) conversion. +This is a library to use GraphQL in a Pythonic and easy way. +It maps the models/fields to internal GraphQLlib objects without effort. Including automatic [Django models](#djangorelay-schema) conversion. -[![Build Status](https://travis-ci.org/syrusakbary/graphene.svg?branch=master)](https://travis-ci.org/syrusakbary/graphene) -[![Coverage Status](https://coveralls.io/repos/syrusakbary/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/syrusakbary/graphene?branch=master) *Note: This library requires installing [graphqllib](https://github.com/dittos/graphqllib) and [graphql-relay](https://github.com/syrusakbary/graphql-relay-py) external libraries.* @@ -83,6 +81,7 @@ class Ship(DjangoNode): class Meta: model = YourDjangoModelHere # only_fields = ('id', 'name') # Only map this fields from the model + # excluxe_fields ('field_to_excluxe', ) # Exclude mapping this fields from the model class Query(graphene.ObjectType): node = relay.NodeField() From 25eca8776a58425e2bec11952c0cc64a1f41d2c2 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 3 Oct 2015 16:48:18 -0700 Subject: [PATCH 50/77] Added ability to skip fields in runtime --- graphene/contrib/django/fields.py | 22 +++++++++++++--------- graphene/contrib/django/types.py | 5 ++++- graphene/core/fields.py | 7 +++++-- graphene/core/scalars.py | 10 ++++++++++ graphene/core/schema.py | 4 ++++ graphene/core/types.py | 4 ++-- graphene/relay/types.py | 2 ++ tests/contrib_django/test_schema.py | 3 --- 8 files changed, 40 insertions(+), 17 deletions(-) create mode 100644 graphene/core/scalars.py diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 4d70b9b6..f92096cd 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -65,14 +65,18 @@ class DjangoModelField(Field): @memoize def internal_type(self, schema): _type = self.get_object_type(schema) - return _type and _type.internal_type(schema) + if not _type and self.object_type._meta.only_fields: + raise Exception( + "Model %r is not accessible by the schema. " + "You can either register the type manually " + "using @schema.register. " + "Or disable the field %s in %s" % ( + self.model, + self.field_name, + self.object_type + ) + ) + return _type and _type.internal_type(schema) or Field.SKIP def get_object_type(self, schema): - _type = get_type_for_model(schema, self.model) - if not _type and self.object_type._meta.only_fields: - # We will only raise the exception if the related field is - # specified in only_fields - raise Exception("Field %s (%s) model not mapped in current schema" % ( - self, self.model._meta.object_name)) - - return _type + return get_type_for_model(schema, self.model) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index 0c5026dc..8e6a3f57 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -46,4 +46,7 @@ class DjangoInterface(six.with_metaclass(DjangoObjectTypeMeta, BaseObjectType)): class DjangoNode(BaseNode, DjangoInterface): - pass + @classmethod + def get_node(cls, id): + instance = cls._meta.model.objects.filter(id=id).first() + return cls(instance) diff --git a/graphene/core/fields.py b/graphene/core/fields.py index c74b2ac4..49f52338 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -12,9 +12,11 @@ from graphql.core.type import ( ) from graphene.utils import memoize, to_camel_case from graphene.core.types import BaseObjectType +from graphene.core.scalars import GraphQLSkipField class Field(object): + SKIP = GraphQLSkipField def __init__(self, field_type, name=None, resolve=None, null=True, args=None, description='', **extra_args): self.field_type = field_type @@ -95,7 +97,8 @@ class Field(object): )) internal_type = self.internal_type(schema) - + if not internal_type: + raise Exception("Internal type for field %s is None" % self) return GraphQLField( internal_type, description=self.description, @@ -111,7 +114,7 @@ class Field(object): """ Displays the module, class and name of the field. """ - path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__) + path = '%r.%s' % (self.object_type, self.__class__.__name__) name = getattr(self, 'field_name', None) if name is not None: return '<%s: %s>' % (path, name) diff --git a/graphene/core/scalars.py b/graphene/core/scalars.py new file mode 100644 index 00000000..2b6a9494 --- /dev/null +++ b/graphene/core/scalars.py @@ -0,0 +1,10 @@ +from graphql.core.type.definition import GraphQLScalarType + + +def skip(value): + return None + +GraphQLSkipField = GraphQLScalarType(name='SkipField', + serialize=skip, + parse_value=skip, + parse_literal=skip) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 6d979f76..7b3cd640 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -45,6 +45,10 @@ class Schema(object): def associate_internal_type(self, internal_type, object_type): self._internal_types[internal_type.name] = object_type + def register(self, object_type): + self._internal_types[object_type._meta.type_name] = object_type + return object_type + def get_type(self, type_name): # print 'get_type' # _type = self.schema.get_type(type_name) diff --git a/graphene/core/types.py b/graphene/core/types.py index 59c7221c..a646f37b 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -49,7 +49,7 @@ class ObjectTypeMeta(type): new_class.add_extra_fields() new_fields = new_class._meta.local_fields - field_names = {f.name for f in new_fields} + field_names = {f.name:f for f in new_fields} for base in parents: original_base = base @@ -65,7 +65,7 @@ class ObjectTypeMeta(type): # on the base classes (we cannot handle shadowed fields at the # moment). for field in parent_fields: - if field.name in field_names: + if field.name in field_names and field.__class__ != field_names[field].__class__: raise Exception( 'Local field %r in class %r clashes ' 'with field of similar name from ' diff --git a/graphene/relay/types.py b/graphene/relay/types.py index 4898c78d..a971447f 100644 --- a/graphene/relay/types.py +++ b/graphene/relay/types.py @@ -19,6 +19,8 @@ def get_node(schema, globalId, *args): resolvedGlobalId = fromGlobalId(globalId) _type, _id = resolvedGlobalId.type, resolvedGlobalId.id object_type = schema.get_type(_type) + if not object_type or not issubclass(object_type, BaseNode): + raise Exception("The type %s is not a Node" % _type) return object_type.get_node(_id) diff --git a/tests/contrib_django/test_schema.py b/tests/contrib_django/test_schema.py index 6fc51074..6469913b 100644 --- a/tests/contrib_django/test_schema.py +++ b/tests/contrib_django/test_schema.py @@ -43,9 +43,6 @@ def test_should_raise_if_model_is_invalid(): result = schema.execute(query) assert not result.errors - assert 'articles (Article) model not mapped in current schema' in str( - excinfo.value) - def test_should_map_fields_correctly(): class ReporterType2(DjangoObjectType): From dd799483b04b5fd02016b1c2bbb9e9fd495ef1a1 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 3 Oct 2015 17:34:55 -0700 Subject: [PATCH 51/77] Added NodeTypeField --- graphene/core/fields.py | 2 -- graphene/relay/fields.py | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 49f52338..5a8a6e20 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -197,12 +197,10 @@ class FloatField(TypeField): class ListField(Field): - def type_wrapper(self, field_type): return GraphQLList(field_type) class NonNullField(Field): - def type_wrapper(self, field_type): return GraphQLNonNull(field_type) diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index 7cc840f3..c9eb87e0 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -7,10 +7,11 @@ from graphql_relay.connection.connection import ( connectionArgs ) from graphql_relay.node.node import ( - globalIdField + globalIdField, + fromGlobalId ) -from graphene.core.fields import Field, LazyNativeField +from graphene.core.fields import Field, LazyNativeField, LazyField from graphene.utils import cached_property from graphene.utils import memoize @@ -42,13 +43,41 @@ class ConnectionField(Field): class NodeField(LazyNativeField): + def __init__(self, object_type=None, *args, **kwargs): + super(NodeField, self).__init__(*args, **kwargs) + self.field_object_type = object_type def get_field(self, schema): + if self.field_object_type: + field = NodeTypeField(self.field_object_type) + field.contribute_to_class(self.object_type, self.field_name) + return field.internal_field(schema) from graphene.relay.types import BaseNode return BaseNode.get_definitions(schema).nodeField -class NodeIDField(LazyNativeField): +class NodeTypeField(LazyField): + def __init__(self, object_type, *args, **kwargs): + super(NodeTypeField, self).__init__(None, *args, **kwargs) + self.field_object_type = object_type + def inner_field(self, schema): + from graphene.relay.types import BaseNode + node_field = BaseNode.get_definitions(schema).nodeField + + def resolver(instance, args, info): + global_id = args.get('id') + resolved_global_id = fromGlobalId(global_id) + if resolved_global_id.type == self.field_object_type._meta.type_name: + return node_field.resolver(instance, args, info) + + args = {a.name: a for a in node_field.args} + field = Field(self.field_object_type, id=args['id'], resolve=resolver) + field.contribute_to_class(self.object_type, self.field_name) + + return field + + +class NodeIDField(LazyNativeField): def get_field(self, schema): return globalIdField(self.object_type._meta.type_name) From cf0906678724da7bf383c1eb12af8b5d00c598d2 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 3 Oct 2015 18:21:47 -0700 Subject: [PATCH 52/77] Fixed ManyToMany relations mapping in Django Models --- graphene/contrib/django/types.py | 6 +++++- tests/contrib_django/models.py | 5 +++++ tests/contrib_django/test_converter.py | 7 ++++--- tests/contrib_django/test_schema.py | 2 +- tests/contrib_django/test_types.py | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index 8e6a3f57..56f0237b 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -26,7 +26,11 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): return only_fields = cls._meta.only_fields reverse_fields = tuple(get_reverse_fields(cls._meta.model)) - for field in cls._meta.model._meta.fields + reverse_fields: + all_fields = (list(cls._meta.model._meta.local_fields) + + list(reverse_fields) + + list(cls._meta.model._meta.local_many_to_many)) + + for field in all_fields: is_not_in_only = only_fields and field.name not in only_fields is_excluded = field.name in cls._meta.exclude_fields if is_not_in_only or is_excluded: diff --git a/tests/contrib_django/models.py b/tests/contrib_django/models.py index 7ded5f4f..72ed0821 100644 --- a/tests/contrib_django/models.py +++ b/tests/contrib_django/models.py @@ -2,10 +2,15 @@ from __future__ import absolute_import from django.db import models +class Pet(models.Model): + name = models.CharField(max_length=30) + + class Reporter(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) email = models.EmailField() + pets = models.ManyToManyField('self') def __str__(self): # __unicode__ on Python 2 return "%s %s" % (self.first_name, self.last_name) diff --git a/tests/contrib_django/test_converter.py b/tests/contrib_django/test_converter.py index 7e587fa9..9d1439c0 100644 --- a/tests/contrib_django/test_converter.py +++ b/tests/contrib_django/test_converter.py @@ -89,9 +89,10 @@ def test_should_float_convert_float(): def test_should_manytomany_convert_connectionorlist(): - field = assert_conversion(models.ManyToManyField, ConnectionOrListField, Article) - assert isinstance(field.field_type, DjangoModelField) - assert field.field_type.model == Article + graphene_type = convert_django_field(Reporter._meta.local_many_to_many[0]) + assert isinstance(graphene_type, ConnectionOrListField) + assert isinstance(graphene_type.field_type, DjangoModelField) + assert graphene_type.field_type.model == Reporter def test_should_manytoone_convert_connectionorlist(): diff --git a/tests/contrib_django/test_schema.py b/tests/contrib_django/test_schema.py index 6469913b..95003004 100644 --- a/tests/contrib_django/test_schema.py +++ b/tests/contrib_django/test_schema.py @@ -50,7 +50,7 @@ def test_should_map_fields_correctly(): class Meta: model = Reporter assert ReporterType2._meta.fields_map.keys( - ) == ['articles', 'first_name', 'last_name', 'id', 'email'] + ) == ['articles', 'first_name', 'last_name', 'email', 'pets', 'id'] def test_should_map_fields(): diff --git a/tests/contrib_django/test_types.py b/tests/contrib_django/test_types.py index 1ab8bd47..28435a13 100644 --- a/tests/contrib_django/test_types.py +++ b/tests/contrib_django/test_types.py @@ -49,7 +49,7 @@ def test_pseudo_interface(): assert isinstance(object_type, GraphQLInterfaceType) assert Character._meta.model == Reporter assert object_type.get_fields().keys() == [ - 'lastName', 'email', 'id', 'firstName', 'articles'] + 'articles', 'firstName', 'lastName', 'email', 'pets', 'id'] def test_interface_resolve_type(): From 948da46fcdbd1d26c34e78c79b4e8f29a5858dc6 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 5 Oct 2015 20:42:24 -0700 Subject: [PATCH 53/77] Changed field attr naming from null to required --- graphene/contrib/django/converter.py | 4 ++-- graphene/contrib/django/views.py | 2 +- graphene/core/fields.py | 6 +++--- tests/contrib_django/test_converter.py | 5 +++-- tests/core/test_fields.py | 6 +++--- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index 098ab4c2..93420964 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -45,12 +45,12 @@ def _(field): @convert_django_field.register(models.BooleanField) def _(field): - return BooleanField(description=field.description, null=False) + return BooleanField(description=field.description, required=True) @convert_django_field.register(models.NullBooleanField) def _(field): - return BooleanField(description=field.description, null=True) + return BooleanField(description=field.description) @convert_django_field.register(models.FloatField) diff --git a/graphene/contrib/django/views.py b/graphene/contrib/django/views.py index b4482c62..f17d550b 100644 --- a/graphene/contrib/django/views.py +++ b/graphene/contrib/django/views.py @@ -60,7 +60,7 @@ class GraphQLView(View): try: received_json_data = json.loads(request.body) query = received_json_data.get('query') - except ValueError, e: + except ValueError: return self.response_errors(ValueError("Malformed json body in the post data")) else: query = request.POST.get('query') or request.GET.get('query') diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 5a8a6e20..f3a905d4 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -18,10 +18,10 @@ from graphene.core.scalars import GraphQLSkipField class Field(object): SKIP = GraphQLSkipField - def __init__(self, field_type, name=None, resolve=None, null=True, args=None, description='', **extra_args): + def __init__(self, field_type, name=None, resolve=None, required=False, args=None, description='', **extra_args): self.field_type = field_type self.resolve_fn = resolve - self.null = null + self.required = required self.args = args or {} self.extra_args = extra_args self._type = None @@ -60,7 +60,7 @@ class Field(object): return schema.get_type(field_type) def type_wrapper(self, field_type): - if not self.null: + if self.required: field_type = GraphQLNonNull(field_type) return field_type diff --git a/tests/contrib_django/test_converter.py b/tests/contrib_django/test_converter.py index 9d1439c0..23a523ac 100644 --- a/tests/contrib_django/test_converter.py +++ b/tests/contrib_django/test_converter.py @@ -76,12 +76,13 @@ def test_should_integer_convert_int(): def test_should_boolean_convert_boolean(): - assert_conversion(models.BooleanField, graphene.BooleanField) + field = assert_conversion(models.BooleanField, graphene.BooleanField) + assert field.required is True def test_should_nullboolean_convert_boolean(): field = assert_conversion(models.NullBooleanField, graphene.BooleanField) - assert field.null is True + assert field.required is False def test_should_float_convert_float(): diff --git a/tests/core/test_fields.py b/tests/core/test_fields.py index dce05ad2..68497843 100644 --- a/tests/core/test_fields.py +++ b/tests/core/test_fields.py @@ -69,15 +69,15 @@ def test_stringfield_type(): assert f.internal_type(schema) == GraphQLString -def test_stringfield_type_null(): - f = StringField(null=False) +def test_stringfield_type_required(): + f = StringField(required=True) f.contribute_to_class(ot, 'field_name') assert isinstance(f.internal_field(schema), GraphQLField) assert isinstance(f.internal_type(schema), GraphQLNonNull) def test_field_resolve(): - f = StringField(null=False, resolve=lambda *args: 'RESOLVED') + f = StringField(required=True, resolve=lambda *args: 'RESOLVED') f.contribute_to_class(ot, 'field_name') field_type = f.internal_field(schema) assert 'RESOLVED' == field_type.resolver(ot, 2, 3) From b58269ce7201d31655c17fb2c66c1cce8b348969 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 5 Oct 2015 22:30:05 -0700 Subject: [PATCH 54/77] Improved Python3 integration --- graphene/contrib/django/views.py | 2 +- graphene/core/fields.py | 5 +++-- graphene/core/types.py | 4 ++-- tests/contrib_django/test_views.py | 1 - 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/graphene/contrib/django/views.py b/graphene/contrib/django/views.py index f17d550b..9e5d6505 100644 --- a/graphene/contrib/django/views.py +++ b/graphene/contrib/django/views.py @@ -38,7 +38,7 @@ class GraphQLView(View): try: result = self.schema.execute(query, root=object()) data = self.format_result(result) - except Exception, e: + except Exception as e: if settings.DEBUG: raise e return self.response_errors(e) diff --git a/graphene/core/fields.py b/graphene/core/fields.py index f3a905d4..02263ad2 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -1,4 +1,5 @@ import inspect +import six from graphql.core.type import ( GraphQLField, GraphQLList, @@ -53,7 +54,7 @@ class Field(object): return field_type.get_object_type(schema) if _is_class and issubclass(field_type, BaseObjectType): return field_type - elif isinstance(field_type, basestring): + elif isinstance(field_type, six.string_types): if field_type == 'self': return self.object_type else: @@ -84,7 +85,7 @@ class Field(object): 'Field could not be constructed in a non graphene.Type or graphene.Interface') extra_args = self.extra_args.copy() - for arg_name, arg_value in extra_args.items(): + for arg_name, arg_value in self.extra_args.items(): if isinstance(arg_value, GraphQLArgument): self.args[arg_name] = arg_value del extra_args[arg_name] diff --git a/graphene/core/types.py b/graphene/core/types.py index a646f37b..89075b94 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -103,9 +103,9 @@ class BaseObjectType(object): return None elif type(instance) is cls: instance = instance.instance - return super(BaseObjectType, cls).__new__(cls, instance, *args, **kwargs) + return super(BaseObjectType, cls).__new__(cls, *args, **kwargs) - def __init__(self, instance=None): + def __init__(self, instance): signals.pre_init.send(self.__class__, instance=instance) self.instance = instance signals.post_init.send(self.__class__, instance=self) diff --git a/tests/contrib_django/test_views.py b/tests/contrib_django/test_views.py index bfd1efc3..f8ab5b20 100644 --- a/tests/contrib_django/test_views.py +++ b/tests/contrib_django/test_views.py @@ -33,7 +33,6 @@ def test_client_get_no_query(settings, client): def test_client_post_no_query(settings, client): settings.ROOT_URLCONF = 'tests.contrib_django.test_urls' response = client.post('/graphql', {}) - print response.content json_response = format_response(response) assert json_response == {'errors': [{'message': 'Must provide query string.'}]} From fe510dc6861d76af2f0fc793c4a22c2d6a8f4df0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 5 Oct 2015 22:59:23 -0700 Subject: [PATCH 55/77] Completed Python3 Compatibility! --- .travis.yml | 4 ++++ graphene/contrib/django/views.py | 5 ++--- graphene/core/types.py | 3 --- setup.py | 6 ++++++ tests/contrib_django/test_schema.py | 13 ++++++++++--- tests/contrib_django/test_types.py | 8 ++++++-- tests/contrib_django/test_views.py | 2 +- tests/core/test_schema.py | 7 +++++-- tests/utils.py | 3 +++ tox.ini | 2 +- 10 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 tests/utils.py diff --git a/.travis.yml b/.travis.yml index 208ed4e9..f7dc3991 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,10 @@ language: python sudo: false python: - 2.7 +- 3.3 +- 3.4 +- 3.5 +- pypy install: - pip install pytest pytest-cov coveralls flake8 six blinker # - pip install -e .[django] # TODO: Commented until graphqllib is in pypi diff --git a/graphene/contrib/django/views.py b/graphene/contrib/django/views.py index 9e5d6505..9a08fe58 100644 --- a/graphene/contrib/django/views.py +++ b/graphene/contrib/django/views.py @@ -20,7 +20,7 @@ class GraphQLView(View): def format_result(result): data = {'data': result.data} if result.errors: - data['errors'] = map(form_error, result.errors) + data['errors'] = list(map(form_error, result.errors)) return data @@ -42,7 +42,6 @@ class GraphQLView(View): if settings.DEBUG: raise e return self.response_errors(e) - return JsonResponse(data) def get(self, request, *args, **kwargs): @@ -58,7 +57,7 @@ class GraphQLView(View): content_type = self.get_content_type(request) if content_type == 'application/json': try: - received_json_data = json.loads(request.body) + received_json_data = json.loads(request.body.decode()) query = received_json_data.get('query') except ValueError: return self.response_errors(ValueError("Malformed json body in the post data")) diff --git a/graphene/core/types.py b/graphene/core/types.py index 89075b94..02173706 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -117,9 +117,6 @@ class BaseObjectType(object): def get_field(self, field): return getattr(self.instance, field, None) - def __eq__(self, other): - return self.instance.__eq__(other) - def resolve(self, field_name, args, info): custom_resolve_fn = 'resolve_%s' % field_name if hasattr(self, custom_resolve_fn): diff --git a/setup.py b/setup.py index 441a1069..6d3a686c 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,12 @@ setup( 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries', 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: Implementation :: PyPy', ], keywords='api graphql protocol rest relay graphene', diff --git a/tests/contrib_django/test_schema.py b/tests/contrib_django/test_schema.py index 95003004..830aa2e7 100644 --- a/tests/contrib_django/test_schema.py +++ b/tests/contrib_django/test_schema.py @@ -9,6 +9,8 @@ from graphene.contrib.django import ( ) from .models import Reporter, Article +from tests.utils import assert_equal_lists + def test_should_raise_if_no_model(): with raises(Exception) as excinfo: @@ -49,8 +51,10 @@ def test_should_map_fields_correctly(): class Meta: model = Reporter - assert ReporterType2._meta.fields_map.keys( - ) == ['articles', 'first_name', 'last_name', 'email', 'pets', 'id'] + assert_equal_lists( + ReporterType2._meta.fields_map.keys(), + ['articles', 'first_name', 'last_name', 'email', 'pets', 'id'] + ) def test_should_map_fields(): @@ -93,7 +97,10 @@ def test_should_map_only_few_fields(): class Meta: model = Reporter only_fields = ('id', 'email') - assert Reporter2._meta.fields_map.keys() == ['id', 'email'] + assert_equal_lists( + Reporter2._meta.fields_map.keys(), + ['id', 'email'] + ) def test_should_node(): diff --git a/tests/contrib_django/test_types.py b/tests/contrib_django/test_types.py index 28435a13..7308da22 100644 --- a/tests/contrib_django/test_types.py +++ b/tests/contrib_django/test_types.py @@ -18,6 +18,8 @@ from graphene.contrib.django.types import ( from .models import Reporter, Article +from tests.utils import assert_equal_lists + class Character(DjangoInterface): @@ -48,8 +50,10 @@ def test_pseudo_interface(): assert Character._meta.interface is True assert isinstance(object_type, GraphQLInterfaceType) assert Character._meta.model == Reporter - assert object_type.get_fields().keys() == [ - 'articles', 'firstName', 'lastName', 'email', 'pets', 'id'] + assert_equal_lists( + object_type.get_fields().keys(), + ['articles', 'firstName', 'lastName', 'email', 'pets', 'id'] + ) def test_interface_resolve_type(): diff --git a/tests/contrib_django/test_views.py b/tests/contrib_django/test_views.py index f8ab5b20..be689062 100644 --- a/tests/contrib_django/test_views.py +++ b/tests/contrib_django/test_views.py @@ -20,7 +20,7 @@ from graphene.contrib.django.types import ( def format_response(response): - return json.loads(response.content) + return json.loads(response.content.decode()) def test_client_get_no_query(settings, client): diff --git a/tests/core/test_schema.py b/tests/core/test_schema.py index fa08f944..4d89d725 100644 --- a/tests/core/test_schema.py +++ b/tests/core/test_schema.py @@ -19,6 +19,7 @@ from graphene import ( Schema ) +from tests.utils import assert_equal_lists schema = Schema(name='My own schema') @@ -104,5 +105,7 @@ def test_query_schema_execute(): def test_schema_get_type_map(): - assert schema.schema.get_type_map().keys() == [ - '__Field', 'String', 'Pet', 'Character', '__InputValue', '__Directive', '__TypeKind', '__Schema', '__Type', 'Human', '__EnumValue', 'Boolean'] + assert_equal_lists( + schema.schema.get_type_map().keys(), + ['__Field', 'String', 'Pet', 'Character', '__InputValue', '__Directive', '__TypeKind', '__Schema', '__Type', 'Human', '__EnumValue', 'Boolean'] + ) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..8101f15c --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,3 @@ + +def assert_equal_lists(l1, l2): + assert sorted(l1) == sorted(l2) diff --git a/tox.ini b/tox.ini index cd4b45a4..2feddd4c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27 +envlist = py27,py33,py34,py35,pypy [testenv] deps= From 08434993062c669e95c411fd06f43f5e1e2d3721 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 6 Oct 2015 00:49:13 -0700 Subject: [PATCH 56/77] Improved field description converter from Django fields --- graphene/contrib/django/converter.py | 14 +++++++------- graphene/core/fields.py | 1 + tests/contrib_django/test_converter.py | 3 ++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index 93420964..7f95dd8f 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -26,12 +26,12 @@ def convert_django_field(field): @convert_django_field.register(models.URLField) @convert_django_field.register(models.UUIDField) def _(field): - return StringField(description=field.description) + return StringField(description=field.help_text) @convert_django_field.register(models.AutoField) def _(field): - return IDField(description=field.description) + return IDField(description=field.help_text) @convert_django_field.register(models.PositiveIntegerField) @@ -40,22 +40,22 @@ def _(field): @convert_django_field.register(models.BigIntegerField) @convert_django_field.register(models.IntegerField) def _(field): - return IntField(description=field.description) + return IntField(description=field.help_text) @convert_django_field.register(models.BooleanField) def _(field): - return BooleanField(description=field.description, required=True) + return BooleanField(description=field.help_text, required=True) @convert_django_field.register(models.NullBooleanField) def _(field): - return BooleanField(description=field.description) + return BooleanField(description=field.help_text) @convert_django_field.register(models.FloatField) def _(field): - return FloatField(description=field.description) + return FloatField(description=field.help_text) @convert_django_field.register(models.ManyToManyField) @@ -68,4 +68,4 @@ def _(field): @convert_django_field.register(models.OneToOneField) @convert_django_field.register(models.ForeignKey) def _(field): - return DjangoModelField(field.related_model, description=field.description) + return DjangoModelField(field.related_model, description=field.help_text) diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 02263ad2..69e831ff 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -100,6 +100,7 @@ class Field(object): internal_type = self.internal_type(schema) if not internal_type: raise Exception("Internal type for field %s is None" % self) + return GraphQLField( internal_type, description=self.description, diff --git a/tests/contrib_django/test_converter.py b/tests/contrib_django/test_converter.py index 23a523ac..8c046997 100644 --- a/tests/contrib_django/test_converter.py +++ b/tests/contrib_django/test_converter.py @@ -15,9 +15,10 @@ from .models import Article, Reporter def assert_conversion(django_field, graphene_field, *args): - field = django_field(*args) + field = django_field(*args, help_text='Custom Help Text') graphene_type = convert_django_field(field) assert isinstance(graphene_type, graphene_field) + assert graphene_type.description == 'Custom Help Text' return graphene_type From f9a0f18b0d6007499a28e657e403f955c66285d0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 6 Oct 2015 01:05:01 -0700 Subject: [PATCH 57/77] Fixed local_fields with fields when Extending from A non abstract Django model --- graphene/contrib/django/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index 56f0237b..0c660217 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -26,7 +26,7 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): return only_fields = cls._meta.only_fields reverse_fields = tuple(get_reverse_fields(cls._meta.model)) - all_fields = (list(cls._meta.model._meta.local_fields) + + all_fields = (list(cls._meta.model._meta.fields) + list(reverse_fields) + list(cls._meta.model._meta.local_many_to_many)) From 2303db7f94361b034c0ea1179d7d26089d6989a7 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 6 Oct 2015 20:59:07 -0700 Subject: [PATCH 58/77] Updated repo url under graphql-python github community --- README.md | 14 +++++++++----- setup.py | 8 ++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e518c422..e8ed6633 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,17 @@ -# Graphene: Pythonic GraphQL [![Build Status](https://travis-ci.org/syrusakbary/graphene.svg?branch=master)](https://travis-ci.org/syrusakbary/graphene) [![Coverage Status](https://coveralls.io/repos/syrusakbary/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/syrusakbary/graphene?branch=master) +# Graphene: Pythonic GraphQL [![Build Status](https://travis-ci.org/graphql-python/graphene.svg?branch=master)](https://travis-ci.org/graphql-python/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master) This is a library to use GraphQL in a Pythonic and easy way. It maps the models/fields to internal GraphQLlib objects without effort. Including automatic [Django models](#djangorelay-schema) conversion. -*Note: This library requires installing [graphqllib](https://github.com/dittos/graphqllib) and [graphql-relay](https://github.com/syrusakbary/graphql-relay-py) external libraries.* +## Installation + +For instaling graphene, just run this command in your shell + +```bash +pip install graphene +``` + ## Usage @@ -13,9 +20,6 @@ Example code of a GraphQL schema using Graphene: ### Schema definition ```python -import graphene -# ... - class Character(graphene.Interface): id = graphene.IDField() name = graphene.StringField() diff --git a/setup.py b/setup.py index 6d3a686c..55654622 100644 --- a/setup.py +++ b/setup.py @@ -24,11 +24,11 @@ class PyTest(TestCommand): setup( name='graphene', - version='0.1', + version='0.1.0', description='Graphene: Python DSL for GraphQL', - url='https://github.com/syrusakbary/graphene', + url='https://github.com/graphql-python/graphene', author='Syrus Akbary', author_email='me@syrusakbary.com', @@ -55,8 +55,8 @@ setup( install_requires=[ 'six', 'blinker', - 'graphqllib', - 'graphql-relay' + 'graphql-core==0.1a0', + 'graphql-relay==0.1.2' ], tests_require=['pytest>=2.7.2'], extras_require={ From b6163709f9af33f6bafad44693c5fb7000b7cc1e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 6 Oct 2015 21:04:24 -0700 Subject: [PATCH 59/77] Added RST Readme --- README.rst | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 1 + 2 files changed, 123 insertions(+) create mode 100644 README.rst diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..f65fd728 --- /dev/null +++ b/README.rst @@ -0,0 +1,122 @@ +Graphene: Pythonic GraphQL |Build Status| |Coverage Status| +=========================================================== + +This is a library to use GraphQL in a Pythonic and easy way. It maps the +models/fields to internal GraphQLlib objects without effort. Including +automatic `Django models`_ conversion. + +Installation +------------ + +For instaling graphene, just run this command in your shell + +.. code:: bash + + pip install graphene + +Usage +----- + +Example code of a GraphQL schema using Graphene: + +Schema definition +~~~~~~~~~~~~~~~~~ + +.. code:: python + + class Character(graphene.Interface): + id = graphene.IDField() + name = graphene.StringField() + friends = graphene.ListField('self') + + def resolve_friends(self, args, *_): + return [Human(f) for f in self.instance.friends] + + class Human(Character): + homePlanet = graphene.StringField() + + class Query(graphene.ObjectType): + human = graphene.Field(Human) + + schema = graphene.Schema(query=Query) + +Querying +~~~~~~~~ + +Querying ``graphene.Schema`` is as simple as: + +.. code:: python + + query = ''' + query HeroNameQuery { + hero { + name + } + } + ''' + result = schema.execute(query) + +Relay Schema +~~~~~~~~~~~~ + +Graphene also supports Relay, check the `Starwars Relay example`_! + +.. code:: python + + 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): + return Ship(getShip(id)) + + + class Query(graphene.ObjectType): + ships = relay.ConnectionField(Ship, description='The ships used by the faction.') + node = relay.NodeField() + + @resolve_only_args + def resolve_ships(self): + return [Ship(s) for s in getShips()] + +Django+Relay Schema +~~~~~~~~~~~~~~~~~~~ + +If you want to use graphene with your Django Models check the `Starwars +Django example`_! + +.. code:: python + + class Ship(DjangoNode): + class Meta: + model = YourDjangoModelHere + # only_fields = ('id', 'name') # Only map this fields from the model + # excluxe_fields ('field_to_excluxe', ) # Exclude mapping this fields from the model + + class Query(graphene.ObjectType): + node = relay.NodeField() + +Contributing +------------ + +After cloning this repo, ensure dependencies are installed by running: + +.. code:: sh + + python setup.py install + +After developing, the full test suite can be evaluated by running: + +.. code:: sh + + python setup.py test # Use --pytest-args="-v -s" for verbose mode + +.. _Django models: #djangorelay-schema +.. _Starwars Relay example: tests/starwars_relay +.. _Starwars Django example: tests/starwars_django + +.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master + :target: https://travis-ci.org/graphql-python/graphene +.. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github + :target: https://coveralls.io/github/graphql-python/graphene?branch=master diff --git a/setup.py b/setup.py index 55654622..242425ec 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,7 @@ setup( version='0.1.0', description='Graphene: Python DSL for GraphQL', + long_description=open('README.rst').read(), url='https://github.com/graphql-python/graphene', From 660b7664485cf4dabb0c6987e4bc11ef345b2794 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 6 Oct 2015 22:31:00 -0700 Subject: [PATCH 60/77] Updated graphql-relay version --- .travis.yml | 5 +---- setup.py | 2 +- tox.ini | 2 ++ 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index f7dc3991..4e7146a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,10 +8,7 @@ python: - pypy install: - pip install pytest pytest-cov coveralls flake8 six blinker -# - pip install -e .[django] # TODO: Commented until graphqllib is in pypi -- pip install Django>=1.8.0 pytest-django singledispatch>=3.4.0.3 -- pip install git+https://github.com/dittos/graphqllib.git # Last version of graphqllib -- pip install graphql-relay +- pip install -e .[django] - python setup.py develop script: - py.test --cov=graphene diff --git a/setup.py b/setup.py index 242425ec..b44ffe5b 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ setup( 'six', 'blinker', 'graphql-core==0.1a0', - 'graphql-relay==0.1.2' + 'graphql-relay==0.1.3' ], tests_require=['pytest>=2.7.2'], extras_require={ diff --git a/tox.ini b/tox.ini index 2feddd4c..24c4c1c0 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,8 @@ deps= pytest>=2.7.2 django>=1.8.0,<1.9 pytest-django + graphql-core==0.1a0 + graphql-relay==0.1.3 flake8 six blinker From f9b48b0fe8adedaef33fb93ad136bd94a9770638 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 6 Oct 2015 22:52:05 -0700 Subject: [PATCH 61/77] Updated version (fixing graphql-relay-py requirement) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b44ffe5b..0558ba0c 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ class PyTest(TestCommand): setup( name='graphene', - version='0.1.0', + version='0.1.1', description='Graphene: Python DSL for GraphQL', long_description=open('README.rst').read(), From 6537154d9fc241f331f871a4f4aa323a6f8e6a6c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 6 Oct 2015 22:53:43 -0700 Subject: [PATCH 62/77] Fixed tests --- tests/starwars/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/starwars/schema.py b/tests/starwars/schema.py index 71bbb92c..e5d97a90 100644 --- a/tests/starwars/schema.py +++ b/tests/starwars/schema.py @@ -50,7 +50,7 @@ class Query(graphene.ObjectType): type_name = 'core.Query' @resolve_only_args - def resolve_hero(self, episode): + def resolve_hero(self, episode=None): return wrap_character(getHero(episode)) @resolve_only_args From d7f4804f5667a0e8f72943adcde3e77396795d35 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 6 Oct 2015 23:13:10 -0700 Subject: [PATCH 63/77] Improved README --- README.md | 24 +++++++++--------------- README.rst | 10 +++++----- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index e8ed6633..d2c6e4a7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -# Graphene: Pythonic GraphQL [![Build Status](https://travis-ci.org/graphql-python/graphene.svg?branch=master)](https://travis-ci.org/graphql-python/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master) +# Graphene [![Build Status](https://travis-ci.org/graphql-python/graphene.svg?branch=master)](https://travis-ci.org/graphql-python/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master) -This is a library to use GraphQL in a Pythonic and easy way. -It maps the models/fields to internal GraphQLlib objects without effort. Including automatic [Django models](#djangorelay-schema) conversion. +Graphene is a Python library for creating GraphQL schemas/types easly. +It maps the models/fields to internal GraphQL objects without effort. +Including automatic [Django models](#djangorelay-schema) conversion. ## Installation @@ -37,9 +38,7 @@ class Query(graphene.ObjectType): schema = graphene.Schema(query=Query) ``` -### Querying - -Querying `graphene.Schema` is as simple as: +Then Querying `graphene.Schema` is as simple as: ```python query = ''' @@ -58,22 +57,17 @@ Graphene also supports Relay, check the [Starwars Relay example](tests/starwars_ ```python class Ship(relay.Node): - '''A ship in the Star Wars saga''' - name = graphene.StringField(description='The name of the ship.') + name = graphene.StringField() @classmethod def get_node(cls, id): - return Ship(getShip(id)) + return Ship(your_ship_instance) class Query(graphene.ObjectType): - ships = relay.ConnectionField(Ship, description='The ships used by the faction.') + ships = relay.ConnectionField(Ship) node = relay.NodeField() - @resolve_only_args - def resolve_ships(self): - return [Ship(s) for s in getShips()] - ``` ### Django+Relay Schema @@ -85,7 +79,7 @@ class Ship(DjangoNode): class Meta: model = YourDjangoModelHere # only_fields = ('id', 'name') # Only map this fields from the model - # excluxe_fields ('field_to_excluxe', ) # Exclude mapping this fields from the model + # exclude_fields ('field_to_exclude', ) # Exclude mapping this fields from the model class Query(graphene.ObjectType): node = relay.NodeField() diff --git a/README.rst b/README.rst index f65fd728..e3f0fdf5 100644 --- a/README.rst +++ b/README.rst @@ -1,9 +1,9 @@ -Graphene: Pythonic GraphQL |Build Status| |Coverage Status| -=========================================================== +Graphene |Build Status| |Coverage Status| +========================================= -This is a library to use GraphQL in a Pythonic and easy way. It maps the -models/fields to internal GraphQLlib objects without effort. Including -automatic `Django models`_ conversion. +Graphene is a Python library for creating GraphQL schemas/types easly. +It maps the models/fields to internal GraphQL objects without effort. +Including automatic `Django models`_ conversion. Installation ------------ From 0c8fa40b624de40ef38d8d6f1b2d07b3285eedf5 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 7 Oct 2015 21:29:25 -0700 Subject: [PATCH 64/77] Improved graphene dependency --- setup.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 0558ba0c..23397f3e 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ class PyTest(TestCommand): setup( name='graphene', - version='0.1.1', + version='0.1.2', description='Graphene: Python DSL for GraphQL', long_description=open('README.rst').read(), @@ -57,13 +57,15 @@ setup( 'six', 'blinker', 'graphql-core==0.1a0', - 'graphql-relay==0.1.3' + 'graphql-relay==0.1.4' + ], + tests_require=[ + 'pytest>=2.7.2', + 'pytest-django', ], - tests_require=['pytest>=2.7.2'], extras_require={ 'django': [ - 'Django>=1.8.0,<1.9', - 'pytest-django', + 'Django>=1.6.0,<1.9', 'singledispatch>=3.4.0.3', ], }, From ecc2fa1e6e4dbbb80395ef6725f6d26b44fe4ef6 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 8 Oct 2015 09:23:55 -0700 Subject: [PATCH 65/77] Fixed tests --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4e7146a0..f4d6a6bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - 3.5 - pypy install: -- pip install pytest pytest-cov coveralls flake8 six blinker +- pip install pytest pytest-cov coveralls flake8 six blinker pytest-django - pip install -e .[django] - python setup.py develop script: From fbd9465e57fb8635d3be9fd05ceb275f96ca4393 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 8 Oct 2015 17:30:46 -0700 Subject: [PATCH 66/77] Use last version of graphql-core. Fixed introspection #3 --- graphene/core/schema.py | 3 ++- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 7b3cd640..69241185 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -4,6 +4,7 @@ from graphql.core import graphql from graphql.core.type import ( GraphQLSchema as _GraphQLSchema ) +from graphql.core.utils.introspection_query import introspection_query from graphene import signals from graphene.utils import cached_property @@ -71,7 +72,7 @@ class Schema(object): ) def introspect(self): - return self._schema.get_type_map() + return self.execute(introspection_query).data def register_internal_type(fun): diff --git a/setup.py b/setup.py index 23397f3e..e5498d0b 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ class PyTest(TestCommand): setup( name='graphene', - version='0.1.2', + version='0.1.3', description='Graphene: Python DSL for GraphQL', long_description=open('README.rst').read(), @@ -56,7 +56,7 @@ setup( install_requires=[ 'six', 'blinker', - 'graphql-core==0.1a0', + 'graphql-core==0.1a2', 'graphql-relay==0.1.4' ], tests_require=[ From 0bc02180323c3202641885a787a3366d5ee5fc52 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 8 Oct 2015 23:24:21 -0700 Subject: [PATCH 67/77] Improved fields ordering. Thanks @leebyron for pointing this! Not everything yet fixed. Have to fix too in graphql-core/relay --- graphene/contrib/django/types.py | 8 ++++---- graphene/core/fields.py | 20 ++++++++++++++++++++ graphene/core/options.py | 9 +++------ graphene/core/types.py | 12 +++++------- graphene/relay/fields.py | 6 +++--- tests/contrib_django/test_types.py | 10 +++++----- 6 files changed, 40 insertions(+), 25 deletions(-) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index 0c660217..87af3cae 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -25,10 +25,10 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): if not cls._meta.model: return only_fields = cls._meta.only_fields - reverse_fields = tuple(get_reverse_fields(cls._meta.model)) - all_fields = (list(cls._meta.model._meta.fields) + - list(reverse_fields) + - list(cls._meta.model._meta.local_many_to_many)) + reverse_fields = get_reverse_fields(cls._meta.model) + all_fields = sorted(list(cls._meta.model._meta.fields) + + list(reverse_fields) + + list(cls._meta.model._meta.local_many_to_many)) for field in all_fields: is_not_in_only = only_fields and field.name not in only_fields diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 69e831ff..862e2b31 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -1,5 +1,6 @@ import inspect import six +from functools import total_ordering from graphql.core.type import ( GraphQLField, GraphQLList, @@ -16,8 +17,10 @@ from graphene.core.types import BaseObjectType from graphene.core.scalars import GraphQLSkipField +@total_ordering class Field(object): SKIP = GraphQLSkipField + creation_counter = 0 def __init__(self, field_type, name=None, resolve=None, required=False, args=None, description='', **extra_args): self.field_type = field_type @@ -29,6 +32,8 @@ class Field(object): self.name = name self.description = description or self.__doc__ self.object_type = None + self.creation_counter = Field.creation_counter + Field.creation_counter += 1 def contribute_to_class(self, cls, name): if not self.name: @@ -122,6 +127,21 @@ class Field(object): return '<%s: %s>' % (path, name) return '<%s>' % path + def __eq__(self, other): + # Needed for @total_ordering + if isinstance(other, Field): + return self.creation_counter == other.creation_counter + return NotImplemented + + def __lt__(self, other): + # This is needed because bisect does not take a comparison function. + if isinstance(other, Field): + return self.creation_counter < other.creation_counter + return NotImplemented + + def __hash__(self): + return hash(self.creation_counter) + class NativeField(Field): diff --git a/graphene/core/options.py b/graphene/core/options.py index 6ff03b3d..1d052f04 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -1,4 +1,5 @@ from graphene.utils import cached_property +from collections import OrderedDict DEFAULT_NAMES = ('description', 'name', 'interface', 'type_name', 'interfaces', 'proxy') @@ -66,12 +67,8 @@ class Options(object): fields = [] for parent in self.parents: fields.extend(parent._meta.fields) - return self.local_fields + fields + return sorted(self.local_fields + fields) @cached_property def fields_map(self): - return {f.field_name: f for f in self.fields} - - @cached_property - def internal_fields_map(self): - return {f.name: f for f in self.fields} + return OrderedDict([(f.field_name, f) for f in self.fields]) diff --git a/graphene/core/types.py b/graphene/core/types.py index 02173706..fed216c4 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -1,5 +1,6 @@ import inspect import six +from collections import OrderedDict from graphql.core.type import ( GraphQLObjectType, @@ -49,10 +50,9 @@ class ObjectTypeMeta(type): new_class.add_extra_fields() new_fields = new_class._meta.local_fields - field_names = {f.name:f for f in new_fields} + field_names = {f.name: f for f in new_fields} for base in parents: - original_base = base if not hasattr(base, '_meta'): # Things without _meta aren't functional models, so they're # uninteresting parents. @@ -132,11 +132,9 @@ class BaseObjectType(object): @memoize @register_internal_type def internal_type(cls, schema): - fields_map = cls._meta.internal_fields_map - fields = lambda: { - name: field.internal_field(schema) - for name, field in fields_map.items() - } + fields_list = cls._meta.fields + fields = lambda: OrderedDict([(f.name, f.internal_field(schema)) + for f in fields_list]) if cls._meta.interface: return GraphQLInterfaceType( cls._meta.type_name, diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index c9eb87e0..4542d558 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -1,4 +1,4 @@ -import collections +from collections import Iterable, OrderedDict from graphql_relay.connection.arrayconnection import ( connectionFromArray @@ -30,7 +30,7 @@ class ConnectionField(Field): if resolved: resolved = self.wrap_resolved(resolved, instance, args, info) assert isinstance( - resolved, collections.Iterable), 'Resolved value from the connection field have to be iterable' + resolved, Iterable), 'Resolved value from the connection field have to be iterable' return connectionFromArray(resolved, args) @memoize @@ -71,7 +71,7 @@ class NodeTypeField(LazyField): if resolved_global_id.type == self.field_object_type._meta.type_name: return node_field.resolver(instance, args, info) - args = {a.name: a for a in node_field.args} + args = OrderedDict([(a.name, a) for a in node_field.args]) field = Field(self.field_object_type, id=args['id'], resolve=resolver) field.contribute_to_class(self.object_type, self.field_name) diff --git a/tests/contrib_django/test_types.py b/tests/contrib_django/test_types.py index 7308da22..4efabc6b 100644 --- a/tests/contrib_django/test_types.py +++ b/tests/contrib_django/test_types.py @@ -63,13 +63,13 @@ def test_interface_resolve_type(): def test_object_type(): object_type = Human.internal_type(schema) - internal_fields_map = Human._meta.internal_fields_map + fields_map = Human._meta.fields_map assert Human._meta.interface is False assert isinstance(object_type, GraphQLObjectType) assert object_type.get_fields() == { - 'headline': internal_fields_map['headline'].internal_field(schema), - 'id': internal_fields_map['id'].internal_field(schema), - 'reporter': internal_fields_map['reporter'].internal_field(schema), - 'pubDate': internal_fields_map['pubDate'].internal_field(schema), + 'headline': fields_map['headline'].internal_field(schema), + 'id': fields_map['id'].internal_field(schema), + 'reporter': fields_map['reporter'].internal_field(schema), + 'pubDate': fields_map['pub_date'].internal_field(schema), } assert object_type.get_interfaces() == [DjangoNode.internal_type(schema)] From cf3a32b50a2420d206d36c09e1c0bb726b8c001e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 9 Oct 2015 22:10:09 -0700 Subject: [PATCH 68/77] Improved field naming --- graphene/core/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 862e2b31..6318c25a 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -121,7 +121,7 @@ class Field(object): """ Displays the module, class and name of the field. """ - path = '%r.%s' % (self.object_type, self.__class__.__name__) + path = '%s[%s]' % (self.__class__.__name__, str(self.field_type)) name = getattr(self, 'field_name', None) if name is not None: return '<%s: %s>' % (path, name) From 042325192a1d2e1772ff4bd9891b7af8812c2f48 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 9 Oct 2015 22:39:29 -0700 Subject: [PATCH 69/77] Added ability to set up a custom graphql executor. Fixed #7. Added automatic field ordering as default --- graphene/core/schema.py | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 69241185..38312c59 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -1,9 +1,18 @@ from functools import wraps +from collections import OrderedDict from graphql.core import graphql from graphql.core.type import ( GraphQLSchema as _GraphQLSchema ) + +from graphql.core.execution.executor import Executor +from graphql.core.execution.middlewares.sync import SynchronousExecutionMiddleware +from graphql.core.execution import ExecutionResult, execute +from graphql.core.language.parser import parse +from graphql.core.language.source import Source +from graphql.core.validation import validate + from graphql.core.utils.introspection_query import introspection_query from graphene import signals from graphene.utils import cached_property @@ -17,12 +26,14 @@ class GraphQLSchema(_GraphQLSchema): class Schema(object): _query = None + _executor = None - def __init__(self, query=None, mutation=None, name='Schema'): + def __init__(self, query=None, mutation=None, name='Schema', executor=None): self._internal_types = {} self.mutation = mutation self.query = query self.name = name + self.executor = executor signals.init_schema.send(self) def __repr__(self): @@ -37,6 +48,16 @@ class Schema(object): self._query = query self._query_type = query and query.internal_type(self) + @property + def executor(self): + if not self._executor: + self.executor = Executor([SynchronousExecutionMiddleware()], map_type=OrderedDict) + return self._executor + + @executor.setter + def executor(self, value): + self._executor = value + @cached_property def schema(self): if not self._query_type: @@ -63,12 +84,22 @@ class Schema(object): def execute(self, request='', root=None, vars=None, operation_name=None): root = root or object() - return graphql( + source = Source(request, 'GraphQL request') + ast = parse(source) + validation_errors = validate(self.schema, ast) + if validation_errors: + return ExecutionResult( + errors=validation_errors, + invalid=True, + ) + + return self.executor.execute( self.schema, - request=request, + ast, root=self.query(root), - vars=vars, - operation_name=operation_name + args=vars, + operation_name=operation_name, + validate_ast=False ) def introspect(self): From 97224c6083d87c5a6eafec84a24f8c8fa4a1a372 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 9 Oct 2015 22:55:15 -0700 Subject: [PATCH 70/77] Fixed foreign key, oneToOne resolve --- graphene/contrib/django/fields.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index f92096cd..a0d24555 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -62,6 +62,12 @@ class DjangoModelField(Field): super(DjangoModelField, self).__init__(None, *args, **kwargs) self.model = model + def resolve(self, instance, args, info): + resolved = super(DjangoModelField, self).resolve(instance, args, info) + schema = info.schema.graphene_schema + _type = self.get_object_type(schema) + return _type(resolved) + @memoize def internal_type(self, schema): _type = self.get_object_type(schema) From ea95ca2b0c3d58c622d30b5e8ae55874bcaf3297 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 9 Oct 2015 23:15:07 -0700 Subject: [PATCH 71/77] Fixed field ordering adding reverse related fields at the end --- graphene/contrib/django/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index 87af3cae..4ae703d5 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -27,8 +27,8 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): only_fields = cls._meta.only_fields reverse_fields = get_reverse_fields(cls._meta.model) all_fields = sorted(list(cls._meta.model._meta.fields) + - list(reverse_fields) + list(cls._meta.model._meta.local_many_to_many)) + all_fields += list(reverse_fields) for field in all_fields: is_not_in_only = only_fields and field.name not in only_fields From 4e8fd488ffc389ab29f2e42ac5b36be3ad35cae6 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 10 Oct 2015 00:09:14 -0700 Subject: [PATCH 72/77] Updated to work with last version of graphql-relay --- graphene/relay/connections.py | 2 +- graphene/relay/fields.py | 16 ++++++++-------- graphene/relay/types.py | 16 ++++++++-------- setup.py | 2 +- tox.ini | 2 +- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/graphene/relay/connections.py b/graphene/relay/connections.py index 4a074634..59c88eaa 100644 --- a/graphene/relay/connections.py +++ b/graphene/relay/connections.py @@ -1,5 +1,5 @@ from graphql_relay.node.node import ( - globalIdField + global_id_field ) from graphene import signals diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index 4542d558..270d907f 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -1,14 +1,14 @@ from collections import Iterable, OrderedDict from graphql_relay.connection.arrayconnection import ( - connectionFromArray + connection_from_list ) from graphql_relay.connection.connection import ( connectionArgs ) from graphql_relay.node.node import ( - globalIdField, - fromGlobalId + global_id_field, + from_global_id ) from graphene.core.fields import Field, LazyNativeField, LazyField @@ -31,7 +31,7 @@ class ConnectionField(Field): resolved = self.wrap_resolved(resolved, instance, args, info) assert isinstance( resolved, Iterable), 'Resolved value from the connection field have to be iterable' - return connectionFromArray(resolved, args) + return connection_from_list(resolved, args) @memoize def internal_type(self, schema): @@ -53,7 +53,7 @@ class NodeField(LazyNativeField): field.contribute_to_class(self.object_type, self.field_name) return field.internal_field(schema) from graphene.relay.types import BaseNode - return BaseNode.get_definitions(schema).nodeField + return BaseNode.get_definitions(schema).node_field class NodeTypeField(LazyField): @@ -63,11 +63,11 @@ class NodeTypeField(LazyField): def inner_field(self, schema): from graphene.relay.types import BaseNode - node_field = BaseNode.get_definitions(schema).nodeField + node_field = BaseNode.get_definitions(schema).node_field def resolver(instance, args, info): global_id = args.get('id') - resolved_global_id = fromGlobalId(global_id) + resolved_global_id = from_global_id(global_id) if resolved_global_id.type == self.field_object_type._meta.type_name: return node_field.resolver(instance, args, info) @@ -80,4 +80,4 @@ class NodeTypeField(LazyField): class NodeIDField(LazyNativeField): def get_field(self, schema): - return globalIdField(self.object_type._meta.type_name) + return global_id_field(self.object_type._meta.type_name) diff --git a/graphene/relay/types.py b/graphene/relay/types.py index a971447f..50e43e9c 100644 --- a/graphene/relay/types.py +++ b/graphene/relay/types.py @@ -1,9 +1,9 @@ from graphql_relay.node.node import ( - nodeDefinitions, - fromGlobalId + node_definitions, + from_global_id ) from graphql_relay.connection.connection import ( - connectionDefinitions + connection_definitions ) from graphene.core.types import Interface @@ -16,7 +16,7 @@ def get_node_type(schema, obj): def get_node(schema, globalId, *args): - resolvedGlobalId = fromGlobalId(globalId) + resolvedGlobalId = from_global_id(globalId) _type, _id = resolvedGlobalId.type, resolvedGlobalId.id object_type = schema.get_type(_type) if not object_type or not issubclass(object_type, BaseNode): @@ -29,22 +29,22 @@ class BaseNode(object): @classmethod @memoize def get_definitions(cls, schema): - return nodeDefinitions(lambda *args: get_node(schema, *args), lambda *args: get_node_type(schema, *args)) + return node_definitions(lambda *args: get_node(schema, *args), lambda *args: get_node_type(schema, *args)) @classmethod @memoize def get_connection(cls, schema): _type = cls.internal_type(schema) type_name = cls._meta.type_name - connection = connectionDefinitions(type_name, _type).connectionType + connection = connection_definitions(type_name, _type).connection_type return connection @classmethod def internal_type(cls, schema): from graphene.relay.utils import is_node_type if is_node_type(cls): - # Return only nodeInterface when is the Node Inerface - return BaseNode.get_definitions(schema).nodeInterface + # Return only node_interface when is the Node Inerface + return BaseNode.get_definitions(schema).node_interface return super(BaseNode, cls).internal_type(schema) diff --git a/setup.py b/setup.py index e5498d0b..4f0aabfd 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ setup( 'six', 'blinker', 'graphql-core==0.1a2', - 'graphql-relay==0.1.4' + 'graphql-relay==0.2.0' ], tests_require=[ 'pytest>=2.7.2', diff --git a/tox.ini b/tox.ini index 24c4c1c0..89fccc2e 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ deps= django>=1.8.0,<1.9 pytest-django graphql-core==0.1a0 - graphql-relay==0.1.3 + graphql-relay==0.2.0 flake8 six blinker From b511563ccccabb0409b278f660fd02620a6520f8 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 10 Oct 2015 00:13:39 -0700 Subject: [PATCH 73/77] Removed map_type until the last version of graphql-core is on PyPI --- graphene/core/schema.py | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 38312c59..90417eea 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -51,7 +51,9 @@ class Schema(object): @property def executor(self): if not self._executor: - self.executor = Executor([SynchronousExecutionMiddleware()], map_type=OrderedDict) + # TODO: Update to map_type=OrderedDict when graphql-core + # update its package in pypi + self.executor = Executor([SynchronousExecutionMiddleware()]) return self._executor @executor.setter @@ -72,8 +74,6 @@ class Schema(object): return object_type def get_type(self, type_name): - # print 'get_type' - # _type = self.schema.get_type(type_name) if type_name not in self._internal_types: raise Exception('Type %s not found in %r' % (type_name, self)) return self._internal_types[type_name] @@ -84,23 +84,30 @@ class Schema(object): def execute(self, request='', root=None, vars=None, operation_name=None): root = root or object() - source = Source(request, 'GraphQL request') - ast = parse(source) - validation_errors = validate(self.schema, ast) - if validation_errors: - return ExecutionResult( - errors=validation_errors, - invalid=True, - ) - - return self.executor.execute( + return graphql( self.schema, - ast, + request, root=self.query(root), - args=vars, - operation_name=operation_name, - validate_ast=False + vars=vars, + operation_name=operation_name ) + # source = Source(request, 'GraphQL request') + # ast = parse(source) + # validation_errors = validate(self.schema, ast) + # if validation_errors: + # return ExecutionResult( + # errors=validation_errors, + # invalid=True, + # ) + + # return self.executor.execute( + # self.schema, + # ast, + # root=self.query(root), + # args=vars, + # operation_name=operation_name, + # validate_ast=False + # ) def introspect(self): return self.execute(introspection_query).data From fb76c1314a660ac201df85ef919ef65e6d7535ae Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 10 Oct 2015 00:20:47 -0700 Subject: [PATCH 74/77] Fixed tests --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4f0aabfd..a686f03d 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ setup( packages=find_packages(exclude=['tests']), install_requires=[ - 'six', + 'six>=1.10.0', 'blinker', 'graphql-core==0.1a2', 'graphql-relay==0.2.0' From 61d5556beb74ceb2055d93c9066f37378fd3b79a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 10 Oct 2015 00:22:01 -0700 Subject: [PATCH 75/77] Updated graphene version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a686f03d..6395c82b 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ class PyTest(TestCommand): setup( name='graphene', - version='0.1.3', + version='0.1.4', description='Graphene: Python DSL for GraphQL', long_description=open('README.rst').read(), From 6eb1a48cb2fc9a6a1fc89c58900a149ea232a39a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 10 Oct 2015 14:53:46 -0700 Subject: [PATCH 76/77] Improved overall testing coverage --- graphene/contrib/django/fields.py | 4 ++ graphene/core/fields.py | 2 +- graphene/core/types.py | 11 ++++ graphene/relay/types.py | 6 +- tests/contrib_django/test_urls.py | 6 ++ tests/contrib_django/test_views.py | 8 +++ tests/core/test_fields.py | 68 +++++++++++++++++++++++ tests/core/test_scalars.py | 18 ++++++ tests/core/test_schema.py | 30 ++++++++++ tests/relay/test_relayfields.py | 43 ++++++++++++++ tests/starwars_django/data.py | 16 +++++- tests/starwars_django/models.py | 8 +++ tests/starwars_django/schema.py | 11 +++- tests/starwars_django/test_connections.py | 6 ++ 14 files changed, 227 insertions(+), 10 deletions(-) create mode 100644 tests/core/test_scalars.py create mode 100644 tests/relay/test_relayfields.py diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index a0d24555..ba47047e 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -66,6 +66,10 @@ class DjangoModelField(Field): resolved = super(DjangoModelField, self).resolve(instance, args, info) schema = info.schema.graphene_schema _type = self.get_object_type(schema) + assert _type, ("Field %s cannot be retrieved as the " + "ObjectType is not registered by the schema" % ( + self.field_name + )) return _type(resolved) @memoize diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 6318c25a..d4c9f4a8 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -121,7 +121,7 @@ class Field(object): """ Displays the module, class and name of the field. """ - path = '%s[%s]' % (self.__class__.__name__, str(self.field_type)) + path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__) name = getattr(self, 'field_name', None) if name is not None: return '<%s: %s>' % (path, name) diff --git a/graphene/core/types.py b/graphene/core/types.py index fed216c4..e7b4e7fa 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -19,6 +19,9 @@ class ObjectTypeMeta(type): def is_interface(cls, parents): return Interface in parents + def is_mutation(cls, parents): + return Mutation in parents + def __new__(cls, name, bases, attrs): super_new = super(ObjectTypeMeta, cls).__new__ parents = [b for b in bases if isinstance(b, cls)] @@ -44,6 +47,10 @@ class ObjectTypeMeta(type): new_class.add_to_class('_meta', new_class.options_cls(meta)) new_class._meta.interface = new_class.is_interface(parents) + new_class._meta.mutation = new_class.is_mutation(parents) + + assert not (new_class._meta.interface and new_class._meta.mutation) + # Add all attributes to the class. for obj_name, obj in attrs.items(): new_class.add_to_class(obj_name, obj) @@ -155,5 +162,9 @@ class ObjectType(six.with_metaclass(ObjectTypeMeta, BaseObjectType)): pass +class Mutation(six.with_metaclass(ObjectTypeMeta, BaseObjectType)): + pass + + class Interface(six.with_metaclass(ObjectTypeMeta, BaseObjectType)): pass diff --git a/graphene/relay/types.py b/graphene/relay/types.py index 50e43e9c..fce02aee 100644 --- a/graphene/relay/types.py +++ b/graphene/relay/types.py @@ -15,9 +15,9 @@ def get_node_type(schema, obj): return obj.internal_type(schema) -def get_node(schema, globalId, *args): - resolvedGlobalId = from_global_id(globalId) - _type, _id = resolvedGlobalId.type, resolvedGlobalId.id +def get_node(schema, global_id, *args): + resolved_global_id = from_global_id(global_id) + _type, _id = resolved_global_id.type, resolved_global_id.id object_type = schema.get_type(_type) if not object_type or not issubclass(object_type, BaseNode): raise Exception("The type %s is not a Node" % _type) diff --git a/tests/contrib_django/test_urls.py b/tests/contrib_django/test_urls.py index 6d0bca57..6b95105c 100644 --- a/tests/contrib_django/test_urls.py +++ b/tests/contrib_django/test_urls.py @@ -2,6 +2,7 @@ from django.conf.urls import url from graphene.contrib.django.views import GraphQLView +import graphene from graphene import Schema from graphene.contrib.django.types import ( DjangoNode, @@ -20,9 +21,14 @@ class Character(DjangoNode): class Human(DjangoNode): + raises = graphene.StringField() + class Meta: model = Article + def resolve_raises(self, *args): + raise Exception("This field should raise exception") + def get_node(self, id): pass diff --git a/tests/contrib_django/test_views.py b/tests/contrib_django/test_views.py index be689062..435bcdf8 100644 --- a/tests/contrib_django/test_views.py +++ b/tests/contrib_django/test_views.py @@ -72,6 +72,14 @@ def test_client_get_good_query(settings, client): assert json_response == expected_json +def test_client_get_good_query_with_raise(settings, client): + settings.ROOT_URLCONF = 'tests.contrib_django.test_urls' + response = client.get('/graphql', {'query': '{ raises }'}) + json_response = format_response(response) + assert json_response['errors'][0]['message'] == 'This field should raise exception' + assert json_response['data']['raises'] is None + + def test_client_post_good_query(settings, client): settings.ROOT_URLCONF = 'tests.contrib_django.test_urls' response = client.post('/graphql', json.dumps({'query': '{ headline }'}), 'application/json') diff --git a/tests/core/test_fields.py b/tests/core/test_fields.py index 68497843..bb644306 100644 --- a/tests/core/test_fields.py +++ b/tests/core/test_fields.py @@ -4,6 +4,7 @@ from pytest import raises from graphene.core.fields import ( Field, StringField, + NonNullField ) from graphene.core.options import Options @@ -27,6 +28,9 @@ class ObjectType(object): def can_resolve(self, *args): return True + def __str__(self): + return "ObjectType" + ot = ObjectType() ObjectType._meta.contribute_to_class(ObjectType, '_meta') @@ -69,6 +73,12 @@ def test_stringfield_type(): assert f.internal_type(schema) == GraphQLString +def test_nonnullfield_type(): + f = NonNullField(StringField()) + f.contribute_to_class(ot, 'field_name') + assert isinstance(f.internal_type(schema), GraphQLNonNull) + + def test_stringfield_type_required(): f = StringField(required=True) f.contribute_to_class(ot, 'field_name') @@ -108,3 +118,61 @@ def test_field_resolve_type_custom(): f.contribute_to_class(ot, 'field_name') field_type = f.get_object_type(s) assert field_type == ot + + +def test_field_orders(): + f1 = Field(None) + f2 = Field(None) + assert f1 < f2 + + +def test_field_orders_wrong_type(): + field = Field(None) + try: + assert not field < 1 + except TypeError: + # Fix exception raising in Python3+ + pass + + +def test_field_eq(): + f1 = Field(None) + f2 = Field(None) + assert f1 != f2 + + +def test_field_eq_wrong_type(): + field = Field(None) + assert field != 1 + + +def test_field_hash(): + f1 = Field(None) + f2 = Field(None) + assert hash(f1) != hash(f2) + + +def test_field_none_type_raises_error(): + s = Schema() + f = Field(None) + f.contribute_to_class(ot, 'field_name') + with raises(Exception) as excinfo: + f.internal_field(s) + assert str(excinfo.value) == "Internal type for field ObjectType.field_name is None" + + +def test_field_str(): + f = StringField() + f.contribute_to_class(ot, 'field_name') + assert str(f) == "ObjectType.field_name" + + +def test_field_repr(): + f = StringField() + assert repr(f) == "" + + +def test_field_repr_contributed(): + f = StringField() + f.contribute_to_class(ot, 'field_name') + assert repr(f) == "" diff --git a/tests/core/test_scalars.py b/tests/core/test_scalars.py new file mode 100644 index 00000000..654e99aa --- /dev/null +++ b/tests/core/test_scalars.py @@ -0,0 +1,18 @@ +from graphene.core.scalars import ( + GraphQLSkipField +) + + +def test_skipfield_serialize(): + f = GraphQLSkipField + assert f.serialize('a') is None + + +def test_skipfield_parse_value(): + f = GraphQLSkipField + assert f.parse_value('a') is None + + +def test_skipfield_parse_literal(): + f = GraphQLSkipField + assert f.parse_literal('a') is None diff --git a/tests/core/test_schema.py b/tests/core/test_schema.py index 4d89d725..f6e3d849 100644 --- a/tests/core/test_schema.py +++ b/tests/core/test_schema.py @@ -109,3 +109,33 @@ def test_schema_get_type_map(): schema.schema.get_type_map().keys(), ['__Field', 'String', 'Pet', 'Character', '__InputValue', '__Directive', '__TypeKind', '__Schema', '__Type', 'Human', '__EnumValue', 'Boolean'] ) + + +def test_schema_no_query(): + schema = Schema(name='My own schema') + with raises(Exception) as excinfo: + schema.schema + assert 'define a base query type' in str(excinfo) + + +def test_schema_register(): + schema = Schema(name='My own schema') + + @schema.register + class MyType(ObjectType): + type = StringField(resolve=lambda *_: 'Dog') + + assert schema.get_type('MyType') == MyType + + +def test_schema_introspect(): + schema = Schema(name='My own schema') + + class MyType(ObjectType): + type = StringField(resolve=lambda *_: 'Dog') + + schema.query = MyType + + introspection = schema.introspect() + assert '__schema' in introspection + diff --git a/tests/relay/test_relayfields.py b/tests/relay/test_relayfields.py new file mode 100644 index 00000000..5285a219 --- /dev/null +++ b/tests/relay/test_relayfields.py @@ -0,0 +1,43 @@ +from pytest import raises + +import graphene +from graphene import relay + +schema = graphene.Schema() + + +class MyType(object): + name = 'my' + + +class MyNode(relay.Node): + name = graphene.StringField() + + @classmethod + def get_node(cls, id): + return MyNode(MyType()) + + +class Query(graphene.ObjectType): + my_node = relay.NodeField(MyNode) + + +schema.query = Query + + +def test_nodefield_query(): + query = ''' + query RebelsShipsQuery { + myNode(id:"TXlOb2RlOjE=") { + name + } + } + ''' + expected = { + 'myNode': { + 'name': 'my' + } + } + result = schema.execute(query) + assert not result.errors + assert result.data == expected diff --git a/tests/starwars_django/data.py b/tests/starwars_django/data.py index cb43c29b..bf88b7a9 100644 --- a/tests/starwars_django/data.py +++ b/tests/starwars_django/data.py @@ -1,18 +1,28 @@ -from collections import namedtuple - -from .models import Ship, Faction +from .models import Ship, Faction, Character def initialize(): + human = Character( + name='Human' + ) + human.save() + + droid = Character( + name='Droid' + ) + droid.save() + rebels = Faction( id='1', name='Alliance to Restore the Republic', + hero=human ) rebels.save() empire = Faction( id='2', name='Galactic Empire', + hero=droid ) empire.save() diff --git a/tests/starwars_django/models.py b/tests/starwars_django/models.py index 6afa152e..10050c33 100644 --- a/tests/starwars_django/models.py +++ b/tests/starwars_django/models.py @@ -2,8 +2,16 @@ from __future__ import absolute_import from django.db import models +class Character(models.Model): + name = models.CharField(max_length=50) + + def __str__(self): + return self.name + + class Faction(models.Model): name = models.CharField(max_length=50) + hero = models.ForeignKey(Character) def __str__(self): return self.name diff --git a/tests/starwars_django/schema.py b/tests/starwars_django/schema.py index 0bde6826..1949da6a 100644 --- a/tests/starwars_django/schema.py +++ b/tests/starwars_django/schema.py @@ -4,7 +4,8 @@ from graphene.contrib.django import ( DjangoObjectType, DjangoNode ) -from .models import Ship as ShipModel, Faction as FactionModel +from .models import ( + Ship as ShipModel, Faction as FactionModel, Character as CharacterModel) from .data import ( getFaction, getShip, @@ -17,7 +18,6 @@ schema = graphene.Schema(name='Starwars Django Relay Schema') class Ship(DjangoNode): - class Meta: model = ShipModel @@ -26,8 +26,13 @@ class Ship(DjangoNode): return Ship(getShip(id)) -class Faction(DjangoNode): +@schema.register +class CharacterModel(DjangoObjectType): + class Meta: + model = CharacterModel + +class Faction(DjangoNode): class Meta: model = FactionModel diff --git a/tests/starwars_django/test_connections.py b/tests/starwars_django/test_connections.py index e7181dc9..9992f0f3 100644 --- a/tests/starwars_django/test_connections.py +++ b/tests/starwars_django/test_connections.py @@ -14,6 +14,9 @@ def test_correct_fetch_first_ship_rebels(): query RebelsShipsQuery { rebels { name, + hero { + name + } ships(first: 1) { edges { node { @@ -27,6 +30,9 @@ def test_correct_fetch_first_ship_rebels(): expected = { 'rebels': { 'name': 'Alliance to Restore the Republic', + 'hero': { + 'name': 'Human' + }, 'ships': { 'edges': [ { From bad3dd4a4e908003d610b3aabce461906dc050d5 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 10 Oct 2015 15:20:52 -0700 Subject: [PATCH 77/77] Improved Readme --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d2c6e4a7..3aa1dcc1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ -# Graphene [![Build Status](https://travis-ci.org/graphql-python/graphene.svg?branch=master)](https://travis-ci.org/graphql-python/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master) +# ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene](http://graphene-python.org) [![Build Status](https://travis-ci.org/graphql-python/graphene.svg?branch=master)](https://travis-ci.org/graphql-python/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master) -Graphene is a Python library for creating GraphQL schemas/types easly. -It maps the models/fields to internal GraphQL objects without effort. -Including automatic [Django models](#djangorelay-schema) conversion. + +Graphene is a Python library for building GraphQL schemas/types fast and easily. +* **Easy to use:** It maps the models/fields to internal GraphQL objects without effort. +* **Relay:** Graphene has builtin support for Relay +* **Django:** Automatic [Django models](#djangorelay-schema) conversion. *See an [example Django](http://github.com/graphql-python/swapi-graphene) implementation* ## Installation