Skip to content

Commit 420bbe5

Browse files
committed
Switch out regular executor for parallel executor running w/ synchronous middleware.
1 parent 208b0a1 commit 420bbe5

File tree

8 files changed

+39
-203
lines changed

8 files changed

+39
-203
lines changed

graphql/core/defer.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,7 @@
6464
from graphql.core.compat import PY3
6565

6666
__all__ = ("Deferred", "AlreadyCalledDeferred", "DeferredException",
67-
"defer", "inline_callbacks", "return_value")
68-
69-
70-
class _DefGen_Return(BaseException):
71-
"""Exception to return a result from an inline callback."""
72-
73-
def __init__(self, value):
74-
self.value = value
67+
"defer", "succeed", "fail", "DeferredDict", "DeferredList")
7568

7669

7770
class AlreadyCalledDeferred(Exception):

graphql/core/error.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .language.location import get_location
2+
from .defer import DeferredException
23

34

45
class Error(Exception):
@@ -38,6 +39,9 @@ def locations(self):
3839

3940

4041
def format_error(error):
42+
if isinstance(error, DeferredException):
43+
error = error.value
44+
4145
return {
4246
'message': error.message,
4347
'locations': error.locations,

graphql/core/execution/__init__.py

Lines changed: 10 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
# -*- coding: utf-8 -*-
2-
import collections
3-
from ..error import GraphQLError, format_error
2+
from ..error import GraphQLError
43
from ..language import ast
54
from ..type.definition import (
6-
GraphQLEnumType,
75
GraphQLInterfaceType,
8-
GraphQLList,
9-
GraphQLNonNull,
10-
GraphQLObjectType,
11-
GraphQLScalarType,
126
GraphQLUnionType,
137
)
148
from ..type.directives import (
@@ -20,12 +14,11 @@
2014
TypeMetaFieldDef,
2115
TypeNameMetaFieldDef,
2216
)
23-
from ..utils import is_nullish, type_from_ast
17+
from ..utils import type_from_ast
2418
from .values import get_argument_values, get_variable_values
2519

2620
Undefined = object()
2721

28-
2922
"""
3023
Terminology
3124
@@ -52,6 +45,7 @@ class ExecutionContext(object):
5245
5346
Namely, schema of the type system that is currently executing,
5447
and the fragments defined in the query document"""
48+
5549
def __init__(self, schema, root, document_ast, operation_name, args):
5650
"""Constructs a ExecutionContext object from the arguments passed
5751
to execute, which we will pass throughout the other execution
@@ -96,27 +90,13 @@ def __init__(self, data, errors=None):
9690

9791

9892
def execute(schema, root, ast, operation_name='', args=None):
99-
"""Implements the "Evaluating requests" section of the spec."""
100-
assert schema, 'Must provide schema'
101-
ctx = ExecutionContext(schema, root, ast, operation_name, args)
102-
try:
103-
data = execute_operation(ctx, root, ctx.operation)
104-
except Exception as e:
105-
ctx.errors.append(e)
106-
data = None
107-
if not ctx.errors:
108-
return ExecutionResult(data)
109-
formatted_errors = list(map(format_error, ctx.errors))
110-
return ExecutionResult(data, formatted_errors)
111-
112-
113-
def execute_operation(ctx, root, operation):
114-
"""Implements the "Evaluating operations" section of the spec."""
115-
type = get_operation_root_type(ctx.schema, operation)
116-
fields = collect_fields(ctx, type, operation.selection_set, {}, set())
117-
if operation.operation == 'mutation':
118-
return execute_fields_serially(ctx, type, root, fields)
119-
return execute_fields(ctx, type, root, fields)
93+
"""
94+
Executes an AST synchronously. Assumes that the AST is already validated.
95+
"""
96+
from .parallel_execution import Executor
97+
from .middlewares import SynchronousExecutionMiddleware
98+
e = Executor(schema, [SynchronousExecutionMiddleware.SynchronousExecutionMiddleware()])
99+
return e.execute(ast, root, args, operation_name, validate_ast=False)
120100

121101

122102
def get_operation_root_type(schema, operation):
@@ -137,24 +117,6 @@ def get_operation_root_type(schema, operation):
137117
)
138118

139119

140-
def execute_fields_serially(ctx, parent_type, source, fields):
141-
"""Implements the "Evaluating selection sets" section of the spec
142-
for "write" mode."""
143-
results = {}
144-
for response_name, field_asts in fields.items():
145-
result = resolve_field(ctx, parent_type, source, field_asts)
146-
if result is not Undefined:
147-
results[response_name] = result
148-
return results
149-
150-
151-
def execute_fields(ctx, parent_type, source, fields):
152-
"""Implements the "Evaluating selection sets" section of the spec
153-
for "read" mode."""
154-
# FIXME: just fallback to serial execution for now.
155-
return execute_fields_serially(ctx, parent_type, source, fields)
156-
157-
158120
def collect_fields(ctx, type, selection_set, fields, prev_fragment_names):
159121
for selection in selection_set.selections:
160122
directives = selection.directives
@@ -268,136 +230,6 @@ def variable_values(self):
268230
return self.context.variables
269231

270232

271-
def resolve_field(ctx, parent_type, source, field_asts):
272-
"""A wrapper function for resolving the field, that catches the error
273-
and adds it to the context's global if the error is not rethrowable."""
274-
field_ast = field_asts[0]
275-
field_name = field_ast.name.value
276-
277-
field_def = get_field_def(ctx.schema, parent_type, field_name)
278-
if not field_def:
279-
return Undefined
280-
281-
return_type = field_def.type
282-
resolve_fn = field_def.resolver or default_resolve_fn
283-
284-
# Build a dict of arguments from the field.arguments AST, using the variables scope to fulfill any variable references.
285-
# TODO: find a way to memoize, in case this field is within a list type.
286-
args = get_argument_values(
287-
field_def.args, field_ast.arguments, ctx.variables
288-
)
289-
290-
# The resolve function's optional third argument is a collection of
291-
# information about the current execution state.
292-
info = ResolveInfo(
293-
field_name,
294-
field_asts,
295-
return_type,
296-
parent_type,
297-
ctx
298-
)
299-
300-
# If an error occurs while calling the field `resolve` function, ensure that it is wrapped as a GraphQLError with locations.
301-
# Log this error and return null if allowed, otherwise throw the error so the parent field can handle it.
302-
try:
303-
result = resolve_fn(source, args, info)
304-
except Exception as e:
305-
reported_error = GraphQLError(str(e), [field_ast], e)
306-
if isinstance(return_type, GraphQLNonNull):
307-
raise reported_error
308-
ctx.errors.append(reported_error)
309-
return None
310-
311-
return complete_value_catching_error(
312-
ctx, return_type, field_asts, info, result
313-
)
314-
315-
316-
def complete_value_catching_error(ctx, return_type, field_asts, info, result):
317-
# If the field type is non-nullable, then it is resolved without any
318-
# protection from errors.
319-
if isinstance(return_type, GraphQLNonNull):
320-
return complete_value(ctx, return_type, field_asts, info, result)
321-
322-
# Otherwise, error protection is applied, logging the error and
323-
# resolving a null value for this field if one is encountered.
324-
try:
325-
return complete_value(ctx, return_type, field_asts, info, result)
326-
except Exception as e:
327-
ctx.errors.append(e)
328-
return None
329-
330-
331-
def complete_value(ctx, return_type, field_asts, info, result):
332-
"""Implements the instructions for completeValue as defined in the
333-
"Field entries" section of the spec.
334-
335-
If the field type is Non-Null, then this recursively completes the value for the inner type. It throws a field error
336-
if that completion returns null, as per the "Nullability" section of the spec.
337-
338-
If the field type is a List, then this recursively completes the value for the inner type on each item in the list.
339-
340-
If the field type is a Scalar or Enum, ensures the completed value is a legal value of the type by calling the `serialize`
341-
method of GraphQL type definition.
342-
343-
Otherwise, the field type expects a sub-selection set, and will complete the value by evaluating all sub-selections."""
344-
# If field type is NonNull, complete for inner type, and throw field error if result is null.
345-
if isinstance(return_type, GraphQLNonNull):
346-
completed = complete_value(
347-
ctx, return_type.of_type, field_asts, info, result
348-
)
349-
if completed is None:
350-
raise GraphQLError(
351-
'Cannot return null for non-nullable type.',
352-
field_asts
353-
)
354-
return completed
355-
356-
# If result is null-like, return null.
357-
if is_nullish(result):
358-
return None
359-
360-
# If field type is List, complete each item in the list with the inner type
361-
if isinstance(return_type, GraphQLList):
362-
assert isinstance(result, collections.Iterable), \
363-
'User Error: expected iterable, but did not find one.'
364-
365-
item_type = return_type.of_type
366-
return [complete_value_catching_error(
367-
ctx, item_type, field_asts, info, item
368-
) for item in result]
369-
370-
# If field type is Scalar or Enum, serialize to a valid value, returning null if coercion is not possible.
371-
if isinstance(return_type, (GraphQLScalarType, GraphQLEnumType)):
372-
serialized_result = return_type.serialize(result)
373-
if is_nullish(serialized_result):
374-
return None
375-
return serialized_result
376-
377-
# Field type must be Object, Interface or Union and expect sub-selections.
378-
if isinstance(return_type, GraphQLObjectType):
379-
object_type = return_type
380-
elif isinstance(return_type, (GraphQLInterfaceType, GraphQLUnionType)):
381-
object_type = return_type.resolve_type(result)
382-
else:
383-
object_type = None
384-
385-
if not object_type:
386-
return None
387-
388-
# Collect sub-fields to execute to complete this value.
389-
subfield_asts = {}
390-
visited_fragment_names = set()
391-
for field_ast in field_asts:
392-
selection_set = field_ast.selection_set
393-
if selection_set:
394-
subfield_asts = collect_fields(
395-
ctx, object_type, selection_set,
396-
subfield_asts, visited_fragment_names)
397-
398-
return execute_fields(ctx, object_type, result, subfield_asts)
399-
400-
401233
def default_resolve_fn(source, args, info):
402234
"""If a resolve function is not given, then a default resolve behavior is used which takes the property of the source object
403235
of the same name as the field and returns it as the result, or if it's a function, returns the result of calling that function."""

graphql/core/execution/middlewares/AsyncioExecutionMiddleware.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# flake8: noqa
12
from asyncio import Future, coroutine, iscoroutine, ensure_future
23
from graphql.core.defer import Deferred
34

graphql/core/execution/parallel_execution.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,33 @@
99
from graphql.core.type import GraphQLEnumType, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, \
1010
GraphQLScalarType, GraphQLUnionType
1111
from graphql.core.utils import is_nullish
12+
from graphql.core.language import ast
1213

1314

1415
class Executor(object):
1516
def __init__(self, schema, execution_middlewares=None):
1617
self.execution_middlewares = execution_middlewares or []
1718
self.schema = schema
1819

19-
def execute(self, request='', root=None, args=None, operation_name=None, execute_serially=False):
20-
if not isinstance(request, Source):
21-
request = Source(request, 'GraphQL request')
20+
def execute(self, request='', root=None, args=None, operation_name=None, execute_serially=False, validate_ast=True):
21+
if not isinstance(request, ast.Document):
22+
if not isinstance(request, Source):
23+
request = Source(request, 'GraphQL request')
2224

23-
ast = parse(request)
24-
validation_errors = validate(self.schema, ast)
25-
if validation_errors:
26-
return ExecutionResult(
27-
data=None,
28-
errors=validation_errors,
29-
)
25+
request = parse(request)
26+
27+
if validate_ast:
28+
validation_errors = validate(self.schema, request)
29+
if validation_errors:
30+
return ExecutionResult(
31+
data=None,
32+
errors=validation_errors,
33+
)
3034

3135
curried_execution_function = functools.partial(
3236
self._execute_graphql_query,
3337
root or object(),
34-
ast,
38+
request,
3539
operation_name,
3640
args or {},
3741
execute_serially)

tests/core_execution/test_lists.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def test_non_nullable_list_of_nullables():
5050
# Returns null
5151
result = run(type, None)
5252
assert len(result.errors) == 1
53-
assert result.errors[0]['message'] == 'Cannot return null for non-nullable type.'
53+
assert result.errors[0]['message'] == 'Cannot return null for non-nullable field DataType.test.'
5454
# TODO: check error location
5555
assert result.data == {'nest': None}
5656

@@ -62,7 +62,7 @@ def test_nullable_list_of_non_nullables():
6262
# Contains null
6363
result = run(type, [1, None, 2])
6464
assert len(result.errors) == 1
65-
assert result.errors[0]['message'] == 'Cannot return null for non-nullable type.'
65+
assert result.errors[0]['message'] == 'Cannot return null for non-nullable field DataType.test.'
6666
# TODO: check error location
6767
assert result.data == {'nest': {'test': None}}
6868
# Returns null
@@ -76,12 +76,12 @@ def test_non_nullable_list_of_non_nullables():
7676
# Contains null
7777
result = run(type, [1, None, 2])
7878
assert len(result.errors) == 1
79-
assert result.errors[0]['message'] == 'Cannot return null for non-nullable type.'
79+
assert result.errors[0]['message'] == 'Cannot return null for non-nullable field DataType.test.'
8080
# TODO: check error location
8181
assert result.data == {'nest': None}
8282
# Returns null
8383
result = run(type, None)
8484
assert len(result.errors) == 1
85-
assert result.errors[0]['message'] == 'Cannot return null for non-nullable type.'
85+
assert result.errors[0]['message'] == 'Cannot return null for non-nullable field DataType.test.'
8686
# TODO: check error location
8787
assert result.data == {'nest': None}

tests/core_execution/test_nonnull.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def test_nulls_a_sync_returned_object_that_contains_a_non_nullable_field_that_re
149149
result = execute(schema, NullingData(), ast, 'Q', {})
150150
assert len(result.errors) == 1
151151
# TODO: check error location
152-
assert result.errors[0]['message'] == 'Cannot return null for non-nullable type.'
152+
assert result.errors[0]['message'] == 'Cannot return null for non-nullable field DataType.nonNullSync.'
153153
assert result.data == {
154154
'nest': None
155155
}
@@ -218,4 +218,4 @@ def test_nulls_the_top_level_if_sync_non_nullable_field_returns_null():
218218
assert result.data is None
219219
assert len(result.errors) == 1
220220
# TODO: check error location
221-
assert result.errors[0]['message'] == 'Cannot return null for non-nullable type.'
221+
assert result.errors[0]['message'] == 'Cannot return null for non-nullable field DataType.nonNullSync.'

tests_py35/core_execution/test_asyncio_executor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# flake8: noqa
2+
13
from graphql.core.execution.parallel_execution import Executor
24
from graphql.core.execution.middlewares.AsyncioExecutionMiddleware import AsyncioExecutionMiddleware
35
from graphql.core.type import (

0 commit comments

Comments
 (0)