From 25fd60d1ffed3e74f3494738aa3a3e693d4a0f4b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 20 May 2016 00:16:34 -0700 Subject: [PATCH 1/6] Improved plugins --- graphene/contrib/django/debug/plugin.py | 30 ++++++++++------------ graphene/core/schema.py | 30 ++++++++++++++++++++++ graphene/core/types/argument.py | 10 -------- graphene/core/types/field.py | 9 +++---- graphene/core/types/tests/test_argument.py | 9 +------ graphene/plugins/camel_case.py | 5 +++- 6 files changed, 52 insertions(+), 41 deletions(-) diff --git a/graphene/contrib/django/debug/plugin.py b/graphene/contrib/django/debug/plugin.py index 2d930673..46030ef5 100644 --- a/graphene/contrib/django/debug/plugin.py +++ b/graphene/contrib/django/debug/plugin.py @@ -1,6 +1,7 @@ from contextlib import contextmanager from django.db import connections +from graphene import with_context from ....core.schema import GraphQLSchema from ....core.types import Field @@ -10,11 +11,14 @@ from .sql.types import DjangoDebugSQL from .types import DjangoDebug -class WrappedRoot(object): +class EmptyContext(object): + pass - def __init__(self, root): + +class DjangoDebugContext(object): + + def __init__(self): self._recorded = [] - self._root = root def record(self, **log): self._recorded.append(DjangoDebugSQL(**log)) @@ -24,17 +28,9 @@ class WrappedRoot(object): 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() + @with_context + def resolve_debug(self, args, context, info): + return context.django_debug.debug() def debug_objecttype(objecttype): @@ -72,8 +68,10 @@ class DjangoDebugPlugin(Plugin): @contextmanager def context_execution(self, executor): - executor['root_value'] = WrappedRoot(root=executor.get('root_value')) + context_value = executor.get('context_value') or EmptyContext() + context_value.django_debug = DjangoDebugContext() + executor['context_value'] = context_value executor['schema'] = self.wrap_schema(executor['schema']) - self.enable_instrumentation(executor['root_value']) + self.enable_instrumentation(context_value.django_debug) yield executor self.disable_instrumentation() diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 966e1fd2..b9e6f56d 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -12,6 +12,16 @@ from .classtypes.base import ClassType from .types.base import InstanceType +class ACI(object): + def __init__(self, args, context, info): + self.args = args + self.context = context + self.info = info + + def __repr__(self): + return "ACI(args={}, context={}, info={})".format(repr(self.args), repr(self.context), repr(self.info)) + + class GraphQLSchema(_GraphQLSchema): def __init__(self, schema, *args, **kwargs): @@ -111,6 +121,26 @@ class Schema(object): raise KeyError('Type %r not found in %r' % (type_name, self)) return self._types_names[type_name] + def resolve(self, resolver, root, args, context, info): + aci = ACI(args, context, info) + plugins_process_aci = self.plugins.get_plugin_functions('process_aci') + plugins_process_response = self.plugins.get_plugin_functions('process_response') + for process_aci in plugins_process_aci: + processed_aci = process_aci(aci) + if processed_aci is None: + continue + return processed_aci + + response = resolver(root, aci.args, aci.context, aci.info) + + for process_response in plugins_process_response: + processed_response = process_response(response, aci) + if processed_response is None: + continue + return processed_response + + return response + @property def types(self): return self._types_names diff --git a/graphene/core/types/argument.py b/graphene/core/types/argument.py index 80039ede..88a8c92f 100644 --- a/graphene/core/types/argument.py +++ b/graphene/core/types/argument.py @@ -1,9 +1,7 @@ -from functools import wraps from itertools import chain from graphql.type import GraphQLArgument -from ...utils import ProxySnakeDict from .base import ArgumentType, GroupNamedType, NamedType, OrderedType @@ -53,11 +51,3 @@ def to_arguments(*args, **kwargs): arguments[name] = argument return sorted(arguments.values()) - - -def snake_case_args(resolver): - @wraps(resolver) - def wrapped_resolver(instance, args, context, info): - return resolver(instance, ProxySnakeDict(args), context, info) - - return wrapped_resolver diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index db17a465..837f505e 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -1,5 +1,5 @@ from collections import OrderedDict -from functools import wraps +from functools import wraps, partial import six from graphql.type import GraphQLField, GraphQLInputObjectField @@ -10,7 +10,7 @@ from ..classtypes.base import FieldsClassType from ..classtypes.inputobjecttype import InputObjectType from ..classtypes.mutation import Mutation from ..exceptions import SkipField -from .argument import Argument, ArgumentsGroup, snake_case_args +from .argument import Argument, ArgumentsGroup from .base import (ArgumentType, GroupNamedType, LazyType, MountType, NamedType, OrderedType) from .definitions import NonNull @@ -89,9 +89,6 @@ class Field(NamedType, OrderedType): return NonNull(self.type) return self.type - def decorate_resolver(self, resolver): - return snake_case_args(resolver) - def internal_type(self, schema): if not self.object_type: raise Exception('The field is not mounted in any ClassType') @@ -119,7 +116,7 @@ class Field(NamedType, OrderedType): assert type, 'Internal type for field %s is None' % str(self) return GraphQLField(type, args=schema.T(arguments), - resolver=self.decorate_resolver(resolver), + resolver=partial(schema.resolve, resolver), deprecation_reason=self.deprecation_reason, description=description,) diff --git a/graphene/core/types/tests/test_argument.py b/graphene/core/types/tests/test_argument.py index 4482c11c..bd9cda10 100644 --- a/graphene/core/types/tests/test_argument.py +++ b/graphene/core/types/tests/test_argument.py @@ -4,7 +4,7 @@ from pytest import raises from graphene.core.schema import Schema from graphene.core.types import ObjectType -from ..argument import Argument, snake_case_args, to_arguments +from ..argument import Argument, to_arguments from ..scalars import String @@ -45,10 +45,3 @@ def test_to_arguments_wrong_type(): p=3 ) assert 'Unknown argument p=3' == str(excinfo.value) - - -def test_snake_case_args(): - def resolver(instance, args, context, info): - return args['my_arg']['inner_arg'] - r = snake_case_args(resolver) - assert r(None, {'myArg': {'innerArg': 3}}, None, None) == 3 diff --git a/graphene/plugins/camel_case.py b/graphene/plugins/camel_case.py index d9a9084f..8b1c2bde 100644 --- a/graphene/plugins/camel_case.py +++ b/graphene/plugins/camel_case.py @@ -1,7 +1,10 @@ -from ..utils import to_camel_case +from ..utils import to_camel_case, ProxySnakeDict class CamelCase(object): def get_default_namedtype_name(self, value): return to_camel_case(value) + + def process_aci(self, aci): + aci.args = ProxySnakeDict(aci.args) From 8421b59d3a0a24882e9ff15666f84d4b348ee3be Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 20 May 2016 21:44:31 -0700 Subject: [PATCH 2/6] Improved resolver logic middleware by using promises --- graphene/core/schema.py | 33 +++---------------------- graphene/core/tests/test_old_fields.py | 2 +- graphene/core/types/field.py | 14 +++++++---- graphene/core/types/tests/test_field.py | 8 +++--- graphene/plugins/camel_case.py | 5 ++-- graphene/utils/__init__.py | 3 ++- graphene/utils/promise_middleware.py | 17 +++++++++++++ 7 files changed, 39 insertions(+), 43 deletions(-) create mode 100644 graphene/utils/promise_middleware.py diff --git a/graphene/core/schema.py b/graphene/core/schema.py index b9e6f56d..46e8768f 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -8,20 +8,11 @@ from graphql.utils.schema_printer import print_schema from graphene import signals from ..plugins import CamelCase, PluginManager +from ..utils import promise_middleware from .classtypes.base import ClassType from .types.base import InstanceType -class ACI(object): - def __init__(self, args, context, info): - self.args = args - self.context = context - self.info = info - - def __repr__(self): - return "ACI(args={}, context={}, info={})".format(repr(self.args), repr(self.context), repr(self.info)) - - class GraphQLSchema(_GraphQLSchema): def __init__(self, schema, *args, **kwargs): @@ -121,25 +112,9 @@ class Schema(object): raise KeyError('Type %r not found in %r' % (type_name, self)) return self._types_names[type_name] - def resolve(self, resolver, root, args, context, info): - aci = ACI(args, context, info) - plugins_process_aci = self.plugins.get_plugin_functions('process_aci') - plugins_process_response = self.plugins.get_plugin_functions('process_response') - for process_aci in plugins_process_aci: - processed_aci = process_aci(aci) - if processed_aci is None: - continue - return processed_aci - - response = resolver(root, aci.args, aci.context, aci.info) - - for process_response in plugins_process_response: - processed_response = process_response(response, aci) - if processed_response is None: - continue - return processed_response - - return response + def resolver_with_middleware(self, resolver): + plugins_resolve = self.plugins.get_plugin_functions('resolve') + return promise_middleware(resolver, plugins_resolve) @property def types(self): diff --git a/graphene/core/tests/test_old_fields.py b/graphene/core/tests/test_old_fields.py index a1d8ac60..696115d0 100644 --- a/graphene/core/tests/test_old_fields.py +++ b/graphene/core/tests/test_old_fields.py @@ -93,7 +93,7 @@ def test_field_resolve(): f = StringField(required=True, resolve=lambda *args: 'RESOLVED').as_field() f.contribute_to_class(MyOt, 'field_name') field_type = schema.T(f) - assert 'RESOLVED' == field_type.resolver(MyOt, None, None, None) + assert 'RESOLVED' == field_type.resolver(MyOt, None, None, None).value def test_field_resolve_type_custom(): diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index 837f505e..e7a36f84 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -115,10 +115,13 @@ class Field(NamedType, OrderedType): resolver = wrapped_func assert type, 'Internal type for field %s is None' % str(self) - return GraphQLField(type, args=schema.T(arguments), - resolver=partial(schema.resolve, resolver), - deprecation_reason=self.deprecation_reason, - description=description,) + return GraphQLField( + type, + args=schema.T(arguments), + resolver=schema.resolver_with_middleware(resolver), + deprecation_reason=self.deprecation_reason, + description=description, + ) def __repr__(self): """ @@ -172,7 +175,8 @@ class InputField(NamedType, OrderedType): def internal_type(self, schema): return GraphQLInputObjectField( schema.T(self.type), - default_value=self.default, description=self.description) + default_value=self.default, description=self.description + ) class FieldsGroupType(GroupNamedType): diff --git a/graphene/core/types/tests/test_field.py b/graphene/core/types/tests/test_field.py index eb99594c..38792705 100644 --- a/graphene/core/types/tests/test_field.py +++ b/graphene/core/types/tests/test_field.py @@ -24,7 +24,7 @@ def test_field_internal_type(): assert field.attname == 'my_field' assert isinstance(type, GraphQLField) assert type.description == 'My argument' - assert type.resolver(None, {}, None, None) == 'RESOLVED' + assert type.resolver(None, {}, None, None).value == 'RESOLVED' assert type.type == GraphQLString @@ -43,7 +43,7 @@ def test_field_objectype_resolver(): type = schema.T(field) assert isinstance(type, GraphQLField) assert type.description == 'Custom description' - assert type.resolver(Query(), {}, None, None) == 'RESOLVED' + assert type.resolver(Query(), {}, None, None).value == 'RESOLVED' def test_field_custom_name(): @@ -161,7 +161,7 @@ def test_field_resolve_argument(): schema = Schema(query=Query) type = schema.T(field) - assert type.resolver(None, {'firstName': 'Peter'}, None, None) == 'Peter' + assert type.resolver(None, {'firstName': 'Peter'}, None, None).value == 'Peter' def test_field_resolve_vars(): @@ -216,7 +216,6 @@ def test_field_resolve_object(): att_func = field_func assert field.resolver(Root, {}, None) is True - assert field.resolver(Root, {}, None) is True def test_field_resolve_source_object(): @@ -235,4 +234,3 @@ def test_field_resolve_source_object(): att_func = field_func assert field.resolver(Root, {}, None) is True - assert field.resolver(Root, {}, None) is True diff --git a/graphene/plugins/camel_case.py b/graphene/plugins/camel_case.py index 8b1c2bde..1f1c6690 100644 --- a/graphene/plugins/camel_case.py +++ b/graphene/plugins/camel_case.py @@ -6,5 +6,6 @@ class CamelCase(object): def get_default_namedtype_name(self, value): return to_camel_case(value) - def process_aci(self, aci): - aci.args = ProxySnakeDict(aci.args) + def resolve(self, next, root, args, context, info): + args = ProxySnakeDict(args) + return next(root, args, context, info) diff --git a/graphene/utils/__init__.py b/graphene/utils/__init__.py index 2ed0f036..261f33e3 100644 --- a/graphene/utils/__init__.py +++ b/graphene/utils/__init__.py @@ -3,6 +3,7 @@ from .proxy_snake_dict import ProxySnakeDict from .caching import cached_property, memoize from .maybe_func import maybe_func from .misc import enum_to_graphql_enum +from .promise_middleware import promise_middleware from .resolve_only_args import resolve_only_args from .lazylist import LazyList from .wrap_resolver_function import with_context, wrap_resolver_function @@ -10,5 +11,5 @@ from .wrap_resolver_function import with_context, wrap_resolver_function __all__ = ['to_camel_case', 'to_snake_case', 'to_const', 'ProxySnakeDict', 'cached_property', 'memoize', 'maybe_func', 'enum_to_graphql_enum', - 'resolve_only_args', 'LazyList', 'with_context', + 'promise_middleware', 'resolve_only_args', 'LazyList', 'with_context', 'wrap_resolver_function'] diff --git a/graphene/utils/promise_middleware.py b/graphene/utils/promise_middleware.py new file mode 100644 index 00000000..5190ed02 --- /dev/null +++ b/graphene/utils/promise_middleware.py @@ -0,0 +1,17 @@ +from functools import partial +from itertools import chain + +from promise import Promise + + +def promise_middleware(func, middlewares): + middlewares = chain((func, make_it_promise), middlewares) + past = None + for m in middlewares: + past = partial(m, past) if past else m + + return past + + +def make_it_promise(next, *a, **b): + return Promise.resolve(next(*a, **b)) From 3428725314a29a3196fb858ecf51c73e4ba64679 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 20 May 2016 23:25:54 -0700 Subject: [PATCH 3/6] First phase of middlewares --- graphene/contrib/django/debug/__init__.py | 4 +- graphene/contrib/django/debug/middleware.py | 56 ++++++++++++++ graphene/contrib/django/debug/plugin.py | 77 ------------------- graphene/contrib/django/debug/sql/tracking.py | 7 +- graphene/contrib/django/debug/sql/types.py | 8 +- .../contrib/django/debug/tests/test_query.py | 26 ++++--- graphene/contrib/django/debug/types.py | 4 +- graphene/core/schema.py | 10 +-- graphene/core/types/base.py | 6 +- 9 files changed, 96 insertions(+), 102 deletions(-) create mode 100644 graphene/contrib/django/debug/middleware.py delete mode 100644 graphene/contrib/django/debug/plugin.py diff --git a/graphene/contrib/django/debug/__init__.py b/graphene/contrib/django/debug/__init__.py index 4c76aeca..cd5015e1 100644 --- a/graphene/contrib/django/debug/__init__.py +++ b/graphene/contrib/django/debug/__init__.py @@ -1,4 +1,4 @@ -from .plugin import DjangoDebugPlugin +from .middleware import DjangoDebugMiddleware from .types import DjangoDebug -__all__ = ['DjangoDebugPlugin', 'DjangoDebug'] +__all__ = ['DjangoDebugMiddleware', 'DjangoDebug'] diff --git a/graphene/contrib/django/debug/middleware.py b/graphene/contrib/django/debug/middleware.py new file mode 100644 index 00000000..adf551ad --- /dev/null +++ b/graphene/contrib/django/debug/middleware.py @@ -0,0 +1,56 @@ +from promise import Promise +from django.db import connections + +from ....core.schema import GraphQLSchema +from ....core.types import Field +from .sql.tracking import unwrap_cursor, wrap_cursor +from .types import DjangoDebug + + +class DjangoDebugContext(object): + + def __init__(self): + self.debug_promise = None + self.promises = [] + self.enable_instrumentation() + self.object = DjangoDebug(sql=[]) + + def get_debug_promise(self): + if not self.debug_promise: + self.debug_promise = Promise.all(self.promises) + return self.debug_promise.then(self.on_resolve_all_promises) + + def on_resolve_all_promises(self, values): + self.disable_instrumentation() + return self.object + + def add_promise(self, promise): + if self.debug_promise and not self.debug_promise.is_fulfilled: + self.promises.append(promise) + + def enable_instrumentation(self): + # This is thread-safe because database connections are thread-local. + for connection in connections.all(): + wrap_cursor(connection, self) + + def disable_instrumentation(self): + for connection in connections.all(): + unwrap_cursor(connection) + + +class DjangoDebugMiddleware(object): + + def resolve(self, next, root, args, context, info): + django_debug = getattr(context, 'django_debug', None) + if not django_debug: + if context is None: + raise Exception('DjangoDebug cannot be executed in None contexts') + try: + context.django_debug = DjangoDebugContext() + except Exception, e: + raise Exception('DjangoDebug need the context to be writable, context received: {}.'.format(context.__class__.__name__)) + if info.schema.graphene_schema.T(DjangoDebug) == info.return_type: + return context.django_debug.get_debug_promise() + promise = next(root, args, context, info) + context.django_debug.add_promise(promise) + return promise diff --git a/graphene/contrib/django/debug/plugin.py b/graphene/contrib/django/debug/plugin.py deleted file mode 100644 index 46030ef5..00000000 --- a/graphene/contrib/django/debug/plugin.py +++ /dev/null @@ -1,77 +0,0 @@ -from contextlib import contextmanager - -from django.db import connections -from graphene import with_context - -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 -from .types import DjangoDebug - - -class EmptyContext(object): - pass - - -class DjangoDebugContext(object): - - def __init__(self): - self._recorded = [] - - def record(self, **log): - self._recorded.append(DjangoDebugSQL(**log)) - - def debug(self): - return DjangoDebug(sql=self._recorded) - - -class WrapRoot(object): - @with_context - def resolve_debug(self, args, context, info): - return context.django_debug.debug() - - -def debug_objecttype(objecttype): - return type( - 'Debug{}'.format(objecttype._meta.type_name), - (WrapRoot, objecttype), - {'debug': Field(DjangoDebug, name='__debug')}) - - -class DjangoDebugPlugin(Plugin): - - 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 wrap_schema(self, schema_type): - query = schema_type._query - if 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) - 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 - def context_execution(self, executor): - context_value = executor.get('context_value') or EmptyContext() - context_value.django_debug = DjangoDebugContext() - executor['context_value'] = context_value - executor['schema'] = self.wrap_schema(executor['schema']) - self.enable_instrumentation(context_value.django_debug) - yield executor - self.disable_instrumentation() diff --git a/graphene/contrib/django/debug/sql/tracking.py b/graphene/contrib/django/debug/sql/tracking.py index 47f7a30c..ab31dbe5 100644 --- a/graphene/contrib/django/debug/sql/tracking.py +++ b/graphene/contrib/django/debug/sql/tracking.py @@ -8,6 +8,7 @@ from time import time from django.utils import six from django.utils.encoding import force_text +from .types import DjangoDebugSQL, DjangoDebugPostgreSQL class SQLQueryTriggered(Exception): """Thrown when template panel triggers a query""" @@ -139,9 +140,11 @@ class NormalCursorWrapper(object): 'iso_level': iso_level, 'encoding': conn.encoding, }) - + _sql = DjangoDebugPostgreSQL(**params) + else: + _sql = DjangoDebugSQL(**params) # We keep `sql` to maintain backwards compatibility - self.logger.record(**params) + self.logger.object.sql.append(_sql) def callproc(self, procname, params=()): return self._record(self.cursor.callproc, procname, params) diff --git a/graphene/contrib/django/debug/sql/types.py b/graphene/contrib/django/debug/sql/types.py index 995aeaa2..43d2c73a 100644 --- a/graphene/contrib/django/debug/sql/types.py +++ b/graphene/contrib/django/debug/sql/types.py @@ -1,7 +1,7 @@ from .....core import Boolean, Float, ObjectType, String -class DjangoDebugSQL(ObjectType): +class DjangoDebugBaseSQL(ObjectType): vendor = String() alias = String() sql = String() @@ -13,6 +13,12 @@ class DjangoDebugSQL(ObjectType): is_slow = Boolean() is_select = Boolean() + +class DjangoDebugSQL(DjangoDebugBaseSQL): + pass + + +class DjangoDebugPostgreSQL(DjangoDebugBaseSQL): trans_id = String() trans_status = String() iso_level = String() diff --git a/graphene/contrib/django/debug/tests/test_query.py b/graphene/contrib/django/debug/tests/test_query.py index c512d6ad..b6fdb8ec 100644 --- a/graphene/contrib/django/debug/tests/test_query.py +++ b/graphene/contrib/django/debug/tests/test_query.py @@ -5,7 +5,11 @@ from graphene.contrib.django import DjangoConnectionField, DjangoNode from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED from ...tests.models import Reporter -from ..plugin import DjangoDebugPlugin +from ..middleware import DjangoDebugMiddleware +from ..types import DjangoDebug + +class context(object): + pass # from examples.starwars_django.models import Character @@ -25,6 +29,7 @@ def test_should_query_field(): class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) + debug = graphene.Field(DjangoDebug, name='__debug') def resolve_reporter(self, *args, **kwargs): return Reporter.objects.first() @@ -51,8 +56,8 @@ def test_should_query_field(): }] } } - schema = graphene.Schema(query=Query, plugins=[DjangoDebugPlugin()]) - result = schema.execute(query) + schema = graphene.Schema(query=Query, plugins=[DjangoDebugMiddleware()]) + result = schema.execute(query, context_value=context()) assert not result.errors assert result.data == expected @@ -70,6 +75,7 @@ def test_should_query_list(): class Query(graphene.ObjectType): all_reporters = ReporterType.List() + debug = graphene.Field(DjangoDebug, name='__debug') def resolve_all_reporters(self, *args, **kwargs): return Reporter.objects.all() @@ -98,8 +104,8 @@ def test_should_query_list(): }] } } - schema = graphene.Schema(query=Query, plugins=[DjangoDebugPlugin()]) - result = schema.execute(query) + schema = graphene.Schema(query=Query, plugins=[DjangoDebugMiddleware()]) + result = schema.execute(query, context_value=context()) assert not result.errors assert result.data == expected @@ -117,6 +123,7 @@ def test_should_query_connection(): class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) + debug = graphene.Field(DjangoDebug, name='__debug') def resolve_all_reporters(self, *args, **kwargs): return Reporter.objects.all() @@ -146,8 +153,8 @@ def test_should_query_connection(): }] }, } - schema = graphene.Schema(query=Query, plugins=[DjangoDebugPlugin()]) - result = schema.execute(query) + schema = graphene.Schema(query=Query, plugins=[DjangoDebugMiddleware()]) + result = schema.execute(query, context_value=context()) assert not result.errors assert result.data['allReporters'] == expected['allReporters'] assert 'COUNT' in result.data['__debug']['sql'][0]['rawSql'] @@ -172,6 +179,7 @@ def test_should_query_connectionfilter(): class Query(graphene.ObjectType): all_reporters = DjangoFilterConnectionField(ReporterType) + debug = graphene.Field(DjangoDebug, name='__debug') def resolve_all_reporters(self, *args, **kwargs): return Reporter.objects.all() @@ -201,8 +209,8 @@ def test_should_query_connectionfilter(): }] }, } - schema = graphene.Schema(query=Query, plugins=[DjangoDebugPlugin()]) - result = schema.execute(query) + schema = graphene.Schema(query=Query, plugins=[DjangoDebugMiddleware()]) + result = schema.execute(query, context_value=context()) assert not result.errors assert result.data['allReporters'] == expected['allReporters'] assert 'COUNT' in result.data['__debug']['sql'][0]['rawSql'] diff --git a/graphene/contrib/django/debug/types.py b/graphene/contrib/django/debug/types.py index bceb54b0..c6a498f4 100644 --- a/graphene/contrib/django/debug/types.py +++ b/graphene/contrib/django/debug/types.py @@ -1,7 +1,7 @@ from ....core.classtypes.objecttype import ObjectType from ....core.types import Field -from .sql.types import DjangoDebugSQL +from .sql.types import DjangoDebugBaseSQL class DjangoDebug(ObjectType): - sql = Field(DjangoDebugSQL.List()) + sql = Field(DjangoDebugBaseSQL.List()) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 46e8768f..86b623ba 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -35,6 +35,7 @@ class Schema(object): plugins = plugins or [] if auto_camelcase: plugins.append(CamelCase()) + self.auto_camelcase = auto_camelcase self.plugins = PluginManager(self, plugins) self.options = options signals.init_schema.send(self) @@ -42,11 +43,6 @@ class Schema(object): def __repr__(self): return '' % (str(self.name), hash(self)) - 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: return @@ -122,7 +118,7 @@ class Schema(object): def execute(self, request_string='', root_value=None, variable_values=None, context_value=None, operation_name=None, executor=None): - kwargs = dict( + return graphql( schema=self.schema, request_string=request_string, root_value=root_value, @@ -131,8 +127,6 @@ class Schema(object): operation_name=operation_name, executor=executor or self._executor ) - with self.plugins.context_execution(**kwargs) as execute_kwargs: - return graphql(**execute_kwargs) def introspect(self): return graphql(self.schema, introspection_query).data diff --git a/graphene/core/types/base.py b/graphene/core/types/base.py index ec8c7b3b..b99d2213 100644 --- a/graphene/core/types/base.py +++ b/graphene/core/types/base.py @@ -3,6 +3,8 @@ from functools import partial, total_ordering import six +from ...utils import to_camel_case + class InstanceType(object): @@ -142,7 +144,9 @@ class GroupNamedType(InstanceType): self.types = types def get_named_type(self, schema, type): - name = type.name or schema.get_default_namedtype_name(type.default_name) + name = type.name + if not name and schema.auto_camelcase: + name = to_camel_case(type.default_name) return name, schema.T(type) def iter_types(self, schema): From 577b76e43668e7b1ac5cbd85c2e01cef8e220c6b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 20 May 2016 23:43:55 -0700 Subject: [PATCH 4/6] Removed plugins. Added middlewares --- .../contrib/django/debug/tests/test_query.py | 9 ++-- graphene/core/schema.py | 16 +++--- graphene/core/tests/test_schema.py | 6 +++ graphene/middlewares/__init__.py | 6 +++ graphene/middlewares/base.py | 23 ++++++++ graphene/middlewares/camel_case.py | 8 +++ graphene/plugins/__init__.py | 6 --- graphene/plugins/base.py | 53 ------------------- graphene/plugins/camel_case.py | 11 ---- 9 files changed, 56 insertions(+), 82 deletions(-) create mode 100644 graphene/middlewares/__init__.py create mode 100644 graphene/middlewares/base.py create mode 100644 graphene/middlewares/camel_case.py delete mode 100644 graphene/plugins/__init__.py delete mode 100644 graphene/plugins/base.py delete mode 100644 graphene/plugins/camel_case.py diff --git a/graphene/contrib/django/debug/tests/test_query.py b/graphene/contrib/django/debug/tests/test_query.py index b6fdb8ec..50976ad5 100644 --- a/graphene/contrib/django/debug/tests/test_query.py +++ b/graphene/contrib/django/debug/tests/test_query.py @@ -8,6 +8,7 @@ from ...tests.models import Reporter from ..middleware import DjangoDebugMiddleware from ..types import DjangoDebug + class context(object): pass @@ -56,7 +57,7 @@ def test_should_query_field(): }] } } - schema = graphene.Schema(query=Query, plugins=[DjangoDebugMiddleware()]) + schema = graphene.Schema(query=Query, middlewares=[DjangoDebugMiddleware()]) result = schema.execute(query, context_value=context()) assert not result.errors assert result.data == expected @@ -104,7 +105,7 @@ def test_should_query_list(): }] } } - schema = graphene.Schema(query=Query, plugins=[DjangoDebugMiddleware()]) + schema = graphene.Schema(query=Query, middlewares=[DjangoDebugMiddleware()]) result = schema.execute(query, context_value=context()) assert not result.errors assert result.data == expected @@ -153,7 +154,7 @@ def test_should_query_connection(): }] }, } - schema = graphene.Schema(query=Query, plugins=[DjangoDebugMiddleware()]) + schema = graphene.Schema(query=Query, middlewares=[DjangoDebugMiddleware()]) result = schema.execute(query, context_value=context()) assert not result.errors assert result.data['allReporters'] == expected['allReporters'] @@ -209,7 +210,7 @@ def test_should_query_connectionfilter(): }] }, } - schema = graphene.Schema(query=Query, plugins=[DjangoDebugMiddleware()]) + schema = graphene.Schema(query=Query, middlewares=[DjangoDebugMiddleware()]) result = schema.execute(query, context_value=context()) assert not result.errors assert result.data['allReporters'] == expected['allReporters'] diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 86b623ba..71fdd742 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -7,8 +7,7 @@ from graphql.utils.schema_printer import print_schema from graphene import signals -from ..plugins import CamelCase, PluginManager -from ..utils import promise_middleware +from ..middlewares import MiddlewareManager, CamelCaseArgsMiddleware from .classtypes.base import ClassType from .types.base import InstanceType @@ -24,7 +23,7 @@ class Schema(object): _executor = None def __init__(self, query=None, mutation=None, subscription=None, - name='Schema', executor=None, plugins=None, auto_camelcase=True, **options): + name='Schema', executor=None, middlewares=None, auto_camelcase=True, **options): self._types_names = {} self._types = {} self.mutation = mutation @@ -32,11 +31,13 @@ class Schema(object): self.subscription = subscription self.name = name self.executor = executor - plugins = plugins or [] + if 'plugins' in options: + raise Exception('Plugins are deprecated, please use middlewares.') + middlewares = middlewares or [] if auto_camelcase: - plugins.append(CamelCase()) + middlewares.append(CamelCaseArgsMiddleware()) self.auto_camelcase = auto_camelcase - self.plugins = PluginManager(self, plugins) + self.middleware_manager = MiddlewareManager(self, middlewares) self.options = options signals.init_schema.send(self) @@ -109,8 +110,7 @@ class Schema(object): return self._types_names[type_name] def resolver_with_middleware(self, resolver): - plugins_resolve = self.plugins.get_plugin_functions('resolve') - return promise_middleware(resolver, plugins_resolve) + return self.middleware_manager.wrap(resolver) @property def types(self): diff --git a/graphene/core/tests/test_schema.py b/graphene/core/tests/test_schema.py index a7e4c2e1..8a182515 100644 --- a/graphene/core/tests/test_schema.py +++ b/graphene/core/tests/test_schema.py @@ -154,6 +154,12 @@ def test_lazytype(): assert schema.T(t) == schema.T(MyType) +def test_deprecated_plugins_throws_exception(): + with raises(Exception) as excinfo: + Schema(plugins=[]) + assert 'Plugins are deprecated, please use middlewares' in str(excinfo.value) + + def test_schema_str(): expected = """ schema { diff --git a/graphene/middlewares/__init__.py b/graphene/middlewares/__init__.py new file mode 100644 index 00000000..4cee0bcc --- /dev/null +++ b/graphene/middlewares/__init__.py @@ -0,0 +1,6 @@ +from .base import MiddlewareManager +from .camel_case import CamelCaseArgsMiddleware + +__all__ = [ + 'MiddlewareManager', 'CamelCaseArgsMiddleware' +] diff --git a/graphene/middlewares/base.py b/graphene/middlewares/base.py new file mode 100644 index 00000000..33a26ac5 --- /dev/null +++ b/graphene/middlewares/base.py @@ -0,0 +1,23 @@ +from ..utils import promise_middleware + +MIDDLEWARE_RESOLVER_FUNCTION = 'resolve' + + +class MiddlewareManager(object): + + def __init__(self, schema, middlewares=None): + self.schema = schema + self.middlewares = middlewares or [] + + def add_middleware(self, middleware): + self.middlewares.append(middleware) + + def get_middleware_resolvers(self): + for middleware in self.middlewares: + if not hasattr(middleware, MIDDLEWARE_RESOLVER_FUNCTION): + continue + yield getattr(middleware, MIDDLEWARE_RESOLVER_FUNCTION) + + def wrap(self, resolver): + middleware_resolvers = self.get_middleware_resolvers() + return promise_middleware(resolver, middleware_resolvers) diff --git a/graphene/middlewares/camel_case.py b/graphene/middlewares/camel_case.py new file mode 100644 index 00000000..b5266f02 --- /dev/null +++ b/graphene/middlewares/camel_case.py @@ -0,0 +1,8 @@ +from ..utils import ProxySnakeDict + + +class CamelCaseArgsMiddleware(object): + + def resolve(self, next, root, args, context, info): + args = ProxySnakeDict(args) + return next(root, args, context, info) diff --git a/graphene/plugins/__init__.py b/graphene/plugins/__init__.py deleted file mode 100644 index 160bffba..00000000 --- a/graphene/plugins/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .base import Plugin, PluginManager -from .camel_case import CamelCase - -__all__ = [ - 'Plugin', 'PluginManager', 'CamelCase' -] diff --git a/graphene/plugins/base.py b/graphene/plugins/base.py deleted file mode 100644 index 2347beba..00000000 --- a/graphene/plugins/base.py +++ /dev/null @@ -1,53 +0,0 @@ -from contextlib import contextmanager -from functools import partial, reduce - - -class Plugin(object): - - def contribute_to_schema(self, schema): - self.schema = schema - - -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 - - @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) diff --git a/graphene/plugins/camel_case.py b/graphene/plugins/camel_case.py deleted file mode 100644 index 1f1c6690..00000000 --- a/graphene/plugins/camel_case.py +++ /dev/null @@ -1,11 +0,0 @@ -from ..utils import to_camel_case, ProxySnakeDict - - -class CamelCase(object): - - def get_default_namedtype_name(self, value): - return to_camel_case(value) - - def resolve(self, next, root, args, context, info): - args = ProxySnakeDict(args) - return next(root, args, context, info) From b9723356fee7de95206111e48b785b18e5d61cc4 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 20 May 2016 23:48:54 -0700 Subject: [PATCH 5/6] Fixed PEP8-Python3 errors --- graphene/contrib/django/debug/middleware.py | 8 ++++---- graphene/contrib/django/debug/sql/tracking.py | 1 + graphene/core/types/field.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/graphene/contrib/django/debug/middleware.py b/graphene/contrib/django/debug/middleware.py index adf551ad..01b09e7b 100644 --- a/graphene/contrib/django/debug/middleware.py +++ b/graphene/contrib/django/debug/middleware.py @@ -1,8 +1,6 @@ from promise import Promise from django.db import connections -from ....core.schema import GraphQLSchema -from ....core.types import Field from .sql.tracking import unwrap_cursor, wrap_cursor from .types import DjangoDebug @@ -47,8 +45,10 @@ class DjangoDebugMiddleware(object): raise Exception('DjangoDebug cannot be executed in None contexts') try: context.django_debug = DjangoDebugContext() - except Exception, e: - raise Exception('DjangoDebug need the context to be writable, context received: {}.'.format(context.__class__.__name__)) + except Exception: + raise Exception('DjangoDebug need the context to be writable, context received: {}.'.format( + context.__class__.__name__ + )) if info.schema.graphene_schema.T(DjangoDebug) == info.return_type: return context.django_debug.get_debug_promise() promise = next(root, args, context, info) diff --git a/graphene/contrib/django/debug/sql/tracking.py b/graphene/contrib/django/debug/sql/tracking.py index ab31dbe5..c636646e 100644 --- a/graphene/contrib/django/debug/sql/tracking.py +++ b/graphene/contrib/django/debug/sql/tracking.py @@ -10,6 +10,7 @@ from django.utils.encoding import force_text from .types import DjangoDebugSQL, DjangoDebugPostgreSQL + class SQLQueryTriggered(Exception): """Thrown when template panel triggers a query""" diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index e7a36f84..7203b3c5 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -1,5 +1,5 @@ from collections import OrderedDict -from functools import wraps, partial +from functools import wraps import six from graphql.type import GraphQLField, GraphQLInputObjectField From 10e5424e313d3486fcbc5bf4a090ee3aceea5a0d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 21 May 2016 00:07:41 -0700 Subject: [PATCH 6/6] Updated documentation referencing middleware --- docs/config.toml | 1 + docs/pages/docs/django/debug.md | 13 +++++++--- docs/pages/docs/middleware.md | 43 +++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 docs/pages/docs/middleware.md diff --git a/docs/config.toml b/docs/config.toml index 74b843ea..59c4ea5f 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -17,6 +17,7 @@ ga = "UA-12613282-7" "/docs/basic-types/", "/docs/enums/", "/docs/relay/", + "/docs/middleware/", ] [docs.django] diff --git a/docs/pages/docs/django/debug.md b/docs/pages/docs/django/debug.md index c4b124e8..f5ac2adc 100644 --- a/docs/pages/docs/django/debug.md +++ b/docs/pages/docs/django/debug.md @@ -13,14 +13,19 @@ For that, you will need to add the plugin in your graphene schema. ## Installation -For use the Django Debug plugin in Graphene, just import `DjangoDebugPlugin` and add it to the `plugins` argument when you initiate the `Schema`. +For use the Django Debug plugin in Graphene: +* Import `DjangoDebugMiddleware` and add it to the `middleware` argument when you initiate the `Schema`. +* Add the `debug` field into the schema root `Query` with the value `graphene.Field(DjangoDebug, name='__debug')`. ```python -from graphene.contrib.django.debug import DjangoDebugPlugin +from graphene.contrib.django.debug import DjangoDebugMiddleware, DjangoDebug -# ... -schema = graphene.Schema(query=Query, plugins=[DjangoDebugPlugin()]) +class Query(graphene.ObjectType): + # ... + debug = graphene.Field(DjangoDebug, name='__debug') + +schema = graphene.Schema(query=Query, middlewares=[DjangoDebugMiddleware()]) ``` This plugin, will add another field in the `Query` named `__debug`. diff --git a/docs/pages/docs/middleware.md b/docs/pages/docs/middleware.md new file mode 100644 index 00000000..252fe384 --- /dev/null +++ b/docs/pages/docs/middleware.md @@ -0,0 +1,43 @@ +--- +title: Middleware +description: Walkthrough Middleware +--- + +# Middleware + +You can use _middleware_ to affect the evaluation of fields in your schema. + +A middleware is any object that responds to `resolve(*args, next_middleware)`. Inside that method, it should either: + +* Send `resolve` to the next middleware to continue the evaluation; or +* Return a value to end the evaluation early. + +Middlewares' `resolve` is invoked with several arguments: + +* `next` represents the execution chain. Call `next` to continue evalution. +* `root` is the root value object passed throughout the query +* `args` is the hash of arguments passed to the field +* `context` is the context object passed throughout the query +* `info` is the resolver info + +Add a middleware to a schema by adding to the `middlewares` list. + + +### Example: Authorization + +This middleware only continues evaluation if the `field_name` is not `'user'`: + +```python +class AuthorizationMiddleware(object): + + def resolve(self, next, root, args, context, info): + if info.field_name == 'user': + return None + return next(root, args, context, info) +``` + +Then, add the middleware to your schema: + +```python +schema = Schema(middlewares=[AuthorizationMiddleware]) +```