Skip to content

Commit 92bff0f

Browse files
committed
Merge pull request #20 from graphql-python/strict-key-ordering
Add option to specify `map_type` in the Executor. Closes #19
2 parents ad228f0 + 02e7331 commit 92bff0f

File tree

5 files changed

+102
-16
lines changed

5 files changed

+102
-16
lines changed

graphql/core/execution/executor.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,21 @@
1515

1616

1717
class Executor(object):
18-
def __init__(self, execution_middlewares=None, default_resolver=default_resolve_fn):
19-
self.execution_middlewares = execution_middlewares or []
20-
self.default_resolve_fn = default_resolver
18+
def __init__(self, execution_middlewares=None, default_resolver=default_resolve_fn, map_type=dict):
19+
assert issubclass(map_type, collections.MutableMapping)
20+
21+
self._execution_middlewares = execution_middlewares or []
22+
self._default_resolve_fn = default_resolver
23+
self._map_type = map_type
24+
self._enforce_strict_ordering = issubclass(map_type, collections.OrderedDict)
25+
26+
@property
27+
def enforce_strict_ordering(self):
28+
return self._enforce_strict_ordering
29+
30+
@property
31+
def map_type(self):
32+
return self._map_type
2133

2234
def execute(self, schema, request='', root=None, args=None, operation_name=None, request_context=None,
2335
execute_serially=False, validate_ast=True):
@@ -34,7 +46,7 @@ def execute(self, schema, request='', root=None, args=None, operation_name=None,
3446
validate_ast
3547
)
3648

37-
for middleware in self.execution_middlewares:
49+
for middleware in self._execution_middlewares:
3850
if hasattr(middleware, 'execution_result'):
3951
curried_execution_function = functools.partial(middleware.execution_result, curried_execution_function)
4052

@@ -81,7 +93,10 @@ def _execute_operation(self, ctx, root, operation, execute_serially):
8193
if operation.operation == 'mutation' or execute_serially:
8294
execute_serially = True
8395

84-
fields = DefaultOrderedDict(list) if execute_serially else collections.defaultdict(list)
96+
fields = DefaultOrderedDict(list) \
97+
if (execute_serially or self._enforce_strict_ordering) \
98+
else collections.defaultdict(list)
99+
85100
fields = collect_fields(ctx, type, operation.selection_set, fields, set())
86101

87102
if execute_serially:
@@ -101,20 +116,20 @@ def collect_result(resolved_result):
101116
return results
102117

103118
if isinstance(result, Deferred):
104-
return result.add_callback(collect_result)
119+
return succeed(result).add_callback(collect_result)
105120

106121
else:
107122
return collect_result(result)
108123

109124
def execute_field(prev_deferred, response_name):
110125
return prev_deferred.add_callback(execute_field_callback, response_name)
111126

112-
return functools.reduce(execute_field, fields.keys(), succeed({}))
127+
return functools.reduce(execute_field, fields.keys(), succeed(self._map_type()))
113128

114129
def _execute_fields(self, execution_context, parent_type, source_value, fields):
115130
contains_deferred = False
116131

117-
results = {}
132+
results = self._map_type()
118133
for response_name, field_asts in fields.items():
119134
result = self._resolve_field(execution_context, parent_type, source_value, field_asts)
120135
if result is Undefined:
@@ -138,7 +153,7 @@ def _resolve_field(self, execution_context, parent_type, source, field_asts):
138153
return Undefined
139154

140155
return_type = field_def.type
141-
resolve_fn = field_def.resolver or self.default_resolve_fn
156+
resolve_fn = field_def.resolver or self._default_resolve_fn
142157

143158
# Build a dict of arguments from the field.arguments AST, using the variables scope to
144159
# fulfill any variable references.
@@ -283,7 +298,7 @@ def complete_value(self, ctx, return_type, field_asts, info, result):
283298
)
284299

285300
# Collect sub-fields to execute to complete this value.
286-
subfield_asts = collections.defaultdict(list)
301+
subfield_asts = DefaultOrderedDict(list) if self._enforce_strict_ordering else collections.defaultdict(list)
287302
visited_fragment_names = set()
288303
for field_ast in field_asts:
289304
selection_set = field_ast.selection_set
@@ -298,7 +313,7 @@ def run_resolve_fn(self, resolve_fn, source, args, info):
298313
curried_resolve_fn = functools.partial(resolve_fn, source, args, info)
299314

300315
try:
301-
for middleware in self.execution_middlewares:
316+
for middleware in self._execution_middlewares:
302317
if hasattr(middleware, 'run_resolve_fn'):
303318
curried_resolve_fn = functools.partial(middleware.run_resolve_fn, curried_resolve_fn, resolve_fn)
304319

graphql/core/pyutils/defer.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -466,16 +466,19 @@ class _ResultCollector(Deferred):
466466
objects_remaining_to_resolve = 0
467467
_result = None
468468

469-
def _schedule_callbacks(self, items, result, objects_remaining_to_resolve=None):
469+
def _schedule_callbacks(self, items, result, objects_remaining_to_resolve=None, preserve_insert_ordering=False):
470470
self.objects_remaining_to_resolve = \
471471
objects_remaining_to_resolve if objects_remaining_to_resolve is not None else len(items)
472472
self._result = result
473473
for key, value in items:
474474
if isinstance(value, Deferred):
475+
# We will place a value in place of the resolved key, so that insert order is preserved.
476+
if preserve_insert_ordering:
477+
result[key] = None
478+
475479
value.add_callbacks(self._cb_deferred, self._cb_deferred,
476480
callback_args=(key, True),
477481
errback_args=(key, False))
478-
479482
else:
480483
self.objects_remaining_to_resolve -= 1
481484
result[key] = value
@@ -509,7 +512,8 @@ class DeferredDict(_ResultCollector):
509512
def __init__(self, mapping):
510513
super(DeferredDict, self).__init__()
511514
assert isinstance(mapping, collections.Mapping)
512-
self._schedule_callbacks(mapping.items(), {})
515+
self._schedule_callbacks(mapping.items(), type(mapping)(),
516+
preserve_insert_ordering=isinstance(mapping, collections.OrderedDict))
513517

