diff --git a/graphql/__init__.py b/graphql/__init__.py index cc73931e..08f25683 100644 --- a/graphql/__init__.py +++ b/graphql/__init__.py @@ -194,7 +194,7 @@ ) -VERSION = (2, 0, 0, 'final', 0) +VERSION = (2, 0, 1, 'final', 0) __version__ = get_version(VERSION) diff --git a/graphql/execution/base.py b/graphql/execution/base.py index 3c0d2e95..21d7378c 100644 --- a/graphql/execution/base.py +++ b/graphql/execution/base.py @@ -129,11 +129,12 @@ class ExecutionResult(object): query, `errors` is null if no errors occurred, and is a non-empty array if an error occurred.""" - __slots__ = 'data', 'errors', 'invalid' + __slots__ = 'data', 'errors', 'invalid', 'extensions' - def __init__(self, data=None, errors=None, invalid=False): + def __init__(self, data=None, errors=None, invalid=False, extensions=None): self.data = data self.errors = errors + self.extensions = extensions or dict() if invalid: assert data is None @@ -297,10 +298,10 @@ def get_field_entry_key(node): class ResolveInfo(object): __slots__ = ('field_name', 'field_asts', 'return_type', 'parent_type', - 'schema', 'fragments', 'root_value', 'operation', 'variable_values', 'context') + 'schema', 'fragments', 'root_value', 'operation', 'variable_values', 'context', 'path') def __init__(self, field_name, field_asts, return_type, parent_type, - schema, fragments, root_value, operation, variable_values, context): + schema, fragments, root_value, operation, variable_values, context, path): self.field_name = field_name self.field_asts = field_asts self.return_type = return_type @@ -311,6 +312,7 @@ def __init__(self, field_name, field_asts, return_type, parent_type, self.operation = operation self.variable_values = variable_values self.context = context + self.path = path def default_resolve_fn(source, info, **args): diff --git a/graphql/execution/executor.py b/graphql/execution/executor.py index 7ae0577a..31428ee6 100644 --- a/graphql/execution/executor.py +++ b/graphql/execution/executor.py @@ -36,6 +36,7 @@ def execute(schema, document_ast, root_value=None, context_value=None, 'Schema must be an instance of GraphQLSchema. Also ensure that there are ' + 'not multiple versions of GraphQL installed in your node_modules directory.' ) + if middleware: if not isinstance(middleware, MiddlewareManager): middleware = MiddlewareManager(*middleware) @@ -73,10 +74,10 @@ def on_resolve(data): if not context.errors: return ExecutionResult(data=data) + return ExecutionResult(data=data, errors=context.errors) - promise = Promise.resolve(None).then( - executor).catch(on_rejected).then(on_resolve) + promise = Promise.resolve(None).then(executor).catch(on_rejected).then(on_resolve) if not return_promise: context.executor.wait_until_finished() @@ -107,7 +108,7 @@ def execute_operation(exe_context, operation, root_value): ) return subscribe_fields(exe_context, type, root_value, fields) - return execute_fields(exe_context, type, root_value, fields) + return execute_fields(exe_context, type, root_value, fields, None) def execute_fields_serially(exe_context, parent_type, source_value, fields): @@ -117,7 +118,8 @@ def execute_field_callback(results, response_name): exe_context, parent_type, source_value, - field_asts + field_asts, + None ) if result is Undefined: return results @@ -138,14 +140,13 @@ def execute_field(prev_promise, response_name): return functools.reduce(execute_field, fields.keys(), Promise.resolve(collections.OrderedDict())) -def execute_fields(exe_context, parent_type, source_value, fields): +def execute_fields(exe_context, parent_type, source_value, fields, info): contains_promise = False final_results = OrderedDict() for response_name, field_asts in fields.items(): - result = resolve_field(exe_context, parent_type, - source_value, field_asts) + result = resolve_field(exe_context, parent_type, source_value, field_asts, info) if result is Undefined: continue @@ -179,8 +180,7 @@ def map_result(data): for response_name, field_asts in fields.items(): - result = subscribe_field(exe_context, parent_type, - source_value, field_asts) + result = subscribe_field(exe_context, parent_type, source_value, field_asts) if result is Undefined: continue @@ -197,7 +197,7 @@ def catch_error(error): return Observable.merge(observables) -def resolve_field(exe_context, parent_type, source, field_asts): +def resolve_field(exe_context, parent_type, source, field_asts, parent_info): field_ast = field_asts[0] field_name = field_ast.name.value @@ -232,12 +232,12 @@ def resolve_field(exe_context, parent_type, source, field_asts): root_value=exe_context.root_value, operation=exe_context.operation, variable_values=exe_context.variable_values, - context=context + context=context, + path=parent_info.path+[field_name] if parent_info else [field_name] ) executor = exe_context.executor - result = resolve_or_error(resolve_fn_middleware, - source, info, args, executor) + result = resolve_or_error(resolve_fn_middleware, source, info, args, executor) return complete_value_catching_error( exe_context, @@ -283,7 +283,8 @@ def subscribe_field(exe_context, parent_type, source, field_asts): root_value=exe_context.root_value, operation=exe_context.operation, variable_values=exe_context.variable_values, - context=context + context=context, + path=[field_name] ) executor = exe_context.executor @@ -326,8 +327,7 @@ def complete_value_catching_error(exe_context, return_type, field_asts, info, re # Otherwise, error protection is applied, logging the error and # resolving a null value for this field if one is encountered. try: - completed = complete_value( - exe_context, return_type, field_asts, info, result) + completed = complete_value(exe_context, return_type, field_asts, info, result) if is_thenable(completed): def handle_error(error): traceback = completed._traceback @@ -364,7 +364,6 @@ def complete_value(exe_context, return_type, field_asts, info, result): """ # If field type is NonNull, complete for inner type, and throw field error # if result is null. - if is_thenable(result): return Promise.resolve(result).then( lambda resolved: complete_value( @@ -419,13 +418,17 @@ def complete_list_value(exe_context, return_type, field_asts, info, result): item_type = return_type.of_type completed_results = [] contains_promise = False + + index = 0 + path = info.path[:] for item in result: - completed_item = complete_value_catching_error( - exe_context, item_type, field_asts, info, item) + info.path = path + [index] + completed_item = complete_value_catching_error(exe_context, item_type, field_asts, info, item) if not contains_promise and is_thenable(completed_item): contains_promise = True completed_results.append(completed_item) + index += 1 return Promise.all(completed_results) if contains_promise else completed_results @@ -501,7 +504,7 @@ def complete_object_value(exe_context, return_type, field_asts, info, result): # Collect sub-fields to execute to complete this value. subfield_asts = exe_context.get_sub_fields(return_type, field_asts) - return execute_fields(exe_context, return_type, result, subfield_asts) + return execute_fields(exe_context, return_type, result, subfield_asts, info) def complete_nonnull_value(exe_context, return_type, field_asts, info, result): 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/tox.ini b/tox.ini index 929bb4e3..215e2656 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ deps = promise>=2.0 six>=1.10.0 pytest-mock + pytest-benchmark commands = py{27,33,34,py}: py.test graphql tests {posargs} py35: py.test graphql tests tests_py35 {posargs}