diff --git a/graphql/error/base.py b/graphql/error/base.py index 6af9caf2..b894ecd5 100644 --- a/graphql/error/base.py +++ b/graphql/error/base.py @@ -3,9 +3,9 @@ class GraphQLError(Exception): - __slots__ = 'message', 'nodes', 'stack', 'original_error', '_source', '_positions', '_locations' + __slots__ = 'message', 'nodes', 'stack', 'original_error', '_source', '_positions', '_locations', 'path' - def __init__(self, message, nodes=None, stack=None, source=None, positions=None, locations=None): + def __init__(self, message, nodes=None, stack=None, source=None, positions=None, locations=None, path=None): super(GraphQLError, self).__init__(message) self.message = message self.nodes = nodes @@ -13,6 +13,7 @@ def __init__(self, message, nodes=None, stack=None, source=None, positions=None, self._source = source self._positions = positions self._locations = locations + self.path = path @property def source(self): diff --git a/graphql/error/format_error.py b/graphql/error/format_error.py index 1ffbd942..3ccdc883 100644 --- a/graphql/error/format_error.py +++ b/graphql/error/format_error.py @@ -11,5 +11,7 @@ def format_error(error): {'line': loc.line, 'column': loc.column} for loc in error.locations ] + if error.path is not None: + formatted_error['path'] = error.path return formatted_error diff --git a/graphql/error/located_error.py b/graphql/error/located_error.py index 9111f764..351989e2 100644 --- a/graphql/error/located_error.py +++ b/graphql/error/located_error.py @@ -7,7 +7,7 @@ class GraphQLLocatedError(GraphQLError): - def __init__(self, nodes, original_error=None): + def __init__(self, nodes, original_error=None, path=None): if original_error: try: message = str(original_error) @@ -24,6 +24,7 @@ def __init__(self, nodes, original_error=None): super(GraphQLLocatedError, self).__init__( message=message, nodes=nodes, - stack=stack + stack=stack, + path=path ) self.original_error = original_error diff --git a/graphql/execution/executor.py b/graphql/execution/executor.py index 17eef54f..fd5cea65 100644 --- a/graphql/execution/executor.py +++ b/graphql/execution/executor.py @@ -124,7 +124,7 @@ def execute_operation(exe_context, operation, root_value): ) if operation.operation == 'mutation': - return execute_fields_serially(exe_context, type, root_value, fields) + return execute_fields_serially(exe_context, type, root_value, [], fields) if operation.operation == 'subscription': if not exe_context.allow_subscriptions: @@ -133,12 +133,12 @@ def execute_operation(exe_context, operation, root_value): "You will need to either use the subscribe function " "or pass allow_subscriptions=True" ) - return subscribe_fields(exe_context, type, root_value, fields) + return subscribe_fields(exe_context, type, root_value, fields,) - return execute_fields(exe_context, type, root_value, fields, None) + return execute_fields(exe_context, type, root_value, fields, [], None) -def execute_fields_serially(exe_context, parent_type, source_value, fields): +def execute_fields_serially(exe_context, parent_type, source_value, path, fields): def execute_field_callback(results, response_name): field_asts = fields[response_name] result = resolve_field( @@ -147,7 +147,7 @@ def execute_field_callback(results, response_name): source_value, field_asts, None, - [response_name] + path+[response_name] ) if result is Undefined: return results @@ -168,13 +168,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, info): +def execute_fields(exe_context, parent_type, source_value, fields, path, 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, info, (info.path if info else []) + [response_name]) + result = resolve_field(exe_context, parent_type, source_value, field_asts, info, path + [response_name]) if result is Undefined: continue @@ -207,8 +207,7 @@ def map_result(data): # assert len(fields) == 1, "Can only subscribe one element at a time." 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, [response_name]) if result is Undefined: continue @@ -272,11 +271,12 @@ def resolve_field(exe_context, parent_type, source, field_asts, parent_info, fie return_type, field_asts, info, + field_path, result ) -def subscribe_field(exe_context, parent_type, source, field_asts): +def subscribe_field(exe_context, parent_type, source, field_asts, path): field_ast = field_asts[0] field_name = field_ast.name.value @@ -312,7 +312,7 @@ def subscribe_field(exe_context, parent_type, source, field_asts): operation=exe_context.operation, variable_values=exe_context.variable_values, context=context, - path=[field_name] + path=path ) executor = exe_context.executor @@ -332,6 +332,7 @@ def subscribe_field(exe_context, parent_type, source, field_asts): return_type, field_asts, info, + path, )) @@ -346,16 +347,16 @@ def resolve_or_error(resolve_fn, source, info, args, executor): return e -def complete_value_catching_error(exe_context, return_type, field_asts, info, result): +def complete_value_catching_error(exe_context, return_type, field_asts, info, path, result): # If the field type is non-nullable, then it is resolved without any # protection from errors. if isinstance(return_type, GraphQLNonNull): - return complete_value(exe_context, return_type, field_asts, info, result) + return complete_value(exe_context, return_type, field_asts, info, path, result) # 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, path, result) if is_thenable(completed): def handle_error(error): traceback = completed._traceback @@ -371,7 +372,7 @@ def handle_error(error): return None -def complete_value(exe_context, return_type, field_asts, info, result): +def complete_value(exe_context, return_type, field_asts, info, path, result): """ Implements the instructions for completeValue as defined in the "Field entries" section of the spec. @@ -399,18 +400,19 @@ def complete_value(exe_context, return_type, field_asts, info, result): return_type, field_asts, info, + path, resolved ), lambda error: Promise.rejected( - GraphQLLocatedError(field_asts, original_error=error)) + GraphQLLocatedError(field_asts, original_error=error, path=path)) ) # print return_type, type(result) if isinstance(result, Exception): - raise GraphQLLocatedError(field_asts, original_error=result) + raise GraphQLLocatedError(field_asts, original_error=result, path=path) if isinstance(return_type, GraphQLNonNull): - return complete_nonnull_value(exe_context, return_type, field_asts, info, result) + return complete_nonnull_value(exe_context, return_type, field_asts, info, path, result) # If result is null-like, return null. if result is None: @@ -418,24 +420,24 @@ def complete_value(exe_context, return_type, field_asts, info, result): # If field type is List, complete each item in the list with the inner type if isinstance(return_type, GraphQLList): - return complete_list_value(exe_context, return_type, field_asts, info, result) + return complete_list_value(exe_context, return_type, field_asts, info, path, result) # If field type is Scalar or Enum, serialize to a valid value, returning # null if coercion is not possible. if isinstance(return_type, (GraphQLScalarType, GraphQLEnumType)): - return complete_leaf_value(return_type, result) + return complete_leaf_value(return_type, path, result) if isinstance(return_type, (GraphQLInterfaceType, GraphQLUnionType)): - return complete_abstract_value(exe_context, return_type, field_asts, info, result) + return complete_abstract_value(exe_context, return_type, field_asts, info, path, result) if isinstance(return_type, GraphQLObjectType): - return complete_object_value(exe_context, return_type, field_asts, info, result) + return complete_object_value(exe_context, return_type, field_asts, info, path, result) assert False, u'Cannot complete value of unexpected type "{}".'.format( return_type) -def complete_list_value(exe_context, return_type, field_asts, info, result): +def complete_list_value(exe_context, return_type, field_asts, info, path, result): """ Complete a list value by completing each item in the list with the inner type """ @@ -448,10 +450,8 @@ def complete_list_value(exe_context, return_type, field_asts, info, result): contains_promise = False index = 0 - path = info.path[:] for item in result: - info.path = path + [index] - completed_item = complete_value_catching_error(exe_context, item_type, field_asts, info, item) + completed_item = complete_value_catching_error(exe_context, item_type, field_asts, info, path + [index], item, ) if not contains_promise and is_thenable(completed_item): contains_promise = True @@ -461,7 +461,7 @@ def complete_list_value(exe_context, return_type, field_asts, info, result): return Promise.all(completed_results) if contains_promise else completed_results -def complete_leaf_value(return_type, result): +def complete_leaf_value(return_type, path, result): """ Complete a Scalar or Enum by serializing to a valid value, returning null if serialization is not possible. """ @@ -471,12 +471,13 @@ def complete_leaf_value(return_type, result): if serialized_result is None: raise GraphQLError( ('Expected a value of type "{}" but ' + - 'received: {}').format(return_type, result) + 'received: {}').format(return_type, result), + path=path ) return serialized_result -def complete_abstract_value(exe_context, return_type, field_asts, info, result): +def complete_abstract_value(exe_context, return_type, field_asts, info, path, result): """ Complete an value of an abstract type by determining the runtime type of that value, then completing based on that type. @@ -514,7 +515,7 @@ def complete_abstract_value(exe_context, return_type, field_asts, info, result): field_asts ) - return complete_object_value(exe_context, runtime_type, field_asts, info, result) + return complete_object_value(exe_context, runtime_type, field_asts, info, path, result) def get_default_resolve_type_fn(value, info, abstract_type): @@ -524,7 +525,7 @@ def get_default_resolve_type_fn(value, info, abstract_type): return type -def complete_object_value(exe_context, return_type, field_asts, info, result): +def complete_object_value(exe_context, return_type, field_asts, info, path, result): """ Complete an Object value by evaluating all sub-selections. """ @@ -537,21 +538,22 @@ 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, info) + return execute_fields(exe_context, return_type, result, subfield_asts, path, info) -def complete_nonnull_value(exe_context, return_type, field_asts, info, result): +def complete_nonnull_value(exe_context, return_type, field_asts, info, path, result): """ Complete a NonNull value by completing the inner type """ completed = complete_value( - exe_context, return_type.of_type, field_asts, info, result + exe_context, return_type.of_type, field_asts, info, path, result ) if completed is None: raise GraphQLError( 'Cannot return null for non-nullable field {}.{}.'.format( info.parent_type, info.field_name), - field_asts + field_asts, + path=path ) return completed diff --git a/graphql/execution/tests/test_executor_asyncio.py b/graphql/execution/tests/test_executor_asyncio.py index ddba29c2..ded4132b 100644 --- a/graphql/execution/tests/test_executor_asyncio.py +++ b/graphql/execution/tests/test_executor_asyncio.py @@ -86,7 +86,11 @@ def resolver_2(context, *_): result = execute(GraphQLSchema(Type), ast, executor=AsyncioExecutor()) formatted_errors = list(map(format_error, result.errors)) - assert formatted_errors == [{'locations': [{'line': 1, 'column': 20}], 'message': 'resolver_2 failed!'}] + assert formatted_errors == [{ + 'locations': [{'line': 1, 'column': 20}], + 'path': ['b'], + 'message': 'resolver_2 failed!' + }] assert result.data == {'a': 'hey', 'b': None} diff --git a/graphql/execution/tests/test_executor_gevent.py b/graphql/execution/tests/test_executor_gevent.py index 15d5ee85..04d164f7 100644 --- a/graphql/execution/tests/test_executor_gevent.py +++ b/graphql/execution/tests/test_executor_gevent.py @@ -59,7 +59,11 @@ def resolver_2(context, *_): result = execute(GraphQLSchema(Type), ast, executor=GeventExecutor()) formatted_errors = list(map(format_error, result.errors)) - assert formatted_errors == [{'locations': [{'line': 1, 'column': 20}], 'message': 'resolver_2 failed!'}] + assert formatted_errors == [{ + 'locations': [{'line': 1, 'column': 20}], + 'path': ['b'], + 'message': 'resolver_2 failed!' + }] assert result.data == {'a': 'hey', 'b': None} diff --git a/graphql/execution/tests/test_executor_thread.py b/graphql/execution/tests/test_executor_thread.py index 2838c9f2..5b44255d 100644 --- a/graphql/execution/tests/test_executor_thread.py +++ b/graphql/execution/tests/test_executor_thread.py @@ -206,13 +206,13 @@ def handle_results(result): 'syncReturnErrorList': ['sync0', None, 'sync2', None] } assert sorted(list(map(format_error, result.errors)), key=sort_key) == sorted([ - {'locations': [{'line': 4, 'column': 9}], 'message': 'Error getting syncError'}, - {'locations': [{'line': 5, 'column': 9}], 'message': 'Error getting syncReturnError'}, - {'locations': [{'line': 6, 'column': 9}], 'message': 'Error getting syncReturnErrorList1'}, - {'locations': [{'line': 6, 'column': 9}], 'message': 'Error getting syncReturnErrorList3'}, - {'locations': [{'line': 8, 'column': 9}], 'message': 'Error getting asyncReject'}, - {'locations': [{'line': 9, 'column': 9}], 'message': 'An unknown error occurred.'}, - {'locations': [{'line': 10, 'column': 9}], 'message': 'Error getting asyncReturnError'} + {'locations': [{'line': 4, 'column': 9}], 'path':['syncError'], 'message': 'Error getting syncError'}, + {'locations': [{'line': 5, 'column': 9}], 'path':['syncReturnError'], 'message': 'Error getting syncReturnError'}, + {'locations': [{'line': 6, 'column': 9}], 'path':['syncReturnErrorList', 1], 'message': 'Error getting syncReturnErrorList1'}, + {'locations': [{'line': 6, 'column': 9}], 'path':['syncReturnErrorList', 3], 'message': 'Error getting syncReturnErrorList3'}, + {'locations': [{'line': 8, 'column': 9}], 'path':['asyncReject'], 'message': 'Error getting asyncReject'}, + {'locations': [{'line': 9, 'column': 9}], 'path':['asyncEmptyReject'], 'message': 'An unknown error occurred.'}, + {'locations': [{'line': 10, 'column': 9}], 'path':['asyncReturnError'], 'message': 'Error getting asyncReturnError'} ], key=sort_key) handle_results(execute(schema, ast, Data(), executor=ThreadExecutor())) diff --git a/graphql/execution/tests/test_lists.py b/graphql/execution/tests/test_lists.py index 034fe7c2..c0874776 100644 --- a/graphql/execution/tests/test_lists.py +++ b/graphql/execution/tests/test_lists.py @@ -59,7 +59,11 @@ class Test_ListOfT_Promise_Array_T: # [T] Promise> test_returns_null = check(resolved(None), {'data': {'nest': {'test': None}}}) test_rejected = check(lambda: rejected(Exception('bad')), { 'data': {'nest': {'test': None}}, - 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + 'errors': [{ + 'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test'], + 'message': 'bad' + }] }) @@ -70,7 +74,11 @@ class Test_ListOfT_Array_Promise_T: # [T] Array> test_contains_null = check([resolved(1), resolved(None), resolved(2)], {'data': {'nest': {'test': [1, None, 2]}}}) test_contains_reject = check(lambda: [resolved(1), rejected(Exception('bad')), resolved(2)], { 'data': {'nest': {'test': [1, None, 2]}}, - 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + 'errors': [{ + 'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test', 1], + 'message': 'bad' + }] }) @@ -82,6 +90,7 @@ class Test_NotNullListOfT_Array_T: # [T]! Array test_returns_null = check(resolved(None), { 'data': {'nest': None}, 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test'], 'message': 'Cannot return null for non-nullable field DataType.test.'}] }) @@ -94,12 +103,17 @@ class Test_NotNullListOfT_Promise_Array_T: # [T]! Promise>> test_returns_null = check(resolved(None), { 'data': {'nest': None}, 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test'], 'message': 'Cannot return null for non-nullable field DataType.test.'}] }) test_rejected = check(lambda: rejected(Exception('bad')), { 'data': {'nest': None}, - 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + 'errors': [{ + 'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test'], + 'message': 'bad' + }] }) @@ -109,7 +123,11 @@ class Test_NotNullListOfT_Array_Promise_T: # [T]! Promise>> test_contains_null = check([resolved(1), resolved(None), resolved(2)], {'data': {'nest': {'test': [1, None, 2]}}}) test_contains_reject = check(lambda: [resolved(1), rejected(Exception('bad')), resolved(2)], { 'data': {'nest': {'test': [1, None, 2]}}, - 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + 'errors': [{ + 'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test', 1], + 'message': 'bad' + }] }) @@ -120,6 +138,7 @@ class TestListOfNotNullT_Array_T: # [T!] Array test_contains_null = check([1, None, 2], { 'data': {'nest': {'test': None}}, 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test', 1], 'message': 'Cannot return null for non-nullable field DataType.test.'}] }) test_returns_null = check(None, {'data': {'nest': {'test': None}}}) @@ -132,6 +151,7 @@ class TestListOfNotNullT_Promise_Array_T: # [T!] Promise> test_contains_null = check(resolved([1, None, 2]), { 'data': {'nest': {'test': None}}, 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test', 1], 'message': 'Cannot return null for non-nullable field DataType.test.'}] }) @@ -139,7 +159,11 @@ class TestListOfNotNullT_Promise_Array_T: # [T!] Promise> test_rejected = check(lambda: rejected(Exception('bad')), { 'data': {'nest': {'test': None}}, - 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + 'errors': [{ + 'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test'], + 'message': 'bad' + }] }) @@ -150,11 +174,16 @@ class TestListOfNotNullT_Array_Promise_T: # [T!] Array> test_contains_null = check([resolved(1), resolved(None), resolved(2)], { 'data': {'nest': {'test': None}}, 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test', 1], 'message': 'Cannot return null for non-nullable field DataType.test.'}] }) test_contains_reject = check(lambda: [resolved(1), rejected(Exception('bad')), resolved(2)], { 'data': {'nest': {'test': None}}, - 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + 'errors': [{ + 'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test', 1], + 'message': 'bad' + }] }) @@ -165,11 +194,13 @@ class TestNotNullListOfNotNullT_Array_T: # [T!]! Array test_contains_null = check([1, None, 2], { 'data': {'nest': None}, 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test', 1], 'message': 'Cannot return null for non-nullable field DataType.test.'}] }) test_returns_null = check(None, { 'data': {'nest': None}, 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test'], 'message': 'Cannot return null for non-nullable field DataType.test.'}] }) @@ -181,18 +212,24 @@ class TestNotNullListOfNotNullT_Promise_Array_T: # [T!]! Promise> test_contains_null = check(resolved([1, None, 2]), { 'data': {'nest': None}, 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test', 1], 'message': 'Cannot return null for non-nullable field DataType.test.'}] }) test_returns_null = check(resolved(None), { 'data': {'nest': None}, 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test'], 'message': 'Cannot return null for non-nullable field DataType.test.'}] }) test_rejected = check(lambda: rejected(Exception('bad')), { 'data': {'nest': None}, - 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + 'errors': [{ + 'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test'], + 'message': 'bad' + }] }) @@ -203,9 +240,14 @@ class TestNotNullListOfNotNullT_Array_Promise_T: # [T!]! Array> test_contains_null = check([resolved(1), resolved(None), resolved(2)], { 'data': {'nest': None}, 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test', 1], 'message': 'Cannot return null for non-nullable field DataType.test.'}] }) test_contains_reject = check(lambda: [resolved(1), rejected(Exception('bad')), resolved(2)], { 'data': {'nest': None}, - 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + 'errors': [{ + 'locations': [{'column': 10, 'line': 1}], + 'path': ['nest', 'test', 1], + 'message': 'bad' + }] }) diff --git a/graphql/execution/tests/test_nonnull.py b/graphql/execution/tests/test_nonnull.py index 65d7f098..b72e2f30 100644 --- a/graphql/execution/tests/test_nonnull.py +++ b/graphql/execution/tests/test_nonnull.py @@ -120,7 +120,11 @@ def test_nulls_a_nullable_field_that_throws_sync(): check(doc, ThrowingData(), { 'data': {'sync': None}, - 'errors': [{'locations': [{'column': 13, 'line': 3}], 'message': str(sync_error)}] + 'errors': [{ + 'locations': [{'column': 13, 'line': 3}], + 'path': ['sync'], + 'message': str(sync_error) + }] }) @@ -133,7 +137,11 @@ def test_nulls_a_nullable_field_that_throws_in_a_promise(): check(doc, ThrowingData(), { 'data': {'promise': None}, - 'errors': [{'locations': [{'column': 13, 'line': 3}], 'message': str(promise_error)}] + 'errors': [{ + 'locations': [{'column': 13, 'line': 3}], + 'path': ['promise'], + 'message': str(promise_error) + }] }) @@ -149,6 +157,7 @@ def test_nulls_a_sync_returned_object_that_contains_a_non_nullable_field_that_th check(doc, ThrowingData(), { 'data': {'nest': None}, 'errors': [{'locations': [{'column': 17, 'line': 4}], + 'path': ['nest', 'nonNullSync'], 'message': str(non_null_sync_error)}] }) @@ -165,6 +174,7 @@ def test_nulls_a_synchronously_returned_object_that_contains_a_non_nullable_fiel check(doc, ThrowingData(), { 'data': {'nest': None}, 'errors': [{'locations': [{'column': 17, 'line': 4}], + 'path': ['nest', 'nonNullPromise'], 'message': str(non_null_promise_error)}] }) @@ -181,6 +191,7 @@ def test_nulls_an_object_returned_in_a_promise_that_contains_a_non_nullable_fiel check(doc, ThrowingData(), { 'data': {'promiseNest': None}, 'errors': [{'locations': [{'column': 17, 'line': 4}], + 'path': ['promiseNest', 'nonNullSync'], 'message': str(non_null_sync_error)}] }) @@ -197,6 +208,7 @@ def test_nulls_an_object_returned_in_a_promise_that_contains_a_non_nullable_fiel check(doc, ThrowingData(), { 'data': {'promiseNest': None}, 'errors': [{'locations': [{'column': 17, 'line': 4}], + 'path': ['promiseNest', 'nonNullPromise'], 'message': str(non_null_promise_error)}] }) @@ -239,18 +251,18 @@ def test_nulls_a_complex_tree_of_nullable_fields_that_throw(): 'promise': None, 'promiseNest': {'promise': None, 'sync': None}, 'sync': None}}, - 'errors': [{'locations': [{'column': 11, 'line': 4}], 'message': str(sync_error)}, - {'locations': [{'column': 11, 'line': 5}], 'message': str(promise_error)}, - {'locations': [{'column': 13, 'line': 7}], 'message': str(sync_error)}, - {'locations': [{'column': 13, 'line': 8}], 'message': str(promise_error)}, - {'locations': [{'column': 13, 'line': 11}], 'message': str(sync_error)}, - {'locations': [{'column': 13, 'line': 12}], 'message': str(promise_error)}, - {'locations': [{'column': 11, 'line': 16}], 'message': str(sync_error)}, - {'locations': [{'column': 11, 'line': 17}], 'message': str(promise_error)}, - {'locations': [{'column': 13, 'line': 19}], 'message': str(sync_error)}, - {'locations': [{'column': 13, 'line': 20}], 'message': str(promise_error)}, - {'locations': [{'column': 13, 'line': 23}], 'message': str(sync_error)}, - {'locations': [{'column': 13, 'line': 24}], 'message': str(promise_error)}] + 'errors': [{'locations': [{'column': 11, 'line': 4}], 'path': ['nest', 'sync'], 'message': str(sync_error)}, + {'locations': [{'column': 11, 'line': 5}], 'path': ['nest', 'promise'], 'message': str(promise_error)}, + {'locations': [{'column': 13, 'line': 7}], 'path': ['nest', 'nest', 'sync'], 'message': str(sync_error)}, + {'locations': [{'column': 13, 'line': 8}], 'path': ['nest', 'nest', 'promise'], 'message': str(promise_error)}, + {'locations': [{'column': 13, 'line': 11}], 'path': ['nest', 'promiseNest', 'sync'], 'message': str(sync_error)}, + {'locations': [{'column': 13, 'line': 12}], 'path': ['nest', 'promiseNest', 'promise'], 'message': str(promise_error)}, + {'locations': [{'column': 11, 'line': 16}], 'path': ['promiseNest', 'sync'], 'message': str(sync_error)}, + {'locations': [{'column': 11, 'line': 17}], 'path': ['promiseNest', 'promise'], 'message': str(promise_error)}, + {'locations': [{'column': 13, 'line': 19}], 'path': ['promiseNest', 'nest', 'sync'], 'message': str(sync_error)}, + {'locations': [{'column': 13, 'line': 20}], 'path': ['promiseNest', 'nest', 'promise'], 'message': str(promise_error)}, + {'locations': [{'column': 13, 'line': 23}], 'path': ['promiseNest', 'promiseNest', 'sync'], 'message': str(sync_error)}, + {'locations': [{'column': 13, 'line': 24}], 'path': ['promiseNest', 'promiseNest', 'promise'], 'message': str(promise_error)}] }) @@ -306,12 +318,28 @@ def test_nulls_the_first_nullable_object_after_a_field_throws_in_a_long_chain_of check(doc, ThrowingData(), { 'data': {'nest': None, 'promiseNest': None, 'anotherNest': None, 'anotherPromiseNest': None}, 'errors': [{'locations': [{'column': 19, 'line': 8}], + 'path': [ + 'nest', 'nonNullNest', 'nonNullPromiseNest', + 'nonNullNest', 'nonNullPromiseNest', 'nonNullSync' + ], 'message': str(non_null_sync_error)}, {'locations': [{'column': 19, 'line': 19}], + 'path': [ + 'promiseNest', 'nonNullNest', 'nonNullPromiseNest', + 'nonNullNest', 'nonNullPromiseNest', 'nonNullSync' + ], 'message': str(non_null_sync_error)}, {'locations': [{'column': 19, 'line': 30}], + 'path': [ + 'anotherNest', 'nonNullNest', 'nonNullPromiseNest', + 'nonNullNest', 'nonNullPromiseNest', 'nonNullPromise' + ], 'message': str(non_null_promise_error)}, {'locations': [{'column': 19, 'line': 41}], + 'path': [ + 'anotherPromiseNest', 'nonNullNest', 'nonNullPromiseNest', + 'nonNullNest', 'nonNullPromiseNest', 'nonNullPromise' + ], 'message': str(non_null_promise_error)}] }) @@ -351,6 +379,7 @@ def test_nulls_a_sync_returned_object_that_contains_a_non_nullable_field_that_re check(doc, NullingData(), { 'data': {'nest': None}, 'errors': [{'locations': [{'column': 17, 'line': 4}], + 'path': ['nest', 'nonNullSync'], 'message': 'Cannot return null for non-nullable field DataType.nonNullSync.'}] }) @@ -366,6 +395,7 @@ def test_nulls_a_synchronously_returned_object_that_contains_a_non_nullable_fiel check(doc, NullingData(), { 'data': {'nest': None}, 'errors': [{'locations': [{'column': 17, 'line': 4}], + 'path': ['nest', 'nonNullPromise'], 'message': 'Cannot return null for non-nullable field DataType.nonNullPromise.'}] }) @@ -381,6 +411,7 @@ def test_nulls_an_object_returned_in_a_promise_that_contains_a_non_nullable_fiel check(doc, NullingData(), { 'data': {'promiseNest': None}, 'errors': [{'locations': [{'column': 17, 'line': 4}], + 'path': ['promiseNest', 'nonNullSync'], 'message': 'Cannot return null for non-nullable field DataType.nonNullSync.'}] }) @@ -398,6 +429,7 @@ def test_nulls_an_object_returned_in_a_promise_that_contains_a_non_nullable_fiel 'data': {'promiseNest': None}, 'errors': [ {'locations': [{'column': 17, 'line': 4}], + 'path': ['promiseNest', 'nonNullPromise'], 'message': 'Cannot return null for non-nullable field DataType.nonNullPromise.'} ] }) @@ -521,12 +553,36 @@ def test_nulls_the_first_nullable_object_after_a_field_returns_null_in_a_long_ch }, 'errors': [ {'locations': [{'column': 19, 'line': 8}], + 'path': ['nest', + 'nonNullNest', + 'nonNullPromiseNest', + 'nonNullNest', + 'nonNullPromiseNest', + 'nonNullSync'], 'message': 'Cannot return null for non-nullable field DataType.nonNullSync.'}, {'locations': [{'column': 19, 'line': 19}], + 'path': ['promiseNest', + 'nonNullNest', + 'nonNullPromiseNest', + 'nonNullNest', + 'nonNullPromiseNest', + 'nonNullSync'], 'message': 'Cannot return null for non-nullable field DataType.nonNullSync.'}, {'locations': [{'column': 19, 'line': 30}], + 'path': ['anotherNest', + 'nonNullNest', + 'nonNullPromiseNest', + 'nonNullNest', + 'nonNullPromiseNest', + 'nonNullPromise'], 'message': 'Cannot return null for non-nullable field DataType.nonNullPromise.'}, {'locations': [{'column': 19, 'line': 41}], + 'path': ['anotherPromiseNest', + 'nonNullNest', + 'nonNullPromiseNest', + 'nonNullNest', + 'nonNullPromiseNest', + 'nonNullPromise'], 'message': 'Cannot return null for non-nullable field DataType.nonNullPromise.'} ] }) @@ -540,6 +596,7 @@ def test_nulls_the_top_level_if_sync_non_nullable_field_throws(): 'data': None, 'errors': [ {'locations': [{'column': 19, 'line': 2}], + 'path': ['nonNullSync'], 'message': str(non_null_sync_error)} ] }) @@ -554,6 +611,7 @@ def test_nulls_the_top_level_if_async_non_nullable_field_errors(): 'data': None, 'errors': [ {'locations': [{'column': 19, 'line': 2}], + 'path': ['nonNullPromise'], 'message': str(non_null_promise_error)} ] }) @@ -567,6 +625,7 @@ def test_nulls_the_top_level_if_sync_non_nullable_field_returns_null(): 'data': None, 'errors': [ {'locations': [{'column': 19, 'line': 2}], + 'path': ['nonNullSync'], 'message': 'Cannot return null for non-nullable field DataType.nonNullSync.'} ] }) @@ -580,6 +639,7 @@ def test_nulls_the_top_level_if_async_non_nullable_field_resolves_null(): 'data': None, 'errors': [ {'locations': [{'column': 19, 'line': 2}], + 'path': ['nonNullPromise'], 'message': 'Cannot return null for non-nullable field DataType.nonNullPromise.'} ] }) diff --git a/graphql/utils/tests/test_build_client_schema.py b/graphql/utils/tests/test_build_client_schema.py index a3650830..6abecb3e 100644 --- a/graphql/utils/tests/test_build_client_schema.py +++ b/graphql/utils/tests/test_build_client_schema.py @@ -573,7 +573,11 @@ class data: assert result.data == {'foo': None} assert [format_error(e) for e in result.errors] == [ - {'locations': [{'column': 32, 'line': 1}], 'message': 'Client Schema cannot be used for execution.'} + { + 'locations': [{'column': 32, 'line': 1}], + 'message': 'Client Schema cannot be used for execution.', + 'path': ['foo'] + } ] diff --git a/tests_py35/core_execution/test_asyncio_executor.py b/tests_py35/core_execution/test_asyncio_executor.py index 79eb0737..b89d2f35 100644 --- a/tests_py35/core_execution/test_asyncio_executor.py +++ b/tests_py35/core_execution/test_asyncio_executor.py @@ -87,5 +87,9 @@ async def resolver_2(context, *_): result = execute(GraphQLSchema(Type), ast, executor=AsyncioExecutor()) formatted_errors = list(map(format_error, result.errors)) - assert formatted_errors == [{'locations': [{'line': 1, 'column': 20}], 'message': 'resolver_2 failed!'}] + assert formatted_errors == [{ + 'locations': [{'line': 1, 'column': 20}], + 'path': ['b'], + 'message': 'resolver_2 failed!' + }] assert result.data == {'a': 'hey', 'b': None}