From 14439155eec40c72afcaa42651c2d3345246edb6 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 3 Dec 2015 22:15:09 -0800 Subject: [PATCH 01/26] Initial version debug schema in django --- graphene/contrib/django/debug/__init__.py | 4 + graphene/contrib/django/debug/schema.py | 68 ++++++++ graphene/contrib/django/debug/sql/__init__.py | 0 graphene/contrib/django/debug/sql/tracking.py | 165 ++++++++++++++++++ graphene/contrib/django/debug/sql/types.py | 19 ++ .../contrib/django/debug/tests/__init__.py | 0 .../contrib/django/debug/tests/test_query.py | 70 ++++++++ graphene/contrib/django/debug/types.py | 7 + 8 files changed, 333 insertions(+) create mode 100644 graphene/contrib/django/debug/__init__.py create mode 100644 graphene/contrib/django/debug/schema.py create mode 100644 graphene/contrib/django/debug/sql/__init__.py create mode 100644 graphene/contrib/django/debug/sql/tracking.py create mode 100644 graphene/contrib/django/debug/sql/types.py create mode 100644 graphene/contrib/django/debug/tests/__init__.py create mode 100644 graphene/contrib/django/debug/tests/test_query.py create mode 100644 graphene/contrib/django/debug/types.py diff --git a/graphene/contrib/django/debug/__init__.py b/graphene/contrib/django/debug/__init__.py new file mode 100644 index 00000000..0636c3fa --- /dev/null +++ b/graphene/contrib/django/debug/__init__.py @@ -0,0 +1,4 @@ +from .schema import DebugSchema +from .types import DjangoDebug + +__all__ = ['DebugSchema', 'DjangoDebug'] diff --git a/graphene/contrib/django/debug/schema.py b/graphene/contrib/django/debug/schema.py new file mode 100644 index 00000000..443f2545 --- /dev/null +++ b/graphene/contrib/django/debug/schema.py @@ -0,0 +1,68 @@ +from django.db import connections + +from ....core.schema import Schema +from ....core.types import Field +from .sql.tracking import unwrap_cursor, wrap_cursor +from .sql.types import DjangoDebugSQL +from .types import DjangoDebug + + +class WrappedRoot(object): + + def __init__(self, root): + self._recorded = [] + self._root = root + + def record(self, **log): + self._recorded.append(DjangoDebugSQL(**log)) + + def debug(self): + return DjangoDebug(sql=self._recorded) + + +class WrapRoot(object): + + @property + def _root(self): + return self._wrapped_root.root + + @_root.setter + def _root(self, value): + self._wrapped_root = value + + def resolve_debug(self, args, info): + return self._wrapped_root.debug() + + +def debug_objecttype(objecttype): + return type('Debug{}'.format(objecttype._meta.type_name), (WrapRoot, objecttype), {'debug': Field(DjangoDebug, name='__debug')}) + + +class DebugSchema(Schema): + + @property + def query(self): + if not self._query: + return + return debug_objecttype(self._query) + + @query.setter + def query(self, value): + self._query = value + + def enable_instrumentation(self, wrapped_root): + # This is thread-safe because database connections are thread-local. + for connection in connections.all(): + wrap_cursor(connection, wrapped_root) + + def disable_instrumentation(self): + for connection in connections.all(): + unwrap_cursor(connection) + + def execute(self, *args, **kwargs): + root = kwargs.pop('root', object()) + wrapped_root = WrappedRoot(root=root) + self.enable_instrumentation(wrapped_root) + result = super(DebugSchema, self).execute(root=wrapped_root, *args, **kwargs) + self.disable_instrumentation() + return result diff --git a/graphene/contrib/django/debug/sql/__init__.py b/graphene/contrib/django/debug/sql/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/contrib/django/debug/sql/tracking.py b/graphene/contrib/django/debug/sql/tracking.py new file mode 100644 index 00000000..487b092c --- /dev/null +++ b/graphene/contrib/django/debug/sql/tracking.py @@ -0,0 +1,165 @@ +# Code obtained from django-debug-toolbar sql panel tracking +from __future__ import absolute_import, unicode_literals + +import json +from threading import local +from time import time + +from django.utils import six +from django.utils.encoding import force_text + + +class SQLQueryTriggered(Exception): + """Thrown when template panel triggers a query""" + + +class ThreadLocalState(local): + + def __init__(self): + self.enabled = True + + @property + def Wrapper(self): + if self.enabled: + return NormalCursorWrapper + return ExceptionCursorWrapper + + def recording(self, v): + self.enabled = v + + +state = ThreadLocalState() +recording = state.recording # export function + + +def wrap_cursor(connection, panel): + if not hasattr(connection, '_djdt_cursor'): + connection._djdt_cursor = connection.cursor + + def cursor(): + return state.Wrapper(connection._djdt_cursor(), connection, panel) + + connection.cursor = cursor + return cursor + + +def unwrap_cursor(connection): + if hasattr(connection, '_djdt_cursor'): + del connection._djdt_cursor + del connection.cursor + + +class ExceptionCursorWrapper(object): + """ + Wraps a cursor and raises an exception on any operation. + Used in Templates panel. + """ + + def __init__(self, cursor, db, logger): + pass + + def __getattr__(self, attr): + raise SQLQueryTriggered() + + +class NormalCursorWrapper(object): + """ + Wraps a cursor and logs queries. + """ + + def __init__(self, cursor, db, logger): + self.cursor = cursor + # Instance of a BaseDatabaseWrapper subclass + self.db = db + # logger must implement a ``record`` method + self.logger = logger + + def _quote_expr(self, element): + if isinstance(element, six.string_types): + return "'%s'" % force_text(element).replace("'", "''") + else: + return repr(element) + + def _quote_params(self, params): + if not params: + return params + if isinstance(params, dict): + return dict((key, self._quote_expr(value)) + for key, value in params.items()) + return list(map(self._quote_expr, params)) + + def _decode(self, param): + try: + return force_text(param, strings_only=True) + except UnicodeDecodeError: + return '(encoded string)' + + def _record(self, method, sql, params): + start_time = time() + try: + return method(sql, params) + finally: + stop_time = time() + duration = (stop_time - start_time) * 1000 + _params = '' + try: + _params = json.dumps(list(map(self._decode, params))) + except Exception: + pass # object not JSON serializable + + alias = getattr(self.db, 'alias', 'default') + conn = self.db.connection + vendor = getattr(conn, 'vendor', 'unknown') + + params = { + 'vendor': vendor, + 'alias': alias, + 'sql': self.db.ops.last_executed_query( + self.cursor, sql, self._quote_params(params)), + 'duration': duration, + 'raw_sql': sql, + 'params': _params, + 'start_time': start_time, + 'stop_time': stop_time, + 'is_slow': duration > 10, + 'is_select': sql.lower().strip().startswith('select'), + } + + if vendor == 'postgresql': + # If an erroneous query was ran on the connection, it might + # be in a state where checking isolation_level raises an + # exception. + try: + iso_level = conn.isolation_level + except conn.InternalError: + iso_level = 'unknown' + params.update({ + 'trans_id': self.logger.get_transaction_id(alias), + 'trans_status': conn.get_transaction_status(), + 'iso_level': iso_level, + 'encoding': conn.encoding, + }) + + # We keep `sql` to maintain backwards compatibility + self.logger.record(**params) + + def callproc(self, procname, params=()): + return self._record(self.cursor.callproc, procname, params) + + def execute(self, sql, params=()): + return self._record(self.cursor.execute, sql, params) + + def executemany(self, sql, param_list): + return self._record(self.cursor.executemany, sql, param_list) + + def __getattr__(self, attr): + return getattr(self.cursor, attr) + + def __iter__(self): + return iter(self.cursor) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() diff --git a/graphene/contrib/django/debug/sql/types.py b/graphene/contrib/django/debug/sql/types.py new file mode 100644 index 00000000..7ee1894f --- /dev/null +++ b/graphene/contrib/django/debug/sql/types.py @@ -0,0 +1,19 @@ +from .....core import Float, ObjectType, String + + +class DjangoDebugSQL(ObjectType): + vendor = String() + alias = String() + sql = String() + duration = Float() + raw_sql = String() + params = String() + start_time = Float() + stop_time = Float() + is_slow = String() + is_select = String() + + trans_id = String() + trans_status = String() + iso_level = String() + encoding = String() diff --git a/graphene/contrib/django/debug/tests/__init__.py b/graphene/contrib/django/debug/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/contrib/django/debug/tests/test_query.py b/graphene/contrib/django/debug/tests/test_query.py new file mode 100644 index 00000000..3b4de477 --- /dev/null +++ b/graphene/contrib/django/debug/tests/test_query.py @@ -0,0 +1,70 @@ +import pytest + +import graphene +from graphene.contrib.django import DjangoObjectType + +from ...tests.models import Reporter +from ..schema import DebugSchema + +# from examples.starwars_django.models import Character + +pytestmark = pytest.mark.django_db + + +def test_should_query_well(): + r1 = Reporter(last_name='ABA') + r1.save() + r2 = Reporter(last_name='Griffin') + r2.save() + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + + class Query(graphene.ObjectType): + reporter = graphene.Field(ReporterType) + all_reporters = ReporterType.List + + def resolve_all_reporters(self, *args, **kwargs): + return Reporter.objects.all() + + def resolve_reporter(self, *args, **kwargs): + return Reporter.objects.first() + + query = ''' + query ReporterQuery { + reporter { + lastName + } + allReporters { + lastName + } + __debug { + sql { + rawSql + } + } + } + ''' + expected = { + 'reporter': { + 'lastName': 'ABA', + }, + 'allReporters': [{ + 'lastName': 'ABA', + }, { + 'lastName': 'Griffin', + }], + '__debug': { + 'sql': [{ + 'rawSql': str(Reporter.objects.order_by('pk')[:1].query) + }, { + 'rawSql': str(Reporter.objects.all().query) + }] + } + } + schema = DebugSchema(query=Query) + result = schema.execute(query) + assert not result.errors + assert result.data == expected diff --git a/graphene/contrib/django/debug/types.py b/graphene/contrib/django/debug/types.py new file mode 100644 index 00000000..d84cbb98 --- /dev/null +++ b/graphene/contrib/django/debug/types.py @@ -0,0 +1,7 @@ +from ....core.classtypes.objecttype import ObjectType +from ....core.types import Field +from .sql.types import DjangoDebugSQL + + +class DjangoDebug(ObjectType): + sql = Field(DjangoDebugSQL.List) From bee0af11251e93fd45c2c83176e63b3506623c61 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 4 Dec 2015 02:34:12 -0800 Subject: [PATCH 02/26] Improved django debug --- graphene/contrib/django/debug/schema.py | 11 ++++------- graphene/contrib/django/debug/sql/tracking.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/graphene/contrib/django/debug/schema.py b/graphene/contrib/django/debug/schema.py index 443f2545..2726f3fb 100644 --- a/graphene/contrib/django/debug/schema.py +++ b/graphene/contrib/django/debug/schema.py @@ -42,13 +42,11 @@ class DebugSchema(Schema): @property def query(self): - if not self._query: - return - return debug_objecttype(self._query) + return self._query @query.setter def query(self, value): - self._query = value + self._query = value and debug_objecttype(value) def enable_instrumentation(self, wrapped_root): # This is thread-safe because database connections are thread-local. @@ -59,10 +57,9 @@ class DebugSchema(Schema): for connection in connections.all(): unwrap_cursor(connection) - def execute(self, *args, **kwargs): - root = kwargs.pop('root', object()) + def execute(self, query, root=None, *args, **kwargs): wrapped_root = WrappedRoot(root=root) self.enable_instrumentation(wrapped_root) - result = super(DebugSchema, self).execute(root=wrapped_root, *args, **kwargs) + result = super(DebugSchema, self).execute(query, wrapped_root, *args, **kwargs) self.disable_instrumentation() return result diff --git a/graphene/contrib/django/debug/sql/tracking.py b/graphene/contrib/django/debug/sql/tracking.py index 487b092c..8ed40492 100644 --- a/graphene/contrib/django/debug/sql/tracking.py +++ b/graphene/contrib/django/debug/sql/tracking.py @@ -100,7 +100,7 @@ class NormalCursorWrapper(object): return method(sql, params) finally: stop_time = time() - duration = (stop_time - start_time) * 1000 + duration = (stop_time - start_time) _params = '' try: _params = json.dumps(list(map(self._decode, params))) From 3586fdfb77bc544f0c493aa5d376f2420f9cee7f Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 4 Dec 2015 02:41:39 -0800 Subject: [PATCH 03/26] Improvex syntax --- graphene/contrib/django/debug/schema.py | 5 ++++- graphene/relay/types.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/graphene/contrib/django/debug/schema.py b/graphene/contrib/django/debug/schema.py index 2726f3fb..e5e5f30b 100644 --- a/graphene/contrib/django/debug/schema.py +++ b/graphene/contrib/django/debug/schema.py @@ -35,7 +35,10 @@ class WrapRoot(object): def debug_objecttype(objecttype): - return type('Debug{}'.format(objecttype._meta.type_name), (WrapRoot, objecttype), {'debug': Field(DjangoDebug, name='__debug')}) + return type( + 'Debug{}'.format(objecttype._meta.type_name), + (WrapRoot, objecttype), + {'debug': Field(DjangoDebug, name='__debug')}) class DebugSchema(Schema): diff --git a/graphene/relay/types.py b/graphene/relay/types.py index 672042e7..425e3038 100644 --- a/graphene/relay/types.py +++ b/graphene/relay/types.py @@ -4,6 +4,7 @@ from collections import Iterable from functools import wraps import six + from graphql_relay.connection.arrayconnection import connection_from_list from graphql_relay.node.node import to_global_id From fae376cbb08978a80fb231998bb744b687bad26b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 6 Dec 2015 01:01:23 -0800 Subject: [PATCH 04/26] Moved arguments to a named group --- .../core/classtypes/tests/test_mutation.py | 2 +- graphene/core/schema.py | 5 +++- graphene/core/types/argument.py | 24 +++------------ graphene/core/types/base.py | 30 ++++++++++++++++++- graphene/core/types/field.py | 6 ++-- graphene/core/types/tests/test_field.py | 3 +- graphene/relay/tests/test_mutations.py | 3 +- 7 files changed, 44 insertions(+), 29 deletions(-) diff --git a/graphene/core/classtypes/tests/test_mutation.py b/graphene/core/classtypes/tests/test_mutation.py index ac32585e..85dd2368 100644 --- a/graphene/core/classtypes/tests/test_mutation.py +++ b/graphene/core/classtypes/tests/test_mutation.py @@ -24,4 +24,4 @@ def test_mutation(): assert list(object_type.get_fields().keys()) == ['name'] assert MyMutation._meta.fields_map['name'].object_type == MyMutation assert isinstance(MyMutation.arguments, ArgumentsGroup) - assert 'argName' in MyMutation.arguments + assert 'argName' in schema.T(MyMutation.arguments) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 1b0ce8f9..2eea83ea 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -38,6 +38,9 @@ class Schema(object): def __repr__(self): return '' % (str(self.name), hash(self)) + def get_internal_type(self, objecttype): + return objecttype.internal_type(self) + def T(self, object_type): if not object_type: return @@ -45,7 +48,7 @@ class Schema(object): object_type, (BaseType, ClassType)) or isinstance( object_type, BaseType): if object_type not in self._types: - internal_type = object_type.internal_type(self) + internal_type = self.get_internal_type(object_type) self._types[object_type] = internal_type is_objecttype = inspect.isclass( object_type) and issubclass(object_type, ClassType) diff --git a/graphene/core/types/argument.py b/graphene/core/types/argument.py index 0892c446..0ef7686a 100644 --- a/graphene/core/types/argument.py +++ b/graphene/core/types/argument.py @@ -5,10 +5,10 @@ from itertools import chain from graphql.core.type import GraphQLArgument from ...utils import ProxySnakeDict, to_camel_case -from .base import ArgumentType, BaseType, OrderedType +from .base import ArgumentType, GroupNamedType, NamedType, OrderedType -class Argument(OrderedType): +class Argument(NamedType, OrderedType): def __init__(self, type, description=None, default=None, name=None, _creation_counter=None): @@ -27,27 +27,11 @@ class Argument(OrderedType): return self.name -class ArgumentsGroup(BaseType): +class ArgumentsGroup(GroupNamedType): def __init__(self, *args, **kwargs): arguments = to_arguments(*args, **kwargs) - self.arguments = OrderedDict([(arg.name, arg) for arg in arguments]) - - def internal_type(self, schema): - return OrderedDict([(arg.name, schema.T(arg)) - for arg in self.arguments.values()]) - - def __len__(self): - return len(self.arguments) - - def __iter__(self): - return iter(self.arguments) - - def __contains__(self, *args): - return self.arguments.__contains__(*args) - - def __getitem__(self, *args): - return self.arguments.__getitem__(*args) + super(ArgumentsGroup, self).__init__(*arguments) def to_arguments(*args, **kwargs): diff --git a/graphene/core/types/base.py b/graphene/core/types/base.py index 2b4078e4..920963b6 100644 --- a/graphene/core/types/base.py +++ b/graphene/core/types/base.py @@ -1,4 +1,5 @@ -from functools import total_ordering +from collections import OrderedDict +from functools import total_ordering, partial import six @@ -126,3 +127,30 @@ class FieldType(MirroredType): class MountedType(FieldType, ArgumentType): pass + + +class NamedType(BaseType): + pass + + +class GroupNamedType(BaseType): + def __init__(self, *types): + self.types = types + + def get_named_type(self, schema, type): + return type.name or type.attname, schema.T(type) + + def internal_type(self, schema): + return OrderedDict(map(partial(self.get_named_type, schema), self.types)) + + def __len__(self): + return len(self.types) + + def __iter__(self): + return iter(self.types) + + def __contains__(self, *args): + return self.types.__contains__(*args) + + def __getitem__(self, *args): + return self.types.__getitem__(*args) diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index c3fa712f..3be90c74 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -9,11 +9,11 @@ from ..classtypes.base import FieldsClassType from ..classtypes.inputobjecttype import InputObjectType from ..classtypes.mutation import Mutation from .argument import ArgumentsGroup, snake_case_args -from .base import LazyType, MountType, OrderedType +from .base import LazyType, NamedType, MountType, OrderedType from .definitions import NonNull -class Field(OrderedType): +class Field(NamedType, OrderedType): def __init__( self, type, description=None, args=None, name=None, resolver=None, @@ -117,7 +117,7 @@ class Field(OrderedType): return hash((self.creation_counter, self.object_type)) -class InputField(OrderedType): +class InputField(NamedType, OrderedType): def __init__(self, type, description=None, default=None, name=None, _creation_counter=None, required=False): diff --git a/graphene/core/types/tests/test_field.py b/graphene/core/types/tests/test_field.py index 8253ed20..bb0bcf2c 100644 --- a/graphene/core/types/tests/test_field.py +++ b/graphene/core/types/tests/test_field.py @@ -98,9 +98,10 @@ def test_field_string_reference(): def test_field_custom_arguments(): field = Field(None, name='my_customName', p=String()) + schema = Schema() args = field.arguments - assert 'p' in args + assert 'p' in schema.T(args) def test_inputfield_internal_type(): diff --git a/graphene/relay/tests/test_mutations.py b/graphene/relay/tests/test_mutations.py index 4356a1ec..02287725 100644 --- a/graphene/relay/tests/test_mutations.py +++ b/graphene/relay/tests/test_mutations.py @@ -34,8 +34,7 @@ schema = Schema(query=Query, mutation=MyResultMutation) def test_mutation_arguments(): assert ChangeNumber.arguments - assert list(ChangeNumber.arguments) == ['input'] - assert 'input' in ChangeNumber.arguments + assert 'input' in schema.T(ChangeNumber.arguments) inner_type = ChangeNumber.input_type client_mutation_id_field = inner_type._meta.fields_map[ 'client_mutation_id'] From 21dffa4aa8ac2d976aa9208b34601f94418046fb Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 6 Dec 2015 01:34:56 -0800 Subject: [PATCH 05/26] Moved fields to a named group --- graphene/core/classtypes/base.py | 15 ++++++--------- graphene/core/types/field.py | 14 +++++++++++++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/graphene/core/classtypes/base.py b/graphene/core/classtypes/base.py index 4f2de009..9ab51427 100644 --- a/graphene/core/classtypes/base.py +++ b/graphene/core/classtypes/base.py @@ -5,7 +5,6 @@ from collections import OrderedDict import six -from ..exceptions import SkipField from .options import Options @@ -82,6 +81,11 @@ class FieldsOptions(Options): def fields_map(self): return OrderedDict([(f.attname, f) for f in self.fields]) + @property + def fields_group_type(self): + from ..types.field import FieldsGroupType + return FieldsGroupType(*self.local_fields) + class FieldsClassTypeMeta(ClassTypeMeta): options_class = FieldsOptions @@ -124,11 +128,4 @@ class FieldsClassType(six.with_metaclass(FieldsClassTypeMeta, ClassType)): @classmethod def fields_internal_types(cls, schema): - fields = [] - for field in cls._meta.fields: - try: - fields.append((field.name, schema.T(field))) - except SkipField: - continue - - return OrderedDict(fields) + return schema.T(cls._meta.fields_group_type) diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index 3be90c74..9f2c3f01 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -8,8 +8,9 @@ from ...utils import to_camel_case from ..classtypes.base import FieldsClassType from ..classtypes.inputobjecttype import InputObjectType from ..classtypes.mutation import Mutation +from ..exceptions import SkipField from .argument import ArgumentsGroup, snake_case_args -from .base import LazyType, NamedType, MountType, OrderedType +from .base import LazyType, NamedType, MountType, OrderedType, GroupNamedType from .definitions import NonNull @@ -146,3 +147,14 @@ class InputField(NamedType, OrderedType): return GraphQLInputObjectField( schema.T(self.type), default_value=self.default, description=self.description) + + +class FieldsGroupType(GroupNamedType): + def internal_type(self, schema): + fields = [] + for field in sorted(self.types): + try: + fields.append(self.get_named_type(schema, field)) + except SkipField: + continue + return OrderedDict(fields) From ec3f29259389b94b294eb74efa0422d06c5727e5 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 6 Dec 2015 01:51:03 -0800 Subject: [PATCH 06/26] Refactored arguments and fields logic --- graphene/core/classtypes/base.py | 10 +++++----- graphene/core/tests/test_old_fields.py | 5 +++-- graphene/core/types/argument.py | 21 +++++++++++---------- graphene/core/types/base.py | 5 ++++- graphene/core/types/field.py | 5 ----- graphene/core/types/tests/test_argument.py | 4 ++-- graphene/core/types/tests/test_field.py | 4 ++-- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/graphene/core/classtypes/base.py b/graphene/core/classtypes/base.py index 9ab51427..9dafff0e 100644 --- a/graphene/core/classtypes/base.py +++ b/graphene/core/classtypes/base.py @@ -92,7 +92,7 @@ class FieldsClassTypeMeta(ClassTypeMeta): def extend_fields(cls, bases): new_fields = cls._meta.local_fields - field_names = {f.name: f for f in new_fields} + field_names = {f.attname: f for f in new_fields} for base in bases: if not isinstance(base, FieldsClassTypeMeta): @@ -100,17 +100,17 @@ class FieldsClassTypeMeta(ClassTypeMeta): parent_fields = base._meta.local_fields for field in parent_fields: - if field.name in field_names and field.type.__class__ != field_names[ - field.name].type.__class__: + if field.attname in field_names and field.type.__class__ != field_names[ + field.attname].type.__class__: raise Exception( 'Local field %r in class %r (%r) clashes ' 'with field with similar name from ' 'Interface %s (%r)' % ( - field.name, + field.attname, cls.__name__, field.__class__, base.__name__, - field_names[field.name].__class__) + field_names[field.attname].__class__) ) new_field = copy.copy(field) cls.add_to_class(field.attname, new_field) diff --git a/graphene/core/tests/test_old_fields.py b/graphene/core/tests/test_old_fields.py index 95bf9aab..3f24aedf 100644 --- a/graphene/core/tests/test_old_fields.py +++ b/graphene/core/tests/test_old_fields.py @@ -34,10 +34,11 @@ def test_field_type(): assert schema.T(f).type == GraphQLString -def test_field_name_automatic_camelcase(): +def test_field_name(): f = Field(GraphQLString) f.contribute_to_class(MyOt, 'field_name') - assert f.name == 'fieldName' + assert f.name is None + assert f.attname == 'field_name' def test_field_name_use_name_if_exists(): diff --git a/graphene/core/types/argument.py b/graphene/core/types/argument.py index 0ef7686a..a8fe343e 100644 --- a/graphene/core/types/argument.py +++ b/graphene/core/types/argument.py @@ -1,10 +1,9 @@ -from collections import OrderedDict from functools import wraps from itertools import chain from graphql.core.type import GraphQLArgument -from ...utils import ProxySnakeDict, to_camel_case +from ...utils import ProxySnakeDict from .base import ArgumentType, GroupNamedType, NamedType, OrderedType @@ -14,6 +13,7 @@ class Argument(NamedType, OrderedType): name=None, _creation_counter=None): super(Argument, self).__init__(_creation_counter=_creation_counter) self.name = name + self.attname = None self.type = type self.description = description self.default = default @@ -38,20 +38,21 @@ def to_arguments(*args, **kwargs): arguments = {} iter_arguments = chain(kwargs.items(), [(None, a) for a in args]) - for name, arg in iter_arguments: + for attname, arg in iter_arguments: if isinstance(arg, Argument): argument = arg elif isinstance(arg, ArgumentType): argument = arg.as_argument() else: - raise ValueError('Unknown argument %s=%r' % (name, arg)) + raise ValueError('Unknown argument %s=%r' % (attname, arg)) - if name: - argument.name = to_camel_case(name) - assert argument.name, 'Argument in field must have a name' - assert argument.name not in arguments, 'Found more than one Argument with same name {}'.format( - argument.name) - arguments[argument.name] = argument + if attname: + argument.attname = attname + + name = argument.name or argument.attname + assert name, 'Argument in field must have a name' + assert name not in arguments, 'Found more than one Argument with same name {}'.format(name) + arguments[name] = argument return sorted(arguments.values()) diff --git a/graphene/core/types/base.py b/graphene/core/types/base.py index 920963b6..96523c7b 100644 --- a/graphene/core/types/base.py +++ b/graphene/core/types/base.py @@ -3,6 +3,8 @@ from functools import total_ordering, partial import six +from ...utils import to_camel_case + class BaseType(object): @@ -138,7 +140,8 @@ class GroupNamedType(BaseType): self.types = types def get_named_type(self, schema, type): - return type.name or type.attname, schema.T(type) + name = type.name or to_camel_case(type.attname) + return name, schema.T(type) def internal_type(self, schema): return OrderedDict(map(partial(self.get_named_type, schema), self.types)) diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index 9f2c3f01..cfe168f7 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -4,7 +4,6 @@ from functools import wraps import six from graphql.core.type import GraphQLField, GraphQLInputObjectField -from ...utils import to_camel_case from ..classtypes.base import FieldsClassType from ..classtypes.inputobjecttype import InputObjectType from ..classtypes.mutation import Mutation @@ -37,8 +36,6 @@ class Field(NamedType, OrderedType): assert issubclass( cls, (FieldsClassType)), 'Field {} cannot be mounted in {}'.format( self, cls) - if not self.name: - self.name = to_camel_case(attname) self.attname = attname self.object_type = cls self.mount(cls) @@ -134,8 +131,6 @@ class InputField(NamedType, OrderedType): assert issubclass( cls, (InputObjectType)), 'InputField {} cannot be mounted in {}'.format( self, cls) - if not self.name: - self.name = to_camel_case(attname) self.attname = attname self.object_type = cls self.mount(cls) diff --git a/graphene/core/types/tests/test_argument.py b/graphene/core/types/tests/test_argument.py index 26bbb310..cf07a7d3 100644 --- a/graphene/core/types/tests/test_argument.py +++ b/graphene/core/types/tests/test_argument.py @@ -27,8 +27,8 @@ def test_to_arguments(): other_kwarg=String(), ) - assert [a.name for a in arguments] == [ - 'myArg', 'otherArg', 'myKwarg', 'otherKwarg'] + assert [a.name or a.attname for a in arguments] == [ + 'myArg', 'otherArg', 'my_kwarg', 'other_kwarg'] def test_to_arguments_no_name(): diff --git a/graphene/core/types/tests/test_field.py b/graphene/core/types/tests/test_field.py index bb0bcf2c..7bb23ca9 100644 --- a/graphene/core/types/tests/test_field.py +++ b/graphene/core/types/tests/test_field.py @@ -20,7 +20,7 @@ def test_field_internal_type(): schema = Schema(query=Query) type = schema.T(field) - assert field.name == 'myField' + assert field.name is None assert field.attname == 'my_field' assert isinstance(type, GraphQLField) assert type.description == 'My argument' @@ -116,7 +116,7 @@ def test_inputfield_internal_type(): schema = Schema(query=MyObjectType) type = schema.T(field) - assert field.name == 'myField' + assert field.name is None assert field.attname == 'my_field' assert isinstance(type, GraphQLInputObjectField) assert type.description == 'My input field' From 12e4e2c0068672aac2b1bfd94e0bd42a155b1917 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 6 Dec 2015 02:35:17 -0800 Subject: [PATCH 07/26] Added Plugins structure. Added option in schema for auto camel case or not. --- graphene/core/schema.py | 16 +++++++++++++++- graphene/core/types/base.py | 2 +- graphene/plugins/__init__.py | 6 ++++++ graphene/plugins/base.py | 6 ++++++ graphene/plugins/camel_case.py | 22 ++++++++++++++++++++++ 5 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 graphene/plugins/__init__.py create mode 100644 graphene/plugins/base.py create mode 100644 graphene/plugins/camel_case.py diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 2eea83ea..4da067e4 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -12,6 +12,7 @@ from graphene import signals from .classtypes.base import ClassType from .types.base import BaseType +from ..plugins import Plugin, CamelCase class GraphQLSchema(_GraphQLSchema): @@ -25,7 +26,7 @@ class Schema(object): _executor = None def __init__(self, query=None, mutation=None, subscription=None, - name='Schema', executor=None): + name='Schema', executor=None, plugins=None, auto_camelcase=True): self._types_names = {} self._types = {} self.mutation = mutation @@ -33,12 +34,25 @@ class Schema(object): self.subscription = subscription self.name = name self.executor = executor + self.plugins = [] + plugins = plugins or [] + if auto_camelcase: + plugins.append(CamelCase()) + for plugin in plugins: + self.add_plugin(plugin) signals.init_schema.send(self) def __repr__(self): return '' % (str(self.name), hash(self)) + def add_plugin(self, plugin): + assert isinstance(plugin, Plugin), 'A plugin need to subclass graphene.Plugin and be instantiated' + plugin.contribute_to_schema(self) + self.plugins.append(plugin) + def get_internal_type(self, objecttype): + for plugin in self.plugins: + objecttype = plugin.transform_type(objecttype) return objecttype.internal_type(self) def T(self, object_type): diff --git a/graphene/core/types/base.py b/graphene/core/types/base.py index 96523c7b..94e8a75c 100644 --- a/graphene/core/types/base.py +++ b/graphene/core/types/base.py @@ -140,7 +140,7 @@ class GroupNamedType(BaseType): self.types = types def get_named_type(self, schema, type): - name = type.name or to_camel_case(type.attname) + name = type.name or type.attname return name, schema.T(type) def internal_type(self, schema): diff --git a/graphene/plugins/__init__.py b/graphene/plugins/__init__.py new file mode 100644 index 00000000..5a9bf26b --- /dev/null +++ b/graphene/plugins/__init__.py @@ -0,0 +1,6 @@ +from .base import Plugin +from .camel_case import CamelCase + +__all__ = [ + 'Plugin', 'CamelCase' +] diff --git a/graphene/plugins/base.py b/graphene/plugins/base.py new file mode 100644 index 00000000..557099c7 --- /dev/null +++ b/graphene/plugins/base.py @@ -0,0 +1,6 @@ +class Plugin(object): + def contribute_to_schema(self, schema): + self.schema = schema + + def transform_type(self, objecttype): + return objecttype diff --git a/graphene/plugins/camel_case.py b/graphene/plugins/camel_case.py new file mode 100644 index 00000000..cba39ad5 --- /dev/null +++ b/graphene/plugins/camel_case.py @@ -0,0 +1,22 @@ +from .base import Plugin + +from ..core.types.base import GroupNamedType +from ..utils import memoize, to_camel_case + + +def camelcase_named_type(schema, type): + name = type.name or to_camel_case(type.attname) + return name, schema.T(type) + + +class CamelCase(Plugin): + @memoize + def transform_group(self, _type): + new_type = _type.__class__(*_type.types) + setattr(new_type, 'get_named_type', camelcase_named_type) + return new_type + + def transform_type(self, _type): + if isinstance(_type, GroupNamedType): + return self.transform_group(_type) + return _type From a153a01f6ba7e5805bed10ec6867961d673835bc Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 6 Dec 2015 03:59:44 -0800 Subject: [PATCH 08/26] Improved debug using plugin structure --- graphene/contrib/django/debug/__init__.py | 4 +-- .../django/debug/{schema.py => plugin.py} | 26 +++++++++---------- .../contrib/django/debug/tests/test_query.py | 6 ++--- graphene/contrib/django/debug/types.py | 2 +- graphene/core/schema.py | 24 +++++++++++------ 5 files changed, 34 insertions(+), 28 deletions(-) rename graphene/contrib/django/debug/{schema.py => plugin.py} (74%) diff --git a/graphene/contrib/django/debug/__init__.py b/graphene/contrib/django/debug/__init__.py index 0636c3fa..4c76aeca 100644 --- a/graphene/contrib/django/debug/__init__.py +++ b/graphene/contrib/django/debug/__init__.py @@ -1,4 +1,4 @@ -from .schema import DebugSchema +from .plugin import DjangoDebugPlugin from .types import DjangoDebug -__all__ = ['DebugSchema', 'DjangoDebug'] +__all__ = ['DjangoDebugPlugin', 'DjangoDebug'] diff --git a/graphene/contrib/django/debug/schema.py b/graphene/contrib/django/debug/plugin.py similarity index 74% rename from graphene/contrib/django/debug/schema.py rename to graphene/contrib/django/debug/plugin.py index e5e5f30b..5c21863f 100644 --- a/graphene/contrib/django/debug/schema.py +++ b/graphene/contrib/django/debug/plugin.py @@ -1,5 +1,7 @@ +from contextlib import contextmanager from django.db import connections +from ....plugins import Plugin from ....core.schema import Schema from ....core.types import Field from .sql.tracking import unwrap_cursor, wrap_cursor @@ -41,15 +43,11 @@ def debug_objecttype(objecttype): {'debug': Field(DjangoDebug, name='__debug')}) -class DebugSchema(Schema): - - @property - def query(self): - return self._query - - @query.setter - def query(self, value): - self._query = value and debug_objecttype(value) +class DjangoDebugPlugin(Plugin): + def transform_type(self, _type): + if _type == self.schema.query: + return debug_objecttype(_type) + return _type def enable_instrumentation(self, wrapped_root): # This is thread-safe because database connections are thread-local. @@ -60,9 +58,9 @@ class DebugSchema(Schema): for connection in connections.all(): unwrap_cursor(connection) - def execute(self, query, root=None, *args, **kwargs): - wrapped_root = WrappedRoot(root=root) - self.enable_instrumentation(wrapped_root) - result = super(DebugSchema, self).execute(query, wrapped_root, *args, **kwargs) + @contextmanager + def context_execution(self, executor): + executor['root'] = WrappedRoot(root=executor['root']) + self.enable_instrumentation(executor['root']) + yield executor self.disable_instrumentation() - return result diff --git a/graphene/contrib/django/debug/tests/test_query.py b/graphene/contrib/django/debug/tests/test_query.py index 3b4de477..4df26e4f 100644 --- a/graphene/contrib/django/debug/tests/test_query.py +++ b/graphene/contrib/django/debug/tests/test_query.py @@ -4,7 +4,7 @@ import graphene from graphene.contrib.django import DjangoObjectType from ...tests.models import Reporter -from ..schema import DebugSchema +from ..plugin import DjangoDebugPlugin # from examples.starwars_django.models import Character @@ -24,7 +24,7 @@ def test_should_query_well(): class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) - all_reporters = ReporterType.List + all_reporters = ReporterType.List() def resolve_all_reporters(self, *args, **kwargs): return Reporter.objects.all() @@ -64,7 +64,7 @@ def test_should_query_well(): }] } } - schema = DebugSchema(query=Query) + schema = graphene.Schema(query=Query, plugins=[DjangoDebugPlugin()]) result = schema.execute(query) assert not result.errors assert result.data == expected diff --git a/graphene/contrib/django/debug/types.py b/graphene/contrib/django/debug/types.py index d84cbb98..bceb54b0 100644 --- a/graphene/contrib/django/debug/types.py +++ b/graphene/contrib/django/debug/types.py @@ -4,4 +4,4 @@ from .sql.types import DjangoDebugSQL class DjangoDebug(ObjectType): - sql = Field(DjangoDebugSQL.List) + sql = Field(DjangoDebugSQL.List()) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 4da067e4..e8995426 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -127,17 +127,25 @@ class Schema(object): def types(self): return self._types_names - def execute(self, request='', root=None, vars=None, - operation_name=None, **kwargs): - root = root or object() - return self.executor.execute( + def execute(self, request='', root=None, args=None, **kwargs): + executor = kwargs + executor['root'] = root + executor['args'] = args + contexts = [] + for plugin in self.plugins: + if not hasattr(plugin, 'context_execution'): + continue + context = plugin.context_execution(executor) + executor = context.__enter__() + contexts.append((context, executor)) + result = self.executor.execute( self.schema, request, - root=root, - args=vars, - operation_name=operation_name, - **kwargs + **executor ) + for context, value in contexts[::-1]: + context.__exit__(None, None, None) + return result def introspect(self): return self.execute(introspection_query).data From c9e7f67ff9b7f283e4a7ea2c2887b18982b0dd03 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 6 Dec 2015 14:36:47 -0800 Subject: [PATCH 09/26] Improved logic in GroupNamedType --- graphene/core/types/base.py | 5 ++++- graphene/core/types/field.py | 6 ++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/graphene/core/types/base.py b/graphene/core/types/base.py index 94e8a75c..1892cc0d 100644 --- a/graphene/core/types/base.py +++ b/graphene/core/types/base.py @@ -143,8 +143,11 @@ class GroupNamedType(BaseType): name = type.name or type.attname return name, schema.T(type) + def iter_types(self, schema): + return map(partial(self.get_named_type, schema), self.types) + def internal_type(self, schema): - return OrderedDict(map(partial(self.get_named_type, schema), self.types)) + return OrderedDict(self.iter_types(schema)) def __len__(self): return len(self.types) diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index cfe168f7..e5736867 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -145,11 +145,9 @@ class InputField(NamedType, OrderedType): class FieldsGroupType(GroupNamedType): - def internal_type(self, schema): - fields = [] + def iter_types(self, schema): for field in sorted(self.types): try: - fields.append(self.get_named_type(schema, field)) + yield self.get_named_type(schema, field) except SkipField: continue - return OrderedDict(fields) From 9ea10f562c94747c127eee9321a18801eafd08c7 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 6 Dec 2015 14:37:44 -0800 Subject: [PATCH 10/26] Fixed lint errors --- graphene/core/classtypes/base.py | 2 +- graphene/core/schema.py | 2 +- graphene/core/types/base.py | 5 ++--- graphene/core/types/field.py | 3 ++- graphene/plugins/base.py | 1 + graphene/plugins/camel_case.py | 4 ++-- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/graphene/core/classtypes/base.py b/graphene/core/classtypes/base.py index 9dafff0e..cabb909a 100644 --- a/graphene/core/classtypes/base.py +++ b/graphene/core/classtypes/base.py @@ -1,7 +1,7 @@ import copy import inspect -from functools import partial from collections import OrderedDict +from functools import partial import six diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 4da067e4..f3a0b96d 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -10,9 +10,9 @@ from graphql.core.utils.schema_printer import print_schema from graphene import signals +from ..plugins import CamelCase, Plugin from .classtypes.base import ClassType from .types.base import BaseType -from ..plugins import Plugin, CamelCase class GraphQLSchema(_GraphQLSchema): diff --git a/graphene/core/types/base.py b/graphene/core/types/base.py index 1892cc0d..35797f8e 100644 --- a/graphene/core/types/base.py +++ b/graphene/core/types/base.py @@ -1,10 +1,8 @@ from collections import OrderedDict -from functools import total_ordering, partial +from functools import partial, total_ordering import six -from ...utils import to_camel_case - class BaseType(object): @@ -136,6 +134,7 @@ class NamedType(BaseType): class GroupNamedType(BaseType): + def __init__(self, *types): self.types = types diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index e5736867..41dc537b 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -9,7 +9,7 @@ from ..classtypes.inputobjecttype import InputObjectType from ..classtypes.mutation import Mutation from ..exceptions import SkipField from .argument import ArgumentsGroup, snake_case_args -from .base import LazyType, NamedType, MountType, OrderedType, GroupNamedType +from .base import GroupNamedType, LazyType, MountType, NamedType, OrderedType from .definitions import NonNull @@ -145,6 +145,7 @@ class InputField(NamedType, OrderedType): class FieldsGroupType(GroupNamedType): + def iter_types(self, schema): for field in sorted(self.types): try: diff --git a/graphene/plugins/base.py b/graphene/plugins/base.py index 557099c7..c881592e 100644 --- a/graphene/plugins/base.py +++ b/graphene/plugins/base.py @@ -1,4 +1,5 @@ class Plugin(object): + def contribute_to_schema(self, schema): self.schema = schema diff --git a/graphene/plugins/camel_case.py b/graphene/plugins/camel_case.py index cba39ad5..281e3d9d 100644 --- a/graphene/plugins/camel_case.py +++ b/graphene/plugins/camel_case.py @@ -1,7 +1,6 @@ -from .base import Plugin - from ..core.types.base import GroupNamedType from ..utils import memoize, to_camel_case +from .base import Plugin def camelcase_named_type(schema, type): @@ -10,6 +9,7 @@ def camelcase_named_type(schema, type): class CamelCase(Plugin): + @memoize def transform_group(self, _type): new_type = _type.__class__(*_type.types) From 5e708cc919d30a4eb99b63a0ebfb87b65e73e251 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 6 Dec 2015 15:29:54 -0800 Subject: [PATCH 11/26] Improved tests. Improved schema.type getter. Remove duplicated Scalar code --- graphene/core/classtypes/base.py | 2 +- graphene/core/schema.py | 26 +++++++++----------- graphene/core/types/__init__.py | 5 ++-- graphene/core/types/scalars.py | 19 +------------- graphene/core/types/tests/test_field.py | 2 +- graphene/core/types/tests/test_scalars.py | 30 ++--------------------- 6 files changed, 19 insertions(+), 65 deletions(-) diff --git a/graphene/core/classtypes/base.py b/graphene/core/classtypes/base.py index 4f2de009..214edf6f 100644 --- a/graphene/core/classtypes/base.py +++ b/graphene/core/classtypes/base.py @@ -1,7 +1,7 @@ import copy import inspect -from functools import partial from collections import OrderedDict +from functools import partial import six diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 1b0ce8f9..a8ef29e3 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -38,22 +38,20 @@ class Schema(object): def __repr__(self): return '' % (str(self.name), hash(self)) - def T(self, object_type): - if not object_type: + def T(self, _type): + if not _type: return - if inspect.isclass(object_type) and issubclass( - object_type, (BaseType, ClassType)) or isinstance( - object_type, BaseType): - if object_type not in self._types: - internal_type = object_type.internal_type(self) - self._types[object_type] = internal_type - is_objecttype = inspect.isclass( - object_type) and issubclass(object_type, ClassType) - if is_objecttype: - self.register(object_type) - return self._types[object_type] + is_classtype = inspect.isclass(_type) and issubclass(_type, ClassType) + is_instancetype = isinstance(_type, BaseType) + if is_classtype or is_instancetype: + if _type not in self._types: + internal_type = _type.internal_type(self) + self._types[_type] = internal_type + if is_classtype: + self.register(_type) + return self._types[_type] else: - return object_type + return _type @property def executor(self): diff --git a/graphene/core/types/__init__.py b/graphene/core/types/__init__.py index 9260476c..0ffa52bb 100644 --- a/graphene/core/types/__init__.py +++ b/graphene/core/types/__init__.py @@ -4,7 +4,7 @@ from .definitions import List, NonNull # Compatibility import from .objecttype import Interface, ObjectType, Mutation, InputObjectType -from .scalars import String, ID, Boolean, Int, Float, Scalar +from .scalars import String, ID, Boolean, Int, Float from .field import Field, InputField __all__ = [ @@ -26,5 +26,4 @@ __all__ = [ 'ID', 'Boolean', 'Int', - 'Float', - 'Scalar'] + 'Float'] diff --git a/graphene/core/types/scalars.py b/graphene/core/types/scalars.py index 75cd70a3..d0d315b4 100644 --- a/graphene/core/types/scalars.py +++ b/graphene/core/types/scalars.py @@ -1,5 +1,5 @@ from graphql.core.type import (GraphQLBoolean, GraphQLFloat, GraphQLID, - GraphQLInt, GraphQLScalarType, GraphQLString) + GraphQLInt, GraphQLString) from .base import MountedType @@ -22,20 +22,3 @@ class ID(MountedType): class Float(MountedType): T = GraphQLFloat - - -class Scalar(MountedType): - - @classmethod - def internal_type(cls, schema): - serialize = getattr(cls, 'serialize') - parse_literal = getattr(cls, 'parse_literal') - parse_value = getattr(cls, 'parse_value') - - return GraphQLScalarType( - name=cls.__name__, - description=cls.__doc__, - serialize=serialize, - parse_value=parse_value, - parse_literal=parse_literal - ) diff --git a/graphene/core/types/tests/test_field.py b/graphene/core/types/tests/test_field.py index 8253ed20..df72a6f7 100644 --- a/graphene/core/types/tests/test_field.py +++ b/graphene/core/types/tests/test_field.py @@ -13,7 +13,7 @@ from ..scalars import String def test_field_internal_type(): resolver = lambda *args: 'RESOLVED' - field = Field(String, description='My argument', resolver=resolver) + field = Field(String(), description='My argument', resolver=resolver) class Query(ObjectType): my_field = field diff --git a/graphene/core/types/tests/test_scalars.py b/graphene/core/types/tests/test_scalars.py index 8b8d930f..39fd6063 100644 --- a/graphene/core/types/tests/test_scalars.py +++ b/graphene/core/types/tests/test_scalars.py @@ -1,9 +1,9 @@ from graphql.core.type import (GraphQLBoolean, GraphQLFloat, GraphQLID, - GraphQLInt, GraphQLScalarType, GraphQLString) + GraphQLInt, GraphQLString) from graphene.core.schema import Schema -from ..scalars import ID, Boolean, Float, Int, Scalar, String +from ..scalars import ID, Boolean, Float, Int, String schema = Schema() @@ -26,29 +26,3 @@ def test_id_scalar(): def test_float_scalar(): assert schema.T(Float()) == GraphQLFloat - - -def test_custom_scalar(): - import datetime - from graphql.core.language import ast - - class DateTimeScalar(Scalar): - '''DateTimeScalar Documentation''' - @staticmethod - def serialize(dt): - return dt.isoformat() - - @staticmethod - def parse_literal(node): - if isinstance(node, ast.StringValue): - return datetime.datetime.strptime( - node.value, "%Y-%m-%dT%H:%M:%S.%f") - - @staticmethod - def parse_value(value): - return datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f") - - scalar_type = schema.T(DateTimeScalar) - assert isinstance(scalar_type, GraphQLScalarType) - assert scalar_type.name == 'DateTimeScalar' - assert scalar_type.description == 'DateTimeScalar Documentation' From 37a454b83138e9fc7525af0806654e0de774f984 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 6 Dec 2015 15:49:54 -0800 Subject: [PATCH 12/26] Renamed BaseType to InstanceType for code clarity --- graphene/__init__.py | 4 ++-- graphene/core/__init__.py | 4 ++-- graphene/core/schema.py | 4 ++-- graphene/core/types/__init__.py | 4 ++-- graphene/core/types/argument.py | 4 ++-- graphene/core/types/base.py | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index 88404d62..71066499 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -11,7 +11,7 @@ from .core import ( Interface, Mutation, Scalar, - BaseType, + InstanceType, LazyType, Argument, Field, @@ -51,7 +51,7 @@ __all__ = [ 'NonNull', 'signals', 'Schema', - 'BaseType', + 'InstanceType', 'LazyType', 'ObjectType', 'InputObjectType', diff --git a/graphene/core/__init__.py b/graphene/core/__init__.py index d27a72bb..9e8c7108 100644 --- a/graphene/core/__init__.py +++ b/graphene/core/__init__.py @@ -11,7 +11,7 @@ from .classtypes import ( ) from .types import ( - BaseType, + InstanceType, LazyType, Argument, Field, @@ -35,7 +35,7 @@ __all__ = [ 'List', 'NonNull', 'Schema', - 'BaseType', + 'InstanceType', 'LazyType', 'ObjectType', 'InputObjectType', diff --git a/graphene/core/schema.py b/graphene/core/schema.py index a8ef29e3..8a1e1b7a 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -11,7 +11,7 @@ from graphql.core.utils.schema_printer import print_schema from graphene import signals from .classtypes.base import ClassType -from .types.base import BaseType +from .types.base import InstanceType class GraphQLSchema(_GraphQLSchema): @@ -42,7 +42,7 @@ class Schema(object): if not _type: return is_classtype = inspect.isclass(_type) and issubclass(_type, ClassType) - is_instancetype = isinstance(_type, BaseType) + is_instancetype = isinstance(_type, InstanceType) if is_classtype or is_instancetype: if _type not in self._types: internal_type = _type.internal_type(self) diff --git a/graphene/core/types/__init__.py b/graphene/core/types/__init__.py index 0ffa52bb..51512ec4 100644 --- a/graphene/core/types/__init__.py +++ b/graphene/core/types/__init__.py @@ -1,4 +1,4 @@ -from .base import BaseType, LazyType, OrderedType +from .base import InstanceType, LazyType, OrderedType from .argument import Argument, ArgumentsGroup, to_arguments from .definitions import List, NonNull # Compatibility import @@ -8,7 +8,7 @@ from .scalars import String, ID, Boolean, Int, Float from .field import Field, InputField __all__ = [ - 'BaseType', + 'InstanceType', 'LazyType', 'OrderedType', 'Argument', diff --git a/graphene/core/types/argument.py b/graphene/core/types/argument.py index 0892c446..7eb77770 100644 --- a/graphene/core/types/argument.py +++ b/graphene/core/types/argument.py @@ -5,7 +5,7 @@ from itertools import chain from graphql.core.type import GraphQLArgument from ...utils import ProxySnakeDict, to_camel_case -from .base import ArgumentType, BaseType, OrderedType +from .base import ArgumentType, InstanceType, OrderedType class Argument(OrderedType): @@ -27,7 +27,7 @@ class Argument(OrderedType): return self.name -class ArgumentsGroup(BaseType): +class ArgumentsGroup(InstanceType): def __init__(self, *args, **kwargs): arguments = to_arguments(*args, **kwargs) diff --git a/graphene/core/types/base.py b/graphene/core/types/base.py index 2b4078e4..8cfa603f 100644 --- a/graphene/core/types/base.py +++ b/graphene/core/types/base.py @@ -3,14 +3,14 @@ from functools import total_ordering import six -class BaseType(object): +class InstanceType(object): @classmethod def internal_type(cls, schema): return getattr(cls, 'T', None) -class MountType(BaseType): +class MountType(InstanceType): parent = None def mount(self, cls): From 2724025a5b4029f36fb4cc6a388dfbf17f31d009 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 6 Dec 2015 15:53:56 -0800 Subject: [PATCH 13/26] Improved ScalarTypes code --- graphene/core/types/base.py | 5 ++--- graphene/core/types/scalars.py | 25 +++++++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/graphene/core/types/base.py b/graphene/core/types/base.py index 8cfa603f..35f93267 100644 --- a/graphene/core/types/base.py +++ b/graphene/core/types/base.py @@ -5,9 +5,8 @@ import six class InstanceType(object): - @classmethod - def internal_type(cls, schema): - return getattr(cls, 'T', None) + def internal_type(self, schema): + raise NotImplementedError("internal_type for type {} is not implemented".format(self.__class__.__name__)) class MountType(InstanceType): diff --git a/graphene/core/types/scalars.py b/graphene/core/types/scalars.py index d0d315b4..a87ddea2 100644 --- a/graphene/core/types/scalars.py +++ b/graphene/core/types/scalars.py @@ -4,21 +4,26 @@ from graphql.core.type import (GraphQLBoolean, GraphQLFloat, GraphQLID, from .base import MountedType -class String(MountedType): - T = GraphQLString +class ScalarType(MountedType): + def internal_type(self, schema): + return self._internal_type -class Int(MountedType): - T = GraphQLInt +class String(ScalarType): + _internal_type = GraphQLString -class Boolean(MountedType): - T = GraphQLBoolean +class Int(ScalarType): + _internal_type = GraphQLInt -class ID(MountedType): - T = GraphQLID +class Boolean(ScalarType): + _internal_type = GraphQLBoolean -class Float(MountedType): - T = GraphQLFloat +class ID(ScalarType): + _internal_type = GraphQLID + + +class Float(ScalarType): + _internal_type = GraphQLFloat From cd5d9b8eead87cb20ad902e96984290e79fe02c3 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 6 Dec 2015 16:34:47 -0800 Subject: [PATCH 14/26] Improved plugin structure based on @adamcharnock suggestions --- graphene/core/schema.py | 10 ++++++---- graphene/core/types/argument.py | 14 ++++++-------- graphene/core/types/base.py | 7 +++++-- graphene/core/types/field.py | 6 +++--- graphene/core/types/tests/test_argument.py | 2 +- graphene/plugins/camel_case.py | 20 +++----------------- 6 files changed, 24 insertions(+), 35 deletions(-) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 960f0aca..d3bff4bd 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -50,10 +50,12 @@ class Schema(object): plugin.contribute_to_schema(self) self.plugins.append(plugin) - def get_internal_type(self, objecttype): + def get_default_namedtype_name(self, value): for plugin in self.plugins: - objecttype = plugin.transform_type(objecttype) - return objecttype.internal_type(self) + if not hasattr(plugin, 'get_default_namedtype_name'): + continue + value = plugin.get_default_namedtype_name(value) + return value def T(self, _type): if not _type: @@ -62,7 +64,7 @@ class Schema(object): is_instancetype = isinstance(_type, InstanceType) if is_classtype or is_instancetype: if _type not in self._types: - internal_type = self.get_internal_type(_type) + internal_type = _type.internal_type(self) self._types[_type] = internal_type if is_classtype: self.register(_type) diff --git a/graphene/core/types/argument.py b/graphene/core/types/argument.py index a8fe343e..b10aff21 100644 --- a/graphene/core/types/argument.py +++ b/graphene/core/types/argument.py @@ -11,9 +11,7 @@ class Argument(NamedType, OrderedType): def __init__(self, type, description=None, default=None, name=None, _creation_counter=None): - super(Argument, self).__init__(_creation_counter=_creation_counter) - self.name = name - self.attname = None + super(Argument, self).__init__(name=name, _creation_counter=_creation_counter) self.type = type self.description = description self.default = default @@ -38,18 +36,18 @@ def to_arguments(*args, **kwargs): arguments = {} iter_arguments = chain(kwargs.items(), [(None, a) for a in args]) - for attname, arg in iter_arguments: + for default_name, arg in iter_arguments: if isinstance(arg, Argument): argument = arg elif isinstance(arg, ArgumentType): argument = arg.as_argument() else: - raise ValueError('Unknown argument %s=%r' % (attname, arg)) + raise ValueError('Unknown argument %s=%r' % (default_name, arg)) - if attname: - argument.attname = attname + if default_name: + argument.default_name = default_name - name = argument.name or argument.attname + name = argument.name or argument.default_name assert name, 'Argument in field must have a name' assert name not in arguments, 'Found more than one Argument with same name {}'.format(name) arguments[name] = argument diff --git a/graphene/core/types/base.py b/graphene/core/types/base.py index 96501a19..c9d26cf5 100644 --- a/graphene/core/types/base.py +++ b/graphene/core/types/base.py @@ -129,7 +129,10 @@ class MountedType(FieldType, ArgumentType): class NamedType(InstanceType): - pass + def __init__(self, name=None, default_name=None, *args, **kwargs): + self.name = name + self.default_name = None + super(NamedType, self).__init__(*args, **kwargs) class GroupNamedType(InstanceType): @@ -138,7 +141,7 @@ class GroupNamedType(InstanceType): self.types = types def get_named_type(self, schema, type): - name = type.name or type.attname + name = type.name or schema.get_default_namedtype_name(type.default_name) return name, schema.T(type) def iter_types(self, schema): diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index 41dc537b..f6c55edb 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -19,8 +19,7 @@ class Field(NamedType, OrderedType): self, type, description=None, args=None, name=None, resolver=None, required=False, default=None, *args_list, **kwargs): _creation_counter = kwargs.pop('_creation_counter', None) - super(Field, self).__init__(_creation_counter=_creation_counter) - self.name = name + super(Field, self).__init__(name=name, _creation_counter=_creation_counter) if isinstance(type, six.string_types): type = LazyType(type) self.required = required @@ -37,6 +36,7 @@ class Field(NamedType, OrderedType): cls, (FieldsClassType)), 'Field {} cannot be mounted in {}'.format( self, cls) self.attname = attname + self.default_name = attname self.object_type = cls self.mount(cls) if isinstance(self.type, MountType): @@ -120,7 +120,6 @@ class InputField(NamedType, OrderedType): def __init__(self, type, description=None, default=None, name=None, _creation_counter=None, required=False): super(InputField, self).__init__(_creation_counter=_creation_counter) - self.name = name if required: type = NonNull(type) self.type = type @@ -132,6 +131,7 @@ class InputField(NamedType, OrderedType): cls, (InputObjectType)), 'InputField {} cannot be mounted in {}'.format( self, cls) self.attname = attname + self.default_name = attname self.object_type = cls self.mount(cls) if isinstance(self.type, MountType): diff --git a/graphene/core/types/tests/test_argument.py b/graphene/core/types/tests/test_argument.py index cf07a7d3..b2f5e239 100644 --- a/graphene/core/types/tests/test_argument.py +++ b/graphene/core/types/tests/test_argument.py @@ -27,7 +27,7 @@ def test_to_arguments(): other_kwarg=String(), ) - assert [a.name or a.attname for a in arguments] == [ + assert [a.name or a.default_name for a in arguments] == [ 'myArg', 'otherArg', 'my_kwarg', 'other_kwarg'] diff --git a/graphene/plugins/camel_case.py b/graphene/plugins/camel_case.py index 281e3d9d..c8ebcd11 100644 --- a/graphene/plugins/camel_case.py +++ b/graphene/plugins/camel_case.py @@ -1,22 +1,8 @@ -from ..core.types.base import GroupNamedType -from ..utils import memoize, to_camel_case +from ..utils import to_camel_case from .base import Plugin -def camelcase_named_type(schema, type): - name = type.name or to_camel_case(type.attname) - return name, schema.T(type) - - class CamelCase(Plugin): - @memoize - def transform_group(self, _type): - new_type = _type.__class__(*_type.types) - setattr(new_type, 'get_named_type', camelcase_named_type) - return new_type - - def transform_type(self, _type): - if isinstance(_type, GroupNamedType): - return self.transform_group(_type) - return _type + def get_default_namedtype_name(self, value): + return to_camel_case(value) From dd5b26e6ed8c9188299ff965d18d8c902bc768d2 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 6 Dec 2015 16:50:34 -0800 Subject: [PATCH 15/26] Improved debug plugin structure --- graphene/contrib/django/debug/plugin.py | 17 ++++++++++++++--- graphene/core/schema.py | 4 ++-- graphene/core/types/base.py | 1 + 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/graphene/contrib/django/debug/plugin.py b/graphene/contrib/django/debug/plugin.py index 5c21863f..4b6b1b5d 100644 --- a/graphene/contrib/django/debug/plugin.py +++ b/graphene/contrib/django/debug/plugin.py @@ -1,9 +1,9 @@ from contextlib import contextmanager + from django.db import connections -from ....plugins import Plugin -from ....core.schema import Schema from ....core.types import Field +from ....plugins import Plugin from .sql.tracking import unwrap_cursor, wrap_cursor from .sql.types import DjangoDebugSQL from .types import DjangoDebug @@ -44,9 +44,10 @@ def debug_objecttype(objecttype): class DjangoDebugPlugin(Plugin): + def transform_type(self, _type): if _type == self.schema.query: - return debug_objecttype(_type) + return return _type def enable_instrumentation(self, wrapped_root): @@ -58,9 +59,19 @@ class DjangoDebugPlugin(Plugin): for connection in connections.all(): unwrap_cursor(connection) + def wrap_schema(self, schema_type): + query = schema_type._query + if query: + class_type = self.schema.objecttype(schema_type._query) + assert class_type, 'The query in schema is not constructed with graphene' + _type = debug_objecttype(class_type) + schema_type._query = self.schema.T(_type) + return schema_type + @contextmanager def context_execution(self, executor): executor['root'] = WrappedRoot(root=executor['root']) + executor['schema'] = self.wrap_schema(executor['schema']) self.enable_instrumentation(executor['root']) yield executor self.disable_instrumentation() diff --git a/graphene/core/schema.py b/graphene/core/schema.py index fc8835c1..d1d33f00 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -131,6 +131,8 @@ class Schema(object): executor = kwargs executor['root'] = root executor['args'] = args + executor['schema'] = self.schema + executor['request'] = request contexts = [] for plugin in self.plugins: if not hasattr(plugin, 'context_execution'): @@ -139,8 +141,6 @@ class Schema(object): executor = context.__enter__() contexts.append((context, executor)) result = self.executor.execute( - self.schema, - request, **executor ) for context, value in contexts[::-1]: diff --git a/graphene/core/types/base.py b/graphene/core/types/base.py index c9d26cf5..ec8c7b3b 100644 --- a/graphene/core/types/base.py +++ b/graphene/core/types/base.py @@ -129,6 +129,7 @@ class MountedType(FieldType, ArgumentType): class NamedType(InstanceType): + def __init__(self, name=None, default_name=None, *args, **kwargs): self.name = name self.default_name = None From dca435a22022ba1f92bd5cd2f2d175f98999c19d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 9 Dec 2015 19:22:54 -0800 Subject: [PATCH 16/26] Improved plugin system --- graphene/core/schema.py | 21 ++++++------------- graphene/plugins/__init__.py | 4 ++-- graphene/plugins/base.py | 37 ++++++++++++++++++++++++++++++++-- graphene/plugins/camel_case.py | 3 +-- 4 files changed, 44 insertions(+), 21 deletions(-) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index d3bff4bd..6c98371f 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -10,7 +10,7 @@ from graphql.core.utils.schema_printer import print_schema from graphene import signals -from ..plugins import CamelCase, Plugin +from ..plugins import CamelCase, PluginManager from .classtypes.base import ClassType from .types.base import InstanceType @@ -34,28 +34,19 @@ class Schema(object): self.subscription = subscription self.name = name self.executor = executor - self.plugins = [] plugins = plugins or [] if auto_camelcase: plugins.append(CamelCase()) - for plugin in plugins: - self.add_plugin(plugin) + self.plugins = PluginManager(self, plugins) signals.init_schema.send(self) def __repr__(self): return '' % (str(self.name), hash(self)) - def add_plugin(self, plugin): - assert isinstance(plugin, Plugin), 'A plugin need to subclass graphene.Plugin and be instantiated' - plugin.contribute_to_schema(self) - self.plugins.append(plugin) - - def get_default_namedtype_name(self, value): - for plugin in self.plugins: - if not hasattr(plugin, 'get_default_namedtype_name'): - continue - value = plugin.get_default_namedtype_name(value) - return value + def __getattr__(self, name): + if name in self.plugins: + return getattr(self.plugins, name) + return super(Schema, self).__getattr__(name) def T(self, _type): if not _type: diff --git a/graphene/plugins/__init__.py b/graphene/plugins/__init__.py index 5a9bf26b..160bffba 100644 --- a/graphene/plugins/__init__.py +++ b/graphene/plugins/__init__.py @@ -1,6 +1,6 @@ -from .base import Plugin +from .base import Plugin, PluginManager from .camel_case import CamelCase __all__ = [ - 'Plugin', 'CamelCase' + 'Plugin', 'PluginManager', 'CamelCase' ] diff --git a/graphene/plugins/base.py b/graphene/plugins/base.py index c881592e..8ab5668a 100644 --- a/graphene/plugins/base.py +++ b/graphene/plugins/base.py @@ -1,7 +1,40 @@ +from functools import partial, reduce + + class Plugin(object): def contribute_to_schema(self, schema): self.schema = schema - def transform_type(self, objecttype): - return objecttype + +def apply_function(a, b): + return b(a) + + +class PluginManager(object): + + PLUGIN_FUNCTIONS = ('get_default_namedtype_name', ) + + def __init__(self, schema, plugins=[]): + self.schema = schema + self.plugins = [] + for plugin in plugins: + self.add_plugin(plugin) + + def add_plugin(self, plugin): + if hasattr(plugin, 'contribute_to_schema'): + plugin.contribute_to_schema(self.schema) + self.plugins.append(plugin) + + def get_plugin_functions(self, function): + for plugin in self.plugins: + if not hasattr(plugin, function): + continue + yield getattr(plugin, function) + + def __getattr__(self, name): + functions = self.get_plugin_functions(name) + return partial(reduce, apply_function, functions) + + def __contains__(self, name): + return name in self.PLUGIN_FUNCTIONS diff --git a/graphene/plugins/camel_case.py b/graphene/plugins/camel_case.py index c8ebcd11..d9a9084f 100644 --- a/graphene/plugins/camel_case.py +++ b/graphene/plugins/camel_case.py @@ -1,8 +1,7 @@ from ..utils import to_camel_case -from .base import Plugin -class CamelCase(Plugin): +class CamelCase(object): def get_default_namedtype_name(self, value): return to_camel_case(value) From c8f4c138224b6e31f01be65a8c1131e2ecf2ed5d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 9 Dec 2015 19:44:35 -0800 Subject: [PATCH 17/26] Improved plugin execution --- graphene/core/schema.py | 21 +++------------------ graphene/plugins/base.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index dd0fce9e..f8f26dc1 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -119,24 +119,9 @@ class Schema(object): return self._types_names def execute(self, request='', root=None, args=None, **kwargs): - executor = kwargs - executor['root'] = root - executor['args'] = args - executor['schema'] = self.schema - executor['request'] = request - contexts = [] - for plugin in self.plugins: - if not hasattr(plugin, 'context_execution'): - continue - context = plugin.context_execution(executor) - executor = context.__enter__() - contexts.append((context, executor)) - result = self.executor.execute( - **executor - ) - for context, value in contexts[::-1]: - context.__exit__(None, None, None) - return result + kwargs = dict(kwargs, request=request, root=root, args=args, schema=self.schema) + with self.plugins.context_execution(**kwargs) as execute_kwargs: + return self.executor.execute(**execute_kwargs) def introspect(self): return self.execute(introspection_query).data diff --git a/graphene/plugins/base.py b/graphene/plugins/base.py index 8ab5668a..2347beba 100644 --- a/graphene/plugins/base.py +++ b/graphene/plugins/base.py @@ -1,3 +1,4 @@ +from contextlib import contextmanager from functools import partial, reduce @@ -38,3 +39,15 @@ class PluginManager(object): def __contains__(self, name): return name in self.PLUGIN_FUNCTIONS + + @contextmanager + def context_execution(self, **executor): + contexts = [] + functions = self.get_plugin_functions('context_execution') + for f in functions: + context = f(executor) + executor = context.__enter__() + contexts.append((context, executor)) + yield executor + for context, value in contexts[::-1]: + context.__exit__(None, None, None) From 930f084912023d5e4e1b633ba2f11b33a1da8838 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 10 Dec 2015 22:11:43 -0800 Subject: [PATCH 18/26] Fixed DjangoDebugPlugin. Improved Django views --- graphene/contrib/django/debug/plugin.py | 16 ++++++++------- graphene/contrib/django/debug/sql/types.py | 6 +++--- graphene/contrib/django/tests/test_urls.py | 10 +++++++++- graphene/contrib/django/tests/test_views.py | 22 +++++++++++++-------- graphene/contrib/django/views.py | 4 ++-- graphene/core/schema.py | 4 ++-- setup.py | 2 +- 7 files changed, 40 insertions(+), 24 deletions(-) diff --git a/graphene/contrib/django/debug/plugin.py b/graphene/contrib/django/debug/plugin.py index 4b6b1b5d..86f8da58 100644 --- a/graphene/contrib/django/debug/plugin.py +++ b/graphene/contrib/django/debug/plugin.py @@ -3,6 +3,7 @@ from contextlib import contextmanager from django.db import connections from ....core.types import Field +from ....core.schema import GraphQLSchema from ....plugins import Plugin from .sql.tracking import unwrap_cursor, wrap_cursor from .sql.types import DjangoDebugSQL @@ -45,11 +46,6 @@ def debug_objecttype(objecttype): class DjangoDebugPlugin(Plugin): - def transform_type(self, _type): - if _type == self.schema.query: - return - return _type - def enable_instrumentation(self, wrapped_root): # This is thread-safe because database connections are thread-local. for connection in connections.all(): @@ -62,10 +58,16 @@ class DjangoDebugPlugin(Plugin): def wrap_schema(self, schema_type): query = schema_type._query if query: - class_type = self.schema.objecttype(schema_type._query) + class_type = self.schema.objecttype(schema_type.get_query_type()) assert class_type, 'The query in schema is not constructed with graphene' _type = debug_objecttype(class_type) - schema_type._query = self.schema.T(_type) + self.schema.register(_type, force=True) + return GraphQLSchema( + self.schema, + self.schema.T(_type), + schema_type.get_mutation_type(), + schema_type.get_subscription_type() + ) return schema_type @contextmanager diff --git a/graphene/contrib/django/debug/sql/types.py b/graphene/contrib/django/debug/sql/types.py index 7ee1894f..5df5e9d8 100644 --- a/graphene/contrib/django/debug/sql/types.py +++ b/graphene/contrib/django/debug/sql/types.py @@ -1,4 +1,4 @@ -from .....core import Float, ObjectType, String +from .....core import Float, ObjectType, String, Boolean class DjangoDebugSQL(ObjectType): @@ -10,8 +10,8 @@ class DjangoDebugSQL(ObjectType): params = String() start_time = Float() stop_time = Float() - is_slow = String() - is_select = String() + is_slow = Boolean() + is_select = Boolean() trans_id = String() trans_status = String() diff --git a/graphene/contrib/django/tests/test_urls.py b/graphene/contrib/django/tests/test_urls.py index 409471d4..9d38980e 100644 --- a/graphene/contrib/django/tests/test_urls.py +++ b/graphene/contrib/django/tests/test_urls.py @@ -29,7 +29,15 @@ class Human(DjangoNode): def get_node(self, id): pass -schema = Schema(query=Human) + +class Query(graphene.ObjectType): + human = graphene.Field(Human) + + def resolve_human(self, args, info): + return Human() + + +schema = Schema(query=Query) urlpatterns = [ diff --git a/graphene/contrib/django/tests/test_views.py b/graphene/contrib/django/tests/test_views.py index f82be99f..b4e1b367 100644 --- a/graphene/contrib/django/tests/test_views.py +++ b/graphene/contrib/django/tests/test_views.py @@ -7,11 +7,13 @@ def format_response(response): def test_client_get_good_query(settings, client): settings.ROOT_URLCONF = 'graphene.contrib.django.tests.test_urls' - response = client.get('/graphql', {'query': '{ headline }'}) + response = client.get('/graphql', {'query': '{ human { headline } }'}) json_response = format_response(response) expected_json = { 'data': { - 'headline': None + 'human': { + 'headline': None + } } } assert json_response == expected_json @@ -19,20 +21,22 @@ def test_client_get_good_query(settings, client): def test_client_get_good_query_with_raise(settings, client): settings.ROOT_URLCONF = 'graphene.contrib.django.tests.test_urls' - response = client.get('/graphql', {'query': '{ raises }'}) + response = client.get('/graphql', {'query': '{ human { raises } }'}) json_response = format_response(response) assert json_response['errors'][0]['message'] == 'This field should raise exception' - assert json_response['data']['raises'] is None + assert json_response['data']['human']['raises'] is None def test_client_post_good_query_json(settings, client): settings.ROOT_URLCONF = 'graphene.contrib.django.tests.test_urls' response = client.post( - '/graphql', json.dumps({'query': '{ headline }'}), 'application/json') + '/graphql', json.dumps({'query': '{ human { headline } }'}), 'application/json') json_response = format_response(response) expected_json = { 'data': { - 'headline': None + 'human': { + 'headline': None + } } } assert json_response == expected_json @@ -41,11 +45,13 @@ def test_client_post_good_query_json(settings, client): def test_client_post_good_query_graphql(settings, client): settings.ROOT_URLCONF = 'graphene.contrib.django.tests.test_urls' response = client.post( - '/graphql', '{ headline }', 'application/graphql') + '/graphql', '{ human { headline } }', 'application/graphql') json_response = format_response(response) expected_json = { 'data': { - 'headline': None + 'human': { + 'headline': None + } } } assert json_response == expected_json diff --git a/graphene/contrib/django/views.py b/graphene/contrib/django/views.py index ad245d72..9a4bd96e 100644 --- a/graphene/contrib/django/views.py +++ b/graphene/contrib/django/views.py @@ -12,5 +12,5 @@ class GraphQLView(BaseGraphQLView): **kwargs ) - def get_root_value(self, request): - return self.graphene_schema.query(super(GraphQLView, self).get_root_value(request)) + def execute(self, *args, **kwargs): + return self.graphene_schema.execute(*args, **kwargs) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index f8f26dc1..c8695317 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -84,9 +84,9 @@ class Schema(object): mutation=self.T(self.mutation), subscription=self.T(self.subscription)) - def register(self, object_type): + def register(self, object_type, force=False): type_name = object_type._meta.type_name - registered_object_type = self._types_names.get(type_name, None) + registered_object_type = not force and self._types_names.get(type_name, None) if registered_object_type: assert registered_object_type == object_type, 'Type {} already registered with other object type'.format( type_name) diff --git a/setup.py b/setup.py index 2c36fcec..7ef809ed 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( 'django': [ 'Django>=1.6.0,<1.9', 'singledispatch>=3.4.0.3', - 'graphql-django-view>=1.0.0', + 'graphql-django-view>=1.1.0', ], }, From e27af63f3a4bd28ae72478778fde75e463882def Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 10 Dec 2015 22:27:56 -0800 Subject: [PATCH 19/26] Updated version to 0.5.0 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 7ef809ed..b7a3e7c5 100644 --- a/setup.py +++ b/setup.py @@ -24,9 +24,9 @@ class PyTest(TestCommand): setup( name='graphene', - version='0.4.3', + version='0.5.0', - description='Graphene: Python DSL for GraphQL', + description='GraphQL Framework for Python', long_description=open('README.rst').read(), url='https://github.com/graphql-python/graphene', From 2eea03cb62f9c254792f066ca8b697060b2407ed Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 11 Dec 2015 10:42:10 -0800 Subject: [PATCH 20/26] Improved name in Field. Could be an Argument --- graphene/core/types/field.py | 7 +++++-- graphene/core/types/tests/test_field.py | 8 ++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index f6c55edb..17fc9fa2 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -8,8 +8,8 @@ from ..classtypes.base import FieldsClassType from ..classtypes.inputobjecttype import InputObjectType from ..classtypes.mutation import Mutation from ..exceptions import SkipField -from .argument import ArgumentsGroup, snake_case_args -from .base import GroupNamedType, LazyType, MountType, NamedType, OrderedType +from .argument import Argument, ArgumentsGroup, snake_case_args +from .base import GroupNamedType, LazyType, MountType, NamedType, ArgumentType, OrderedType from .definitions import NonNull @@ -19,6 +19,9 @@ class Field(NamedType, OrderedType): self, type, description=None, args=None, name=None, resolver=None, required=False, default=None, *args_list, **kwargs): _creation_counter = kwargs.pop('_creation_counter', None) + if isinstance(name, (Argument, ArgumentType)): + kwargs['name'] = name + name = None super(Field, self).__init__(name=name, _creation_counter=_creation_counter) if isinstance(type, six.string_types): type = LazyType(type) diff --git a/graphene/core/types/tests/test_field.py b/graphene/core/types/tests/test_field.py index dd30e556..2b1f7e40 100644 --- a/graphene/core/types/tests/test_field.py +++ b/graphene/core/types/tests/test_field.py @@ -104,6 +104,14 @@ def test_field_custom_arguments(): assert 'p' in schema.T(args) +def test_field_name_as_argument(): + field = Field(None, name=String()) + schema = Schema() + + args = field.arguments + assert 'name' in schema.T(args) + + def test_inputfield_internal_type(): field = InputField(String, description='My input field', default='3') From 9dcd7986b86010c78d8fb2a8d5da6e1bf915653c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 11 Dec 2015 10:53:05 -0800 Subject: [PATCH 21/26] Improved testing on fields --- graphene/core/types/tests/test_field.py | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/graphene/core/types/tests/test_field.py b/graphene/core/types/tests/test_field.py index 2b1f7e40..706cbc59 100644 --- a/graphene/core/types/tests/test_field.py +++ b/graphene/core/types/tests/test_field.py @@ -129,3 +129,38 @@ def test_inputfield_internal_type(): assert isinstance(type, GraphQLInputObjectField) assert type.description == 'My input field' assert type.default_value == '3' + + +def test_field_resolve_argument(): + resolver = lambda instance, args, info: args.get('first_name') + + field = Field(String(), first_name=String(), description='My argument', resolver=resolver) + + class Query(ObjectType): + my_field = field + schema = Schema(query=Query) + + type = schema.T(field) + assert type.resolver(None, {'firstName': 'Peter'}, None) == 'Peter' + + +def test_field_resolve_vars(): + class Query(ObjectType): + hello = String(first_name=String()) + + def resolve_hello(self, args, info): + return 'Hello ' + args.get('first_name') + + schema = Schema(query=Query) + + result = schema.execute(""" + query foo($firstName:String) + { + hello(firstName:$firstName) + } + """, args={"firstName": "Serkan"}) + + expected = { + 'hello': 'Hello Serkan' + } + assert result.data == expected From 689db2c70ebfe8a06abda3571378303e8ff04fac Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 11 Dec 2015 11:03:49 -0800 Subject: [PATCH 22/26] Fixed incompatible syntax in Python 2.7 --- graphene/contrib/django/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index e25130df..d8fc1b86 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -102,13 +102,13 @@ class DjangoConnection(Connection): return super(DjangoConnection, cls).from_list(iterable, *args, **kwargs) -django_node_meta_bases = (DjangoObjectTypeMeta, NodeMeta) +django_filter_metabase = type # Only include filter functionality if available if DJANGO_FILTER_INSTALLED: - django_node_meta_bases = (DjangoFilterObjectTypeMeta,) + django_node_meta_bases + django_filter_metabase = DjangoFilterObjectTypeMeta -class DjangoNodeMeta(*django_node_meta_bases): +class DjangoNodeMeta(django_filter_metabase, DjangoObjectTypeMeta, NodeMeta): pass From b4f7df3c9de122275f33e6a360560c576511601a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 11 Dec 2015 11:20:35 -0800 Subject: [PATCH 23/26] Fixed argument getter in django filters tests --- graphene/contrib/django/tests/filter/test_fields.py | 7 ++++--- graphene/contrib/django/tests/test_query.py | 4 ++++ graphene/contrib/django/utils.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index 33cc2421..4c93ca1e 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -35,12 +35,13 @@ class PetNode(DjangoNode): class Meta: model = Pet +schema = Schema() def assert_arguments(field, *arguments): ignore = ('after', 'before', 'first', 'last', 'orderBy') actual = [ name - for name in field.arguments.arguments.keys() + for name in schema.T(field.arguments) if name not in ignore and not name.startswith('_') ] assert set(arguments) == set(actual), \ @@ -51,12 +52,12 @@ def assert_arguments(field, *arguments): def assert_orderable(field): - assert 'orderBy' in field.arguments.arguments.keys(), \ + assert 'orderBy' in schema.T(field.arguments), \ 'Field cannot be ordered' def assert_not_orderable(field): - assert 'orderBy' not in field.arguments.arguments.keys(), \ + assert 'orderBy' not in schema.T(field.arguments), \ 'Field can be ordered' diff --git a/graphene/contrib/django/tests/test_query.py b/graphene/contrib/django/tests/test_query.py index 090c8695..4b37d517 100644 --- a/graphene/contrib/django/tests/test_query.py +++ b/graphene/contrib/django/tests/test_query.py @@ -1,3 +1,4 @@ +import pytest from py.test import raises import graphene @@ -7,6 +8,9 @@ from graphene.contrib.django import DjangoNode, DjangoObjectType from .models import Article, Reporter +pytestmark = pytest.mark.django_db + + def test_should_query_only_fields(): with raises(Exception): class ReporterType(DjangoObjectType): diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index 38bf7546..4be3c55f 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -66,7 +66,7 @@ def get_filtering_args_from_filterset(filterset_class, type): # Also add the 'order_by' field if filterset_class._meta.order_by: - args[filterset_class.order_by_field] = Argument(String) + args[filterset_class.order_by_field] = Argument(String()) return args From 35d78320e8dd9a1cf91de01881d757c6b09ea591 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 11 Dec 2015 23:39:41 -0800 Subject: [PATCH 24/26] Simplified filter in types and fields. All tests passing --- graphene/contrib/django/fields.py | 9 ++++++--- graphene/contrib/django/types.py | 22 ++-------------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 76a85580..f33cd77e 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -1,6 +1,7 @@ import warnings -from .utils import get_type_for_model +from .utils import get_type_for_model, DJANGO_FILTER_INSTALLED +from .filter.fields import DjangoFilterConnectionField from ...core.exceptions import SkipField from ...core.fields import Field from ...core.types.base import FieldType @@ -20,7 +21,6 @@ class DjangoConnectionField(ConnectionField): class ConnectionOrListField(Field): - connection_field_class = ConnectionField def internal_type(self, schema): model_field = self.type @@ -28,7 +28,10 @@ class ConnectionOrListField(Field): if not field_object_type: raise SkipField() if is_node(field_object_type): - field = self.connection_field_class(field_object_type) + if field_object_type._meta.filter_fields: + field = DjangoFilterConnectionField(field_object_type) + else: + field = ConnectionField(field_object_type) else: field = Field(List(field_object_type)) field.contribute_to_class(self.object_type, self.attname) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index d8fc1b86..b961ceed 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -30,12 +30,9 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): # We skip this field if we specify only_fields and is not # in there. Or when we exclude this field in exclude_fields continue - converted_field = cls.convert_django_field(field) + converted_field = convert_django_field(field) cls.add_to_class(field.name, converted_field) - def convert_django_field(cls, field): - return convert_django_field(field) - def construct(cls, *args, **kwargs): cls = super(DjangoObjectTypeMeta, cls).construct(*args, **kwargs) if not cls._meta.abstract: @@ -50,15 +47,6 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): return cls -class DjangoFilterObjectTypeMeta(ObjectTypeMeta): - - def convert_django_field(cls, field): - from graphene.contrib.django.filter import DjangoFilterConnectionField - field = super(DjangoFilterObjectTypeMeta, cls).convert_django_field(field) - field.connection_field_class = DjangoFilterConnectionField - return field - - class InstanceObjectType(ObjectType): class Meta: @@ -102,13 +90,7 @@ class DjangoConnection(Connection): return super(DjangoConnection, cls).from_list(iterable, *args, **kwargs) -django_filter_metabase = type -# Only include filter functionality if available -if DJANGO_FILTER_INSTALLED: - django_filter_metabase = DjangoFilterObjectTypeMeta - - -class DjangoNodeMeta(django_filter_metabase, DjangoObjectTypeMeta, NodeMeta): +class DjangoNodeMeta(DjangoObjectTypeMeta, NodeMeta): pass From 8eaa2cfc4918a4a454ab5783083a0d6ace2127d0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 12 Dec 2015 00:40:26 -0800 Subject: [PATCH 25/26] Fixed compatibility with Django 1.6, 1.7, 1.8 and 1.9 --- .travis.yml | 11 ++++++++-- graphene/contrib/django/compat.py | 15 +++++++++++++ graphene/contrib/django/converter.py | 22 ++++++++++--------- graphene/contrib/django/debug/plugin.py | 2 +- graphene/contrib/django/debug/sql/types.py | 2 +- graphene/contrib/django/fields.py | 4 ++-- graphene/contrib/django/filter/fields.py | 2 +- graphene/contrib/django/filter/filterset.py | 11 +++++----- graphene/contrib/django/filter/resolvers.py | 3 ++- graphene/contrib/django/form_converter.py | 7 ++++-- graphene/contrib/django/forms.py | 2 +- graphene/contrib/django/options.py | 2 +- .../contrib/django/tests/filter/filters.py | 4 +--- .../django/tests/filter/test_fields.py | 21 +++++++++++++----- .../django/tests/filter/test_resolvers.py | 6 ++--- .../contrib/django/tests/test_converter.py | 6 ++--- .../django/tests/test_form_converter.py | 3 +-- graphene/contrib/django/tests/test_query.py | 1 - .../contrib/django/tests/test_resolvers.py | 6 +++-- graphene/contrib/django/types.py | 1 - graphene/contrib/django/utils.py | 17 ++++++++++++-- graphene/core/types/field.py | 3 ++- setup.py | 2 +- 23 files changed, 101 insertions(+), 52 deletions(-) create mode 100644 graphene/contrib/django/compat.py diff --git a/.travis.yml b/.travis.yml index 93f4550f..3dbb00e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -73,13 +73,20 @@ after_success: fi env: matrix: - - TEST_TYPE=build DJANGO_VERSION=1.8 - - TEST_TYPE=build DJANGO_VERSION=1.9 + - TEST_TYPE=build global: secure: SQC0eCWCWw8bZxbLE8vQn+UjJOp3Z1m779s9SMK3lCLwJxro/VCLBZ7hj4xsrq1MtcFO2U2Kqf068symw4Hr/0amYI3HFTCFiwXAC3PAKXeURca03eNO2heku+FtnQcOjBanExTsIBQRLDXMOaUkf3MIztpLJ4LHqMfUupKmw9YSB0v40jDbSN8khBnndFykmOnVVHznFp8USoN5F0CiPpnfEvHnJkaX76lNf7Kc9XNShBTTtJsnsHMhuYQeInt0vg9HSjoIYC38Tv2hmMj1myNdzyrHF+LgRjI6ceGi50ApAnGepXC/DNRhXROfECKez+LON/ZSqBGdJhUILqC8A4WmWmIjNcwitVFp3JGBqO7LULS0BI96EtSLe8rD1rkkdTbjivajkbykM1Q0Tnmg1adzGwLxRUbTq9tJQlTTkHBCuXIkpKb1mAtb/TY7A6BqfnPi2xTc/++qEawUG7ePhscdTj0IBrUfZsUNUYZqD8E8XbSWKIuS3SHE+cZ+s/kdAsm4q+FFAlpZKOYGxIkwvgyfu4/Plfol4b7X6iAP9J3r1Kv0DgBVFst5CXEwzZs19/g0CgokQbCXf1N+xeNnUELl6/fImaR3RKP22EaABoil4z8vzl4EqxqVoH1nfhE+WlpryXsuSaF/1R+WklR7aQ1FwoCk8V8HxM2zrj4tI8k= matrix: fast_finish: true include: + - python: '2.7' + env: DJANGO_VERSION=1.6 + - python: '2.7' + env: DJANGO_VERSION=1.7 + - python: '2.7' + env: DJANGO_VERSION=1.8 + - python: '2.7' + env: DJANGO_VERSION=1.9 - python: '2.7' env: TEST_TYPE=build_website - python: '2.7' diff --git a/graphene/contrib/django/compat.py b/graphene/contrib/django/compat.py new file mode 100644 index 00000000..a5b444c7 --- /dev/null +++ b/graphene/contrib/django/compat.py @@ -0,0 +1,15 @@ +from django.db import models + +try: + UUIDField = models.UUIDField +except AttributeError: + # Improved compatibility for Django 1.6 + class UUIDField(object): + pass + +try: + from django.db.models.related import RelatedObject +except: + # Improved compatibility for Django 1.6 + class RelatedObject(object): + pass diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index 0722643b..ef1265ac 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -1,17 +1,11 @@ from django.db import models -from .utils import import_single_dispatch from ...core.types.scalars import ID, Boolean, Float, Int, String +from .compat import RelatedObject, UUIDField +from .utils import get_related_model, import_single_dispatch singledispatch = import_single_dispatch() -try: - UUIDField = models.UUIDField -except AttributeError: - # Improved compatibility for Django 1.6 - class UUIDField(object): - pass - @singledispatch def convert_django_field(field): @@ -65,7 +59,15 @@ def convert_field_to_float(field): @convert_django_field.register(models.ManyToOneRel) def convert_field_to_list_or_connection(field): from .fields import DjangoModelField, ConnectionOrListField - model_field = DjangoModelField(field.related_model) + model_field = DjangoModelField(get_related_model(field)) + return ConnectionOrListField(model_field) + + +# For Django 1.6 +@convert_django_field.register(RelatedObject) +def convert_relatedfield_to_djangomodel(field): + from .fields import DjangoModelField, ConnectionOrListField + model_field = DjangoModelField(field.model) return ConnectionOrListField(model_field) @@ -73,4 +75,4 @@ def convert_field_to_list_or_connection(field): @convert_django_field.register(models.ForeignKey) def convert_field_to_djangomodel(field): from .fields import DjangoModelField - return DjangoModelField(field.related_model, description=field.help_text) + return DjangoModelField(get_related_model(field), description=field.help_text) diff --git a/graphene/contrib/django/debug/plugin.py b/graphene/contrib/django/debug/plugin.py index 86f8da58..70cd6741 100644 --- a/graphene/contrib/django/debug/plugin.py +++ b/graphene/contrib/django/debug/plugin.py @@ -2,8 +2,8 @@ from contextlib import contextmanager from django.db import connections -from ....core.types import Field from ....core.schema import GraphQLSchema +from ....core.types import Field from ....plugins import Plugin from .sql.tracking import unwrap_cursor, wrap_cursor from .sql.types import DjangoDebugSQL diff --git a/graphene/contrib/django/debug/sql/types.py b/graphene/contrib/django/debug/sql/types.py index 5df5e9d8..995aeaa2 100644 --- a/graphene/contrib/django/debug/sql/types.py +++ b/graphene/contrib/django/debug/sql/types.py @@ -1,4 +1,4 @@ -from .....core import Float, ObjectType, String, Boolean +from .....core import Boolean, Float, ObjectType, String class DjangoDebugSQL(ObjectType): diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index f33cd77e..d9d6f3da 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -1,13 +1,13 @@ import warnings -from .utils import get_type_for_model, DJANGO_FILTER_INSTALLED -from .filter.fields import DjangoFilterConnectionField from ...core.exceptions import SkipField from ...core.fields import Field from ...core.types.base import FieldType from ...core.types.definitions import List from ...relay import ConnectionField from ...relay.utils import is_node +from .filter.fields import DjangoFilterConnectionField +from .utils import get_type_for_model class DjangoConnectionField(ConnectionField): diff --git a/graphene/contrib/django/filter/fields.py b/graphene/contrib/django/filter/fields.py index 012ae00a..43196f6e 100644 --- a/graphene/contrib/django/filter/fields.py +++ b/graphene/contrib/django/filter/fields.py @@ -1,6 +1,6 @@ -from graphene.relay import ConnectionField from graphene.contrib.django.filter.resolvers import FilterConnectionResolver from graphene.contrib.django.utils import get_filtering_args_from_filterset +from graphene.relay import ConnectionField class DjangoFilterConnectionField(ConnectionField): diff --git a/graphene/contrib/django/filter/filterset.py b/graphene/contrib/django/filter/filterset.py index 4755eac2..3ecd9680 100644 --- a/graphene/contrib/django/filter/filterset.py +++ b/graphene/contrib/django/filter/filterset.py @@ -2,11 +2,12 @@ import six from django.conf import settings from django.db import models from django.utils.text import capfirst -from django_filters import Filter, MultipleChoiceFilter -from django_filters.filterset import FilterSetMetaclass, FilterSet -from graphql_relay.node.node import from_global_id -from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField +from django_filters import Filter, MultipleChoiceFilter +from django_filters.filterset import FilterSet, FilterSetMetaclass +from graphene.contrib.django.forms import (GlobalIDFormField, + GlobalIDMultipleChoiceField) +from graphql_relay.node.node import from_global_id class GlobalIDFilter(Filter): @@ -45,6 +46,7 @@ GRAPHENE_FILTER_SET_OVERRIDES = { class GrapheneFilterSetMetaclass(FilterSetMetaclass): + def __new__(cls, name, bases, attrs): new_class = super(GrapheneFilterSetMetaclass, cls).__new__(cls, name, bases, attrs) # Customise the filter_overrides for Graphene @@ -84,7 +86,6 @@ class GrapheneFilterSet(six.with_metaclass(GrapheneFilterSetMetaclass, GrapheneF DjangoFilterConnectionField will wrap FilterSets with this class as necessary """ - pass def setup_filterset(filterset_class): diff --git a/graphene/contrib/django/filter/resolvers.py b/graphene/contrib/django/filter/resolvers.py index c2204d6c..76b3e7ad 100644 --- a/graphene/contrib/django/filter/resolvers.py +++ b/graphene/contrib/django/filter/resolvers.py @@ -1,6 +1,7 @@ from django.core.exceptions import ImproperlyConfigured -from graphene.contrib.django.filter.filterset import setup_filterset, custom_filterset_factory +from graphene.contrib.django.filter.filterset import (custom_filterset_factory, + setup_filterset) from graphene.contrib.django.resolvers import BaseQuerySetConnectionResolver diff --git a/graphene/contrib/django/form_converter.py b/graphene/contrib/django/form_converter.py index 826c8c69..de2a40d8 100644 --- a/graphene/contrib/django/form_converter.py +++ b/graphene/contrib/django/form_converter.py @@ -1,9 +1,12 @@ from django import forms from django.forms.fields import BaseTemporalField -from graphene import String, Int, Boolean, Float, ID -from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField + +from graphene import ID, Boolean, Float, Int, String +from graphene.contrib.django.forms import (GlobalIDFormField, + GlobalIDMultipleChoiceField) from graphene.contrib.django.utils import import_single_dispatch from graphene.core.types.definitions import List + singledispatch = import_single_dispatch() try: diff --git a/graphene/contrib/django/forms.py b/graphene/contrib/django/forms.py index f971897b..88f1665e 100644 --- a/graphene/contrib/django/forms.py +++ b/graphene/contrib/django/forms.py @@ -1,7 +1,7 @@ import binascii from django.core.exceptions import ValidationError -from django.forms import Field, IntegerField, CharField, MultipleChoiceField +from django.forms import CharField, Field, IntegerField, MultipleChoiceField from django.utils.translation import ugettext_lazy as _ from graphql_relay import from_global_id diff --git a/graphene/contrib/django/options.py b/graphene/contrib/django/options.py index 55868dd7..dbd88aca 100644 --- a/graphene/contrib/django/options.py +++ b/graphene/contrib/django/options.py @@ -1,7 +1,7 @@ -from .utils import DJANGO_FILTER_INSTALLED from ...core.classtypes.objecttype import ObjectTypeOptions from ...relay.types import Node from ...relay.utils import is_node +from .utils import DJANGO_FILTER_INSTALLED VALID_ATTRS = ('model', 'only_fields', 'exclude_fields') diff --git a/graphene/contrib/django/tests/filter/filters.py b/graphene/contrib/django/tests/filter/filters.py index 4549a83e..94c0dffe 100644 --- a/graphene/contrib/django/tests/filter/filters.py +++ b/graphene/contrib/django/tests/filter/filters.py @@ -1,7 +1,5 @@ import django_filters - -from graphene.contrib.django.tests.models import Reporter -from graphene.contrib.django.tests.models import Article, Pet +from graphene.contrib.django.tests.models import Article, Pet, Reporter class ArticleFilter(django_filters.FilterSet): diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index 4c93ca1e..efa1757f 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -1,14 +1,13 @@ import pytest from graphene import ObjectType, Schema +from graphene.contrib.django import DjangoNode +from graphene.contrib.django.forms import (GlobalIDFormField, + GlobalIDMultipleChoiceField) +from graphene.contrib.django.tests.models import Article, Pet, Reporter from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED from graphene.relay import NodeField - -from graphene.contrib.django import DjangoNode -from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField -from graphene.contrib.django.tests.models import Article, Pet, Reporter - pytestmark = [] if DJANGO_FILTER_INSTALLED: import django_filters @@ -22,21 +21,25 @@ pytestmark.append(pytest.mark.django_db) class ArticleNode(DjangoNode): + class Meta: model = Article class ReporterNode(DjangoNode): + class Meta: model = Reporter class PetNode(DjangoNode): + class Meta: model = Pet schema = Schema() + def assert_arguments(field, *arguments): ignore = ('after', 'before', 'first', 'last', 'orderBy') actual = [ @@ -48,7 +51,7 @@ def assert_arguments(field, *arguments): 'Expected arguments ({}) did not match actual ({})'.format( arguments, actual - ) + ) def assert_orderable(field): @@ -118,6 +121,7 @@ def test_filter_shortcut_filterset_extra_meta(): def test_filter_filterset_information_on_meta(): class ReporterFilterNode(DjangoNode): + class Meta: model = Reporter filter_fields = ['first_name', 'articles'] @@ -130,12 +134,14 @@ def test_filter_filterset_information_on_meta(): def test_filter_filterset_information_on_meta_related(): class ReporterFilterNode(DjangoNode): + class Meta: model = Reporter filter_fields = ['first_name', 'articles'] filter_order_by = True class ArticleFilterNode(DjangoNode): + class Meta: model = Article filter_fields = ['headline', 'reporter'] @@ -164,6 +170,7 @@ def test_global_id_field_implicit(): def test_global_id_field_explicit(): class ArticleIdFilter(django_filters.FilterSet): + class Meta: model = Article fields = ['id'] @@ -193,6 +200,7 @@ def test_global_id_multiple_field_implicit(): def test_global_id_multiple_field_explicit(): class ReporterPetsFilter(django_filters.FilterSet): + class Meta: model = Reporter fields = ['pets'] @@ -214,6 +222,7 @@ def test_global_id_multiple_field_implicit_reverse(): def test_global_id_multiple_field_explicit_reverse(): class ReporterPetsFilter(django_filters.FilterSet): + class Meta: model = Reporter fields = ['articles'] diff --git a/graphene/contrib/django/tests/filter/test_resolvers.py b/graphene/contrib/django/tests/filter/test_resolvers.py index a336cddf..dd9940f0 100644 --- a/graphene/contrib/django/tests/filter/test_resolvers.py +++ b/graphene/contrib/django/tests/filter/test_resolvers.py @@ -1,6 +1,9 @@ import pytest from django.core.exceptions import ImproperlyConfigured +from graphene.contrib.django.tests.models import Article, Reporter +from graphene.contrib.django.tests.test_resolvers import (ArticleNode, + ReporterNode) from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED if DJANGO_FILTER_INSTALLED: @@ -9,9 +12,6 @@ if DJANGO_FILTER_INSTALLED: else: pytestmark = pytest.mark.skipif(True, reason='django_filters not installed') -from graphene.contrib.django.tests.models import Reporter, Article -from graphene.contrib.django.tests.test_resolvers import ReporterNode, ArticleNode - def test_filter_get_filterset_class_explicit(): reporter = Reporter(id=1, first_name='Cookie Monster') diff --git a/graphene/contrib/django/tests/test_converter.py b/graphene/contrib/django/tests/test_converter.py index dcbb3e30..3a02b03a 100644 --- a/graphene/contrib/django/tests/test_converter.py +++ b/graphene/contrib/django/tests/test_converter.py @@ -9,8 +9,8 @@ from graphene.contrib.django.fields import (ConnectionOrListField, from .models import Article, Reporter -def assert_conversion(django_field, graphene_field, *args): - field = django_field(*args, help_text='Custom Help Text') +def assert_conversion(django_field, graphene_field, *args, **kwargs): + field = django_field(help_text='Custom Help Text', *args, **kwargs) graphene_type = convert_django_field(field) assert isinstance(graphene_type, graphene_field) field = graphene_type.as_field() @@ -49,7 +49,7 @@ def test_should_url_convert_string(): def test_should_auto_convert_id(): - assert_conversion(models.AutoField, graphene.ID) + assert_conversion(models.AutoField, graphene.ID, primary_key=True) def test_should_positive_integer_convert_int(): diff --git a/graphene/contrib/django/tests/test_form_converter.py b/graphene/contrib/django/tests/test_form_converter.py index 7492fc51..44d9bec3 100644 --- a/graphene/contrib/django/tests/test_form_converter.py +++ b/graphene/contrib/django/tests/test_form_converter.py @@ -1,10 +1,9 @@ from django import forms -from graphene.core.types import List, ID from py.test import raises import graphene from graphene.contrib.django.form_converter import convert_form_field - +from graphene.core.types import ID, List from .models import Reporter diff --git a/graphene/contrib/django/tests/test_query.py b/graphene/contrib/django/tests/test_query.py index 4b37d517..460c8e22 100644 --- a/graphene/contrib/django/tests/test_query.py +++ b/graphene/contrib/django/tests/test_query.py @@ -7,7 +7,6 @@ from graphene.contrib.django import DjangoNode, DjangoObjectType from .models import Article, Reporter - pytestmark = pytest.mark.django_db diff --git a/graphene/contrib/django/tests/test_resolvers.py b/graphene/contrib/django/tests/test_resolvers.py index fe617666..db1610c9 100644 --- a/graphene/contrib/django/tests/test_resolvers.py +++ b/graphene/contrib/django/tests/test_resolvers.py @@ -3,15 +3,17 @@ from django.db.models.query import QuerySet from graphene.contrib.django import DjangoNode from graphene.contrib.django.resolvers import SimpleQuerySetConnectionResolver -from graphene.contrib.django.tests.models import Reporter, Article +from graphene.contrib.django.tests.models import Article, Reporter class ReporterNode(DjangoNode): + class Meta: model = Reporter class ArticleNode(DjangoNode): + class Meta: model = Article @@ -34,7 +36,7 @@ def test_simple_get_manager_all(): reporter = Reporter(id=1, first_name='Cookie Monster') resolver = SimpleQuerySetConnectionResolver(ReporterNode) resolver(inst=reporter, args={}, info=None) - assert type(resolver.get_manager()) == Manager, 'Resolver did not return a Manager' + assert isinstance(resolver.get_manager(), Manager), 'Resolver did not return a Manager' def test_simple_filter(): diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index b961ceed..5b68ebbb 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -5,7 +5,6 @@ from django.db import models from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta from ...relay.types import Connection, Node, NodeMeta -from .utils import DJANGO_FILTER_INSTALLED from .converter import convert_django_field from .options import DjangoOptions from .utils import get_reverse_fields, maybe_queryset diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index 4be3c55f..76f4477c 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -3,9 +3,10 @@ from django.db import models from django.db.models.manager import Manager from django.db.models.query import QuerySet +from graphene import Argument, String from graphene.utils import LazyList -from graphene import Argument, String +from .compat import RelatedObject try: import django_filters # noqa @@ -29,7 +30,12 @@ def get_reverse_fields(model): # Django =>1.9 uses 'rel', django <1.9 uses 'related' related = getattr(attr, 'rel', None) or \ getattr(attr, 'related', None) - if isinstance(related, models.ManyToOneRel): + if isinstance(related, RelatedObject): + # Hack for making it compatible with Django 1.6 + new_related = RelatedObject(related.parent_model, related.model, related.field) + new_related.name = name + yield new_related + elif isinstance(related, models.ManyToOneRel): yield related @@ -70,6 +76,13 @@ def get_filtering_args_from_filterset(filterset_class, type): return args +def get_related_model(field): + if hasattr(field, 'rel'): + # Django 1.6, 1.7 + return field.rel.to + return field.related_model + + def import_single_dispatch(): try: from functools import singledispatch diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index 17fc9fa2..6cbfff96 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -9,7 +9,8 @@ from ..classtypes.inputobjecttype import InputObjectType from ..classtypes.mutation import Mutation from ..exceptions import SkipField from .argument import Argument, ArgumentsGroup, snake_case_args -from .base import GroupNamedType, LazyType, MountType, NamedType, ArgumentType, OrderedType +from .base import (ArgumentType, GroupNamedType, LazyType, MountType, + NamedType, OrderedType) from .definitions import NonNull diff --git a/setup.py b/setup.py index 74865f3f..ac0e3d0f 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( ], extras_require={ 'django': [ - 'Django>=1.8.0', + 'Django>=1.6.0', 'singledispatch>=3.4.0.3', 'graphql-django-view>=1.1.0', ], From c7026329d3a5a0186019b5d5f774ec5e83a74e0c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 12 Dec 2015 00:54:51 -0800 Subject: [PATCH 26/26] Moved django filter tests --- .../contrib/django/{tests/filter => filter/tests}/__init__.py | 0 .../contrib/django/{tests/filter => filter/tests}/filters.py | 0 .../django/{tests/filter => filter/tests}/test_fields.py | 2 +- .../django/{tests/filter => filter/tests}/test_resolvers.py | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename graphene/contrib/django/{tests/filter => filter/tests}/__init__.py (100%) rename graphene/contrib/django/{tests/filter => filter/tests}/filters.py (100%) rename graphene/contrib/django/{tests/filter => filter/tests}/test_fields.py (99%) rename graphene/contrib/django/{tests/filter => filter/tests}/test_resolvers.py (97%) diff --git a/graphene/contrib/django/tests/filter/__init__.py b/graphene/contrib/django/filter/tests/__init__.py similarity index 100% rename from graphene/contrib/django/tests/filter/__init__.py rename to graphene/contrib/django/filter/tests/__init__.py diff --git a/graphene/contrib/django/tests/filter/filters.py b/graphene/contrib/django/filter/tests/filters.py similarity index 100% rename from graphene/contrib/django/tests/filter/filters.py rename to graphene/contrib/django/filter/tests/filters.py diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/filter/tests/test_fields.py similarity index 99% rename from graphene/contrib/django/tests/filter/test_fields.py rename to graphene/contrib/django/filter/tests/test_fields.py index efa1757f..45c1f0d0 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/filter/tests/test_fields.py @@ -13,7 +13,7 @@ if DJANGO_FILTER_INSTALLED: import django_filters from graphene.contrib.django.filter import (GlobalIDFilter, DjangoFilterConnectionField, GlobalIDMultipleChoiceFilter) - from graphene.contrib.django.tests.filter.filters import ArticleFilter, PetFilter + from graphene.contrib.django.filter.tests.filters import ArticleFilter, PetFilter else: pytestmark.append(pytest.mark.skipif(True, reason='django_filters not installed')) diff --git a/graphene/contrib/django/tests/filter/test_resolvers.py b/graphene/contrib/django/filter/tests/test_resolvers.py similarity index 97% rename from graphene/contrib/django/tests/filter/test_resolvers.py rename to graphene/contrib/django/filter/tests/test_resolvers.py index dd9940f0..670e87c8 100644 --- a/graphene/contrib/django/tests/filter/test_resolvers.py +++ b/graphene/contrib/django/filter/tests/test_resolvers.py @@ -8,7 +8,7 @@ from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED if DJANGO_FILTER_INSTALLED: from graphene.contrib.django.filter.resolvers import FilterConnectionResolver - from graphene.contrib.django.tests.filter.filters import ReporterFilter, ArticleFilter + from graphene.contrib.django.filter.tests.filters import ArticleFilter, ReporterFilter else: pytestmark = pytest.mark.skipif(True, reason='django_filters not installed')