Skip to content

Commit b3a6bd0

Browse files
authored
Merge pull request #148 from ekampf/feature/tracing_support
Add execution path information to Info variable
2 parents 6a55133 + b791ac3 commit b3a6bd0

File tree

5 files changed

+177
-26
lines changed

5 files changed

+177
-26
lines changed

graphql/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@
194194
)
195195

196196

197-
VERSION = (2, 0, 0, 'final', 0)
197+
VERSION = (2, 0, 1, 'final', 0)
198198
__version__ = get_version(VERSION)
199199

200200

graphql/execution/base.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,12 @@ class ExecutionResult(object):
129129
query, `errors` is null if no errors occurred, and is a
130130
non-empty array if an error occurred."""
131131

132-
__slots__ = 'data', 'errors', 'invalid'
132+
__slots__ = 'data', 'errors', 'invalid', 'extensions'
133133

134-
def __init__(self, data=None, errors=None, invalid=False):
134+
def __init__(self, data=None, errors=None, invalid=False, extensions=None):
135135
self.data = data
136136
self.errors = errors
137+
self.extensions = extensions or dict()
137138

138139
if invalid:
139140
assert data is None
@@ -297,10 +298,10 @@ def get_field_entry_key(node):
297298

298299
class ResolveInfo(object):
299300
__slots__ = ('field_name', 'field_asts', 'return_type', 'parent_type',
300-
'schema', 'fragments', 'root_value', 'operation', 'variable_values', 'context')
301+
'schema', 'fragments', 'root_value', 'operation', 'variable_values', 'context', 'path')
301302

302303
def __init__(self, field_name, field_asts, return_type, parent_type,
303-
schema, fragments, root_value, operation, variable_values, context):
304+
schema, fragments, root_value, operation, variable_values, context, path):
304305
self.field_name = field_name
305306
self.field_asts = field_asts
306307
self.return_type = return_type
@@ -311,6 +312,7 @@ def __init__(self, field_name, field_asts, return_type, parent_type,
311312
self.operation = operation
312313
self.variable_values = variable_values
313314
self.context = context
315+
self.path = path
314316

315317

316318
def default_resolve_fn(source, info, **args):

graphql/execution/executor.py

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def execute(schema, document_ast, root_value=None, context_value=None,
3636
'Schema must be an instance of GraphQLSchema. Also ensure that there are ' +
3737
'not multiple versions of GraphQL installed in your node_modules directory.'
3838
)
39+
3940
if middleware:
4041
if not isinstance(middleware, MiddlewareManager):
4142
middleware = MiddlewareManager(*middleware)
@@ -73,10 +74,10 @@ def on_resolve(data):
7374

7475
if not context.errors:
7576
return ExecutionResult(data=data)
77+
7678
return ExecutionResult(data=data, errors=context.errors)
7779

78-
promise = Promise.resolve(None).then(
79-
executor).catch(on_rejected).then(on_resolve)
80+
promise = Promise.resolve(None).then(executor).catch(on_rejected).then(on_resolve)
8081

8182
if not return_promise:
8283
context.executor.wait_until_finished()
@@ -107,7 +108,7 @@ def execute_operation(exe_context, operation, root_value):
107108
)
108109
return subscribe_fields(exe_context, type, root_value, fields)
109110

110-
return execute_fields(exe_context, type, root_value, fields)
111+
return execute_fields(exe_context, type, root_value, fields, None)
111112

112113

113114
def execute_fields_serially(exe_context, parent_type, source_value, fields):
@@ -117,7 +118,8 @@ def execute_field_callback(results, response_name):
117118
exe_context,
118119
parent_type,
119120
source_value,
120-
field_asts
121+
field_asts,
122+
None
121123
)
122124
if result is Undefined:
123125
return results
@@ -138,14 +140,13 @@ def execute_field(prev_promise, response_name):
138140
return functools.reduce(execute_field, fields.keys(), Promise.resolve(collections.OrderedDict()))
139141

140142

141-
def execute_fields(exe_context, parent_type, source_value, fields):
143+
def execute_fields(exe_context, parent_type, source_value, fields, info):
142144
contains_promise = False
143145

144146
final_results = OrderedDict()
145147

146148
for response_name, field_asts in fields.items():
147-
result = resolve_field(exe_context, parent_type,
148-
source_value, field_asts)
149+
result = resolve_field(exe_context, parent_type, source_value, field_asts, info)
149150
if result is Undefined:
150151
continue
151152

@@ -179,8 +180,7 @@ def map_result(data):
179180

180181
for response_name, field_asts in fields.items():
181182

182-
result = subscribe_field(exe_context, parent_type,
183-
source_value, field_asts)
183+
result = subscribe_field(exe_context, parent_type, source_value, field_asts)
184184
if result is Undefined:
185185
continue
186186

@@ -197,7 +197,7 @@ def catch_error(error):
197197
return Observable.merge(observables)
198198

199199

200-
def resolve_field(exe_context, parent_type, source, field_asts):
200+
def resolve_field(exe_context, parent_type, source, field_asts, parent_info):
201201
field_ast = field_asts[0]
202202
field_name = field_ast.name.value
203203

@@ -232,12 +232,12 @@ def resolve_field(exe_context, parent_type, source, field_asts):
232232
root_value=exe_context.root_value,
233233
operation=exe_context.operation,
234234
variable_values=exe_context.variable_values,
235-
context=context
235+
context=context,
236+
path=parent_info.path+[field_name] if parent_info else [field_name]
236237
)
237238

238239
executor = exe_context.executor
239-
result = resolve_or_error(resolve_fn_middleware,
240-
source, info, args, executor)
240+
result = resolve_or_error(resolve_fn_middleware, source, info, args, executor)
241241

242242
return complete_value_catching_error(
243243
exe_context,
@@ -283,7 +283,8 @@ def subscribe_field(exe_context, parent_type, source, field_asts):
283283
root_value=exe_context.root_value,
284284
operation=exe_context.operation,
285285
variable_values=exe_context.variable_values,
286-
context=context
286+
context=context,
287+
path=[field_name]
287288
)
288289

289290
executor = exe_context.executor
@@ -326,8 +327,7 @@ def complete_value_catching_error(exe_context, return_type, field_asts, info, re
326327
# Otherwise, error protection is applied, logging the error and
327328
# resolving a null value for this field if one is encountered.
328329
try:
329-
completed = complete_value(
330-
exe_context, return_type, field_asts, info, result)
330+
completed = complete_value(exe_context, return_type, field_asts, info, result)
331331
if is_thenable(completed):
332332
def handle_error(error):
333333
traceback = completed._traceback
@@ -364,7 +364,6 @@ def complete_value(exe_context, return_type, field_asts, info, result):
364364
"""
365365
# If field type is NonNull, complete for inner type, and throw field error
366366
# if result is null.
367-
368367
if is_thenable(result):
369368
return Promise.resolve(result).then(
370369
lambda resolved: complete_value(
@@ -419,13 +418,17 @@ def complete_list_value(exe_context, return_type, field_asts, info, result):
419418
item_type = return_type.of_type
420419
completed_results = []
421420
contains_promise = False
421+
422+
index = 0
423+
path = info.path[:]
422424
for item in result:
423-
completed_item = complete_value_catching_error(
424-
exe_context, item_type, field_asts, info, item)
425+
info.path = path + [index]
426+
completed_item = complete_value_catching_error(exe_context, item_type, field_asts, info, item)
425427
if not contains_promise and is_thenable(completed_item):
426428
contains_promise = True
427429

428430
completed_results.append(completed_item)
431+
index += 1
429432

430433
return Promise.all(completed_results) if contains_promise else completed_results
431434

@@ -501,7 +504,7 @@ def complete_object_value(exe_context, return_type, field_asts, info, result):
501504

502505
# Collect sub-fields to execute to complete this value.
503506
subfield_asts = exe_context.get_sub_fields(return_type, field_asts)
504-
return execute_fields(exe_context, return_type, result, subfield_asts)
507+
return execute_fields(exe_context, return_type, result, subfield_asts, info)
505508

506509

507510
def complete_nonnull_value(exe_context, return_type, field_asts, info, result):

graphql/execution/tests/test_executor.py

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from graphql.language.parser import parse
88
from graphql.type import (GraphQLArgument, GraphQLBoolean, GraphQLField,
99
GraphQLInt, GraphQLList, GraphQLObjectType,
10-
GraphQLSchema, GraphQLString)
10+
GraphQLSchema, GraphQLString, GraphQLNonNull, GraphQLID)
1111
from promise import Promise
1212

1313

@@ -668,3 +668,148 @@ def resolve(self, next, *args, **kwargs):
668668
middleware=middlewares_without_promise)
669669
assert result1.data == result2.data and result1.data == {
670670
'ok': 'ok', 'not_ok': 'not_ok'}
671+
672+
673+
def test_executor_properly_propogates_path_data(mocker):
674+
time_mock = mocker.patch('time.time')
675+
time_mock.side_effect = range(0, 10000)
676+
677+
BlogImage = GraphQLObjectType('BlogImage', {
678+
'url': GraphQLField(GraphQLString),
679+
'width': GraphQLField(GraphQLInt),
680+
'height': GraphQLField(GraphQLInt),
681+
})
682+
683+
BlogAuthor = GraphQLObjectType('Author', lambda: {
684+
'id': GraphQLField(GraphQLString),
685+
'name': GraphQLField(GraphQLString),
686+
'pic': GraphQLField(BlogImage,
687+
args={
688+
'width': GraphQLArgument(GraphQLInt),
689+
'height': GraphQLArgument(GraphQLInt),
690+
},
691+
resolver=lambda obj, info, **args:
692+
obj.pic(args['width'], args['height'])
693+
),
694+
'recentArticle': GraphQLField(BlogArticle),
695+
})
696+
697+
BlogArticle = GraphQLObjectType('Article', {
698+
'id': GraphQLField(GraphQLNonNull(GraphQLString)),
699+
'isPublished': GraphQLField(GraphQLBoolean),
700+
'author': GraphQLField(BlogAuthor),
701+
'title': GraphQLField(GraphQLString),
702+
'body': GraphQLField(GraphQLString),
703+
'keywords': GraphQLField(GraphQLList(GraphQLString)),
704+
})
705+
706+
BlogQuery = GraphQLObjectType('Query', {
707+
'article': GraphQLField(
708+
BlogArticle,
709+
args={'id': GraphQLArgument(GraphQLID)},
710+
resolver=lambda obj, info, **args: Article(args['id'])),
711+
'feed': GraphQLField(
712+
GraphQLList(BlogArticle),
713+
resolver=lambda *_: map(Article, range(1, 2 + 1))),
714+
})
715+
716+
BlogSchema = GraphQLSchema(BlogQuery)
717+
718+
class Article(object):
719+
720+
def __init__(self, id):
721+
self.id = id
722+
self.isPublished = True
723+
self.author = Author()
724+
self.title = 'My Article {}'.format(id)
725+
self.body = 'This is a post'
726+
self.hidden = 'This data is not exposed in the schema'
727+
self.keywords = ['foo', 'bar', 1, True, None]
728+
729+
class Author(object):
730+
id = 123
731+
name = 'John Smith'
732+
733+
def pic(self, width, height):
734+
return Pic(123, width, height)
735+
736+
@property
737+
def recentArticle(self): return Article(1)
738+
739+
class Pic(object):
740+
def __init__(self, uid, width, height):
741+
self.url = 'cdn://{}'.format(uid)
742+
self.width = str(width)
743+
self.height = str(height)
744+
745+
class PathCollectorMiddleware(object):
746+
def __init__(self):
747+
self.paths = []
748+
749+
def resolve(self, _next, root, info, *args, **kwargs):
750+
self.paths.append(info.path)
751+
return _next(root, info, *args, **kwargs)
752+
753+
request = '''
754+
{
755+
feed {
756+
id
757+
...articleFields
758+
author {
759+
id
760+
name
761+
}
762+
},
763+
}
764+
fragment articleFields on Article {
765+
title,
766+
body,
767+
hidden,
768+
}
769+
'''
770+
771+
paths_middleware = PathCollectorMiddleware()
772+
773+
result = execute(BlogSchema, parse(request), middleware=(paths_middleware, ))
774+
assert not result.errors
775+
assert result.data == \
776+
{
777+
"feed": [
778+
{
779+
"id": "1",
780+
"title": "My Article 1",
781+
"body": "This is a post",
782+
"author": {
783+
"id": "123",
784+
"name": "John Smith"
785+
}
786+
},
787+
{
788+
"id": "2",
789+
"title": "My Article 2",
790+
"body": "This is a post",
791+
"author": {
792+
"id": "123",
793+
"name": "John Smith"
794+
}
795+
},
796+
],
797+
}
798+
799+
traversed_paths = paths_middleware.paths
800+
assert traversed_paths == [
801+
['feed'],
802+
['feed', 0, 'id'],
803+
['feed', 0, 'title'],
804+
['feed', 0, 'body'],
805+
['feed', 0, 'author'],
806+
['feed', 1, 'id'],
807+
['feed', 1, 'title'],
808+
['feed', 1, 'body'],
809+
['feed', 1, 'author'],
810+
['feed', 0, 'author', 'id'],
811+
['feed', 0, 'author', 'name'],
812+
['feed', 1, 'author', 'id'],
813+
['feed', 1, 'author', 'name']
814+
]
815+

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ deps =
88
promise>=2.0
99
six>=1.10.0
1010
pytest-mock
11+
pytest-benchmark
1112
commands =
1213
py{27,33,34,py}: py.test graphql tests {posargs}
1314
py35: py.test graphql tests tests_py35 {posargs}

0 commit comments

Comments
 (0)