Skip to content

Commit

Permalink
Merge pull request graphql-python#148 from ekampf/feature/tracing_sup…
Browse files Browse the repository at this point in the history
…port

Add execution path information to Info variable
  • Loading branch information
syrusakbary authored Jan 21, 2018
2 parents 6a55133 + b791ac3 commit b3a6bd0
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 26 deletions.
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 @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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):
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

0 comments on commit b3a6bd0

Please sign in to comment.