514518

515519
class DeferredList(_ResultCollector):

graphql/core/utils/build_ast_schema.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ def build_ast_schema(document, query_type_name, mutation_type_name=None):
6868

6969
def produce_type_def(type_ast):
7070
type_name = _get_inner_type_name(type_ast)
71-
print('ptd', type_name)
7271
if type_name in inner_type_map:
7372
return _build_wrapped_type(inner_type_map[type_name], type_ast)
7473

tests/core_execution/test_concurrent_executor.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import OrderedDict
12
from graphql.core.error import format_error
23
from graphql.core.execution import Executor
34
from graphql.core.execution.middlewares.sync import SynchronousExecutionMiddleware
@@ -233,3 +234,36 @@ def promise(self):
233234
assert not isinstance(result, Deferred)
234235
assert result.data == {"promise": 'I should work'}
235236
assert not result.errors
237+
238+
239+
def test_executor_can_enforce_strict_ordering():
240+
Type = GraphQLObjectType('Type', lambda: {
241+
'a': GraphQLField(GraphQLString,
242+
resolver=lambda *_: succeed('Apple')),
243+
'b': GraphQLField(GraphQLString,
244+
resolver=lambda *_: succeed('Banana')),
245+
'c': GraphQLField(GraphQLString,
246+
resolver=lambda *_: succeed('Cherry')),
247+
'deep': GraphQLField(Type, resolver=lambda *_: succeed({})),
248+
})
249+
schema = GraphQLSchema(query=Type)
250+
executor = Executor(map_type=OrderedDict)
251+
252+
query = '{ a b c aa: c cc: c bb: b aaz: a bbz: b deep { b a c deeper: deep { c a b } } ' \
253+
'ccz: c zzz: c aaa: a }'
254+
255+
def handle_results(result):
256+
assert not result.errors
257+
258+
data = result.data
259+
assert isinstance(data, OrderedDict)
260+
assert list(data.keys()) == ['a', 'b', 'c', 'aa', 'cc', 'bb', 'aaz', 'bbz', 'deep', 'ccz', 'zzz', 'aaa']
261+
deep = data['deep']
262+
assert isinstance(deep, OrderedDict)
263+
assert list(deep.keys()) == ['b', 'a', 'c', 'deeper']
264+
deeper = deep['deeper']
265+
assert isinstance(deeper, OrderedDict)
266+
assert list(deeper.keys()) == ['c', 'a', 'b']
267+
268+
raise_callback_results(executor.execute(schema, query), handle_results)
269+
raise_callback_results(executor.execute(schema, query, execute_serially=True), handle_results)

tests/core_execution/test_executor.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from collections import OrderedDict
12
import json
23
from pytest import raises
3-
from graphql.core.execution import execute
4+
from graphql.core.execution import execute, Executor
5+
from graphql.core.execution.middlewares.sync import SynchronousExecutionMiddleware
46
from graphql.core.language.parser import parse
57
from graphql.core.type import (GraphQLSchema, GraphQLObjectType, GraphQLField,
68
GraphQLArgument, GraphQLList, GraphQLInt, GraphQLString,
@@ -450,3 +452,35 @@ def test_fails_to_execute_a_query_containing_a_type_definition():
450452
result = execute(schema, None, query)
451453

452454
assert excinfo.value.message == 'GraphQL cannot execute a request containing a ObjectTypeDefinition.'
455+
456+
457+
def test_executor_can_enforce_strict_ordering():
458+
Type = GraphQLObjectType('Type', lambda: {
459+
'a': GraphQLField(GraphQLString,
460+
resolver=lambda *_: 'Apple'),
461+
'b': GraphQLField(GraphQLString,
462+
resolver=lambda *_: 'Banana'),
463+
'c': GraphQLField(GraphQLString,
464+
resolver=lambda *_: 'Cherry'),
465+
'deep': GraphQLField(Type, resolver=lambda *_: {}),
466+
})
467+
schema = GraphQLSchema(query=Type)
468+
executor = Executor(execution_middlewares=[SynchronousExecutionMiddleware], map_type=OrderedDict)
469+
query = '{ a b c aa: c cc: c bb: b aaz: a bbz: b deep { b a c deeper: deep { c a b } } ' \
470+
'ccz: c zzz: c aaa: a }'
471+
472+
def check_result(result):
473+
assert not result.errors
474+
475+
data = result.data
476+
assert isinstance(data, OrderedDict)
477+
assert list(data.keys()) == ['a', 'b', 'c', 'aa', 'cc', 'bb', 'aaz', 'bbz', 'deep', 'ccz', 'zzz', 'aaa']
478+
deep = data['deep']
479+
assert isinstance(deep, OrderedDict)
480+
assert list(deep.keys()) == ['b', 'a', 'c', 'deeper']
481+
deeper = deep['deeper']
482+
assert isinstance(deeper, OrderedDict)
483+
assert list(deeper.keys()) == ['c', 'a', 'b']
484+
485+
check_result(executor.execute(schema, query))
486+
check_result(executor.execute(schema, query, execute_serially=True))

0 commit comments

Comments
 (0)