From 14439155eec40c72afcaa42651c2d3345246edb6 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 3 Dec 2015 22:15:09 -0800 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 a153a01f6ba7e5805bed10ec6867961d673835bc Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 6 Dec 2015 03:59:44 -0800 Subject: [PATCH 4/6] 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 dd5b26e6ed8c9188299ff965d18d8c902bc768d2 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 6 Dec 2015 16:50:34 -0800 Subject: [PATCH 5/6] 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 c8f4c138224b6e31f01be65a8c1131e2ecf2ed5d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 9 Dec 2015 19:44:35 -0800 Subject: [PATCH 6/6] 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)