diff --git a/graphql/execution/executor.py b/graphql/execution/executor.py index 7b790739..31428ee6 100644 --- a/graphql/execution/executor.py +++ b/graphql/execution/executor.py @@ -19,7 +19,6 @@ get_operation_root_type, SubscriberExecutionContext) from .executors.sync import SyncExecutor from .middleware import MiddlewareManager -from .tracing import TracingMiddleware logger = logging.getLogger(__name__) @@ -31,8 +30,7 @@ def subscribe(*args, **kwargs): def execute(schema, document_ast, root_value=None, context_value=None, variable_values=None, operation_name=None, executor=None, - return_promise=False, middleware=None, allow_subscriptions=False, - tracing=False): + return_promise=False, middleware=None, allow_subscriptions=False): assert schema, 'Must provide schema' assert isinstance(schema, GraphQLSchema), ( 'Schema must be an instance of GraphQLSchema. Also ensure that there are ' + @@ -48,17 +46,6 @@ def execute(schema, document_ast, root_value=None, context_value=None, ' of MiddlewareManager. Received "{}".'.format(middleware) ) - tracing_middleware = None - if tracing: - tracing_middleware = TracingMiddleware() - tracing_middleware.start() - - if middleware: - middleware.middlewares.insert(0, tracing_middleware) - else: - middleware = MiddlewareManager(tracing_middleware) - - if executor is None: executor = SyncExecutor() @@ -85,15 +72,10 @@ def on_resolve(data): if isinstance(data, Observable): return data - extensions = dict() - if tracing_middleware: - tracing_middleware.end() - extensions['tracing'] = tracing_middleware.tracing_dict - if not context.errors: - return ExecutionResult(data=data, extensions=extensions) + return ExecutionResult(data=data) - return ExecutionResult(data=data, extensions=extensions, errors=context.errors) + return ExecutionResult(data=data, errors=context.errors) promise = Promise.resolve(None).then(executor).catch(on_rejected).then(on_resolve) diff --git a/graphql/execution/tests/test_executor.py b/graphql/execution/tests/test_executor.py index e36c596f..cc107362 100644 --- a/graphql/execution/tests/test_executor.py +++ b/graphql/execution/tests/test_executor.py @@ -7,7 +7,7 @@ from graphql.language.parser import parse from graphql.type import (GraphQLArgument, GraphQLBoolean, GraphQLField, GraphQLInt, GraphQLList, GraphQLObjectType, - GraphQLSchema, GraphQLString) + GraphQLSchema, GraphQLString, GraphQLNonNull, GraphQLID) from promise import Promise @@ -668,3 +668,148 @@ def resolve(self, next, *args, **kwargs): middleware=middlewares_without_promise) assert result1.data == result2.data and result1.data == { 'ok': 'ok', 'not_ok': 'not_ok'} + + +def test_executor_properly_propogates_path_data(mocker): + time_mock = mocker.patch('time.time') + time_mock.side_effect = range(0, 10000) + + BlogImage = GraphQLObjectType('BlogImage', { + 'url': GraphQLField(GraphQLString), + 'width': GraphQLField(GraphQLInt), + 'height': GraphQLField(GraphQLInt), + }) + + BlogAuthor = GraphQLObjectType('Author', lambda: { + 'id': GraphQLField(GraphQLString), + 'name': GraphQLField(GraphQLString), + 'pic': GraphQLField(BlogImage, + args={ + 'width': GraphQLArgument(GraphQLInt), + 'height': GraphQLArgument(GraphQLInt), + }, + resolver=lambda obj, info, **args: + obj.pic(args['width'], args['height']) + ), + 'recentArticle': GraphQLField(BlogArticle), + }) + + BlogArticle = GraphQLObjectType('Article', { + 'id': GraphQLField(GraphQLNonNull(GraphQLString)), + 'isPublished': GraphQLField(GraphQLBoolean), + 'author': GraphQLField(BlogAuthor), + 'title': GraphQLField(GraphQLString), + 'body': GraphQLField(GraphQLString), + 'keywords': GraphQLField(GraphQLList(GraphQLString)), + }) + + BlogQuery = GraphQLObjectType('Query', { + 'article': GraphQLField( + BlogArticle, + args={'id': GraphQLArgument(GraphQLID)}, + resolver=lambda obj, info, **args: Article(args['id'])), + 'feed': GraphQLField( + GraphQLList(BlogArticle), + resolver=lambda *_: map(Article, range(1, 2 + 1))), + }) + + BlogSchema = GraphQLSchema(BlogQuery) + + class Article(object): + + def __init__(self, id): + self.id = id + self.isPublished = True + self.author = Author() + self.title = 'My Article {}'.format(id) + self.body = 'This is a post' + self.hidden = 'This data is not exposed in the schema' + self.keywords = ['foo', 'bar', 1, True, None] + + class Author(object): + id = 123 + name = 'John Smith' + + def pic(self, width, height): + return Pic(123, width, height) + + @property + def recentArticle(self): return Article(1) + + class Pic(object): + def __init__(self, uid, width, height): + self.url = 'cdn://{}'.format(uid) + self.width = str(width) + self.height = str(height) + + class PathCollectorMiddleware(object): + def __init__(self): + self.paths = [] + + def resolve(self, _next, root, info, *args, **kwargs): + self.paths.append(info.path) + return _next(root, info, *args, **kwargs) + + request = ''' + { + feed { + id + ...articleFields + author { + id + name + } + }, + } + fragment articleFields on Article { + title, + body, + hidden, + } + ''' + + paths_middleware = PathCollectorMiddleware() + + result = execute(BlogSchema, parse(request), middleware=(paths_middleware, )) + assert not result.errors + assert result.data == \ + { + "feed": [ + { + "id": "1", + "title": "My Article 1", + "body": "This is a post", + "author": { + "id": "123", + "name": "John Smith" + } + }, + { + "id": "2", + "title": "My Article 2", + "body": "This is a post", + "author": { + "id": "123", + "name": "John Smith" + } + }, + ], + } + + traversed_paths = paths_middleware.paths + assert traversed_paths == [ + ['feed'], + ['feed', 0, 'id'], + ['feed', 0, 'title'], + ['feed', 0, 'body'], + ['feed', 0, 'author'], + ['feed', 1, 'id'], + ['feed', 1, 'title'], + ['feed', 1, 'body'], + ['feed', 1, 'author'], + ['feed', 0, 'author', 'id'], + ['feed', 0, 'author', 'name'], + ['feed', 1, 'author', 'id'], + ['feed', 1, 'author', 'name'] + ] + diff --git a/graphql/execution/tests/test_tracing.py b/graphql/execution/tests/test_tracing.py deleted file mode 100644 index 95640949..00000000 --- a/graphql/execution/tests/test_tracing.py +++ /dev/null @@ -1,153 +0,0 @@ -import time - -from graphql.execution import execute -from graphql.execution.tracing import TracingMiddleware -from graphql.language.parser import parse -from graphql.type import (GraphQLArgument, GraphQLBoolean, GraphQLField, - GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, - GraphQLObjectType, GraphQLSchema, GraphQLString) - - -def test_tracing(mocker): - time_mock = mocker.patch('time.time') - time_mock.side_effect = range(0, 10000) - - BlogImage = GraphQLObjectType('BlogImage', { - 'url': GraphQLField(GraphQLString), - 'width': GraphQLField(GraphQLInt), - 'height': GraphQLField(GraphQLInt), - }) - - BlogAuthor = GraphQLObjectType('Author', lambda: { - 'id': GraphQLField(GraphQLString), - 'name': GraphQLField(GraphQLString), - 'pic': GraphQLField(BlogImage, - args={ - 'width': GraphQLArgument(GraphQLInt), - 'height': GraphQLArgument(GraphQLInt), - }, - resolver=lambda obj, info, **args: - obj.pic(args['width'], args['height']) - ), - 'recentArticle': GraphQLField(BlogArticle), - }) - - BlogArticle = GraphQLObjectType('Article', { - 'id': GraphQLField(GraphQLNonNull(GraphQLString)), - 'isPublished': GraphQLField(GraphQLBoolean), - 'author': GraphQLField(BlogAuthor), - 'title': GraphQLField(GraphQLString), - 'body': GraphQLField(GraphQLString), - 'keywords': GraphQLField(GraphQLList(GraphQLString)), - }) - - BlogQuery = GraphQLObjectType('Query', { - 'article': GraphQLField( - BlogArticle, - args={'id': GraphQLArgument(GraphQLID)}, - resolver=lambda obj, info, **args: Article(args['id'])), - 'feed': GraphQLField( - GraphQLList(BlogArticle), - resolver=lambda *_: map(Article, range(1, 2 + 1))), - }) - - BlogSchema = GraphQLSchema(BlogQuery) - - class Article(object): - - def __init__(self, id): - self.id = id - self.isPublished = True - self.author = Author() - self.title = 'My Article {}'.format(id) - self.body = 'This is a post' - self.hidden = 'This data is not exposed in the schema' - self.keywords = ['foo', 'bar', 1, True, None] - - class Author(object): - id = 123 - name = 'John Smith' - - def pic(self, width, height): - return Pic(123, width, height) - - @property - def recentArticle(self): return Article(1) - - class Pic(object): - - def __init__(self, uid, width, height): - self.url = 'cdn://{}'.format(uid) - self.width = str(width) - self.height = str(height) - - request = ''' - { - feed { - id - ...articleFields - author { - id - name - } - }, - } - fragment articleFields on Article { - title, - body, - hidden, - } - ''' - - # Note: this is intentionally not validating to ensure appropriate - # behavior occurs when executing an invalid query. - result = execute(BlogSchema, parse(request), tracing=True) - assert not result.errors - assert result.data == \ - { - "feed": [ - { - "id": "1", - "title": "My Article 1", - "body": "This is a post", - "author": { - "id": "123", - "name": "John Smith" - } - }, - { - "id": "2", - "title": "My Article 2", - "body": "This is a post", - "author": { - "id": "123", - "name": "John Smith" - } - }, - ], - } - - assert result.extensions['tracing'] == { - 'version': 1, - 'startTime': time.strftime(TracingMiddleware.DATETIME_FORMAT, time.gmtime(0)), - 'endTime': time.strftime(TracingMiddleware.DATETIME_FORMAT, time.gmtime(40)), - 'duration': 40000, - 'execution': { - 'resolvers': [ - {'path': ['feed'], 'parentType': 'Query', 'fieldName': 'feed', 'returnType': '[Article]', 'startOffset': 3000, 'duration': 1000}, - {'path': ['feed', 0, 'id'], 'parentType': 'Article', 'fieldName': 'id', 'returnType': 'String!', 'startOffset': 6000, 'duration': 1000}, - {'path': ['feed', 0, 'title'], 'parentType': 'Article', 'fieldName': 'title', 'returnType': 'String', 'startOffset': 9000, 'duration': 1000}, - {'path': ['feed', 0, 'body'], 'parentType': 'Article', 'fieldName': 'body', 'returnType': 'String', 'startOffset': 12000, 'duration': 1000}, - {'path': ['feed', 0, 'author'], 'parentType': 'Article', 'fieldName': 'author', 'returnType': 'Author', 'startOffset': 15000, 'duration': 1000}, - {'path': ['feed', 1, 'id'], 'parentType': 'Article', 'fieldName': 'id', 'returnType': 'String!', 'startOffset': 18000, 'duration': 1000}, - {'path': ['feed', 1, 'title'], 'parentType': 'Article', 'fieldName': 'title', 'returnType': 'String', 'startOffset': 21000, 'duration': 1000}, - {'path': ['feed', 1, 'body'], 'parentType': 'Article', 'fieldName': 'body', 'returnType': 'String', 'startOffset': 24000, 'duration': 1000}, - {'path': ['feed', 1, 'author'], 'parentType': 'Article', 'fieldName': 'author', 'returnType': 'Author', 'startOffset': 27000, 'duration': 1000}, - {'path': ['feed', 0, 'author', 'id'], 'parentType': 'Author', 'fieldName': 'id', 'returnType': 'String', 'startOffset': 30000, 'duration': 1000}, - {'path': ['feed', 0, 'author', 'name'], 'parentType': 'Author', 'fieldName': 'name', 'returnType': 'String', 'startOffset': 33000, 'duration': 1000}, - {'path': ['feed', 1, 'author', 'id'], 'parentType': 'Author', 'fieldName': 'id', 'returnType': 'String', 'startOffset': 36000, 'duration': 1000}, - {'path': ['feed', 1, 'author', 'name'], 'parentType': 'Author', 'fieldName': 'name', 'returnType': 'String', 'startOffset': 39000, 'duration': 1000} - ] - } - } - diff --git a/graphql/execution/tracing.py b/graphql/execution/tracing.py deleted file mode 100644 index c2bda4b0..00000000 --- a/graphql/execution/tracing.py +++ /dev/null @@ -1,61 +0,0 @@ -import time - - -class TracingMiddleware(object): - DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' - - def __init__(self): - self.resolver_stats = list() - self.start_time = None - self.end_time = None - - def start(self): - self.start_time = time.time() * 1000 - - def end(self): - self.end_time = time.time() * 1000 - - @property - def start_time_str(self): - return time.strftime(self.DATETIME_FORMAT, time.gmtime(self.start_time/1000)) - - @property - def end_time_str(self): - return time.strftime(self.DATETIME_FORMAT, time.gmtime(self.end_time/1000)) - - @property - def duration(self): - if not self.end_time: - raise ValueError("Tracing has not ended yet!") - - return (self.end_time - self.start_time) - - @property - def tracing_dict(self): - return dict( - version=1, - startTime=self.start_time_str, - endTime=self.end_time_str, - duration=self.duration, - execution=dict( - resolvers=self.resolver_stats - ) - ) - - def resolve(self, _next, root, info, *args, **kwargs): - start = time.time() - try: - return _next(root, info, *args, **kwargs) - finally: - end = time.time() - elapsed_ms = (end - start) * 1000 - - stat = { - "path": info.path, - "parentType": str(info.parent_type), - "fieldName": info.field_name, - "returnType": str(info.return_type), - "startOffset": (time.time() * 1000) - self.start_time, - "duration": elapsed_ms, - } - self.resolver_stats.append(stat)