From 9a84d595a1e5d151ee0e41400d2cce63fb08f974 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 25 Sep 2015 16:35:17 -0700 Subject: [PATCH] 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