From 2e8707aee66a83b4ac91ca2b33c26624cd1e0ea7 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 28 Sep 2015 00:34:25 -0700 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 04/16] 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 05/16] 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 06/16] 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 2faa8223e8df7f5ad992fc0554a6869c0c32bef8 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 29 Sep 2015 01:18:32 -0700 Subject: [PATCH 07/16] 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 08/16] 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 e89eb3456e0614c5c45306e3cf0da3aed4212c60 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 29 Sep 2015 18:25:56 -0700 Subject: [PATCH 09/16] 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 10/16] 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 11/16] 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 12/16] 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 13/16] 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 14/16] 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 15/16] 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 16/16] 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):