Skip to content

Add execution path information to Info variable #148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jan 21, 2018
2 changes: 1 addition & 1 deletion graphql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@
)


VERSION = (2, 0, 0, 'final', 0)
VERSION = (2, 0, 1, 'final', 0)
__version__ = get_version(VERSION)


Expand Down
10 changes: 6 additions & 4 deletions graphql/execution/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,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
Expand Down Expand Up @@ -294,10 +295,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
Expand All @@ -308,6 +309,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):
Expand Down
43 changes: 23 additions & 20 deletions graphql/execution/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
147 changes: 146 additions & 1 deletion graphql/execution/tests/test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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']
]

1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down