Skip to content

Commit 5db7a7a

Browse files
committed
Improve performance using faster is_awaitable (#54)
1 parent 9feec00 commit 5db7a7a

File tree

5 files changed

+143
-91
lines changed

5 files changed

+143
-91
lines changed

src/graphql/execution/execute.py

+90-75
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from asyncio import gather
2-
from inspect import isawaitable
32
from typing import (
43
Any,
54
Awaitable,
5+
Callable,
66
Dict,
77
Iterable,
88
List,
@@ -28,6 +28,7 @@
2828
)
2929
from ..pyutils import (
3030
inspect,
31+
is_awaitable as default_is_awaitable,
3132
AwaitableOrValue,
3233
FrozenList,
3334
Path,
@@ -110,64 +111,6 @@ class ExecutionResult(NamedTuple):
110111
Middleware = Optional[Union[Tuple, List, MiddlewareManager]]
111112

112113

113-
def execute(
114-
schema: GraphQLSchema,
115-
document: DocumentNode,
116-
root_value: Any = None,
117-
context_value: Any = None,
118-
variable_values: Optional[Dict[str, Any]] = None,
119-
operation_name: Optional[str] = None,
120-
field_resolver: Optional[GraphQLFieldResolver] = None,
121-
type_resolver: Optional[GraphQLTypeResolver] = None,
122-
middleware: Optional[Middleware] = None,
123-
execution_context_class: Optional[Type["ExecutionContext"]] = None,
124-
) -> AwaitableOrValue[ExecutionResult]:
125-
"""Execute a GraphQL operation.
126-
127-
Implements the "Evaluating requests" section of the GraphQL specification.
128-
129-
Returns an ExecutionResult (if all encountered resolvers are synchronous),
130-
or a coroutine object eventually yielding an ExecutionResult.
131-
132-
If the arguments to this function do not result in a legal execution context,
133-
a GraphQLError will be thrown immediately explaining the invalid input.
134-
"""
135-
# If arguments are missing or incorrect, throw an error.
136-
assert_valid_execution_arguments(schema, document, variable_values)
137-
138-
if execution_context_class is None:
139-
execution_context_class = ExecutionContext
140-
141-
# If a valid execution context cannot be created due to incorrect arguments,
142-
# a "Response" with only errors is returned.
143-
exe_context = execution_context_class.build(
144-
schema,
145-
document,
146-
root_value,
147-
context_value,
148-
variable_values,
149-
operation_name,
150-
field_resolver,
151-
type_resolver,
152-
middleware,
153-
)
154-
155-
# Return early errors if execution context failed.
156-
if isinstance(exe_context, list):
157-
return ExecutionResult(data=None, errors=exe_context)
158-
159-
# Return a possible coroutine object that will eventually yield the data described
160-
# by the "Response" section of the GraphQL specification.
161-
#
162-
# If errors are encountered while executing a GraphQL field, only that field and
163-
# its descendants will be omitted, and sibling fields will still be executed. An
164-
# execution which encounters errors will still result in a coroutine object that
165-
# can be executed without errors.
166-
167-
data = exe_context.execute_operation(exe_context.operation, root_value)
168-
return exe_context.build_response(data)
169-
170-
171114
class ExecutionContext:
172115
"""Data that must be available at all points during query execution.
173116
@@ -186,6 +129,8 @@ class ExecutionContext:
186129
errors: List[GraphQLError]
187130
middleware_manager: Optional[MiddlewareManager]
188131

132+
is_awaitable = staticmethod(default_is_awaitable)
133+
189134
def __init__(
190135
self,
191136
schema: GraphQLSchema,
@@ -198,6 +143,7 @@ def __init__(
198143
type_resolver: GraphQLTypeResolver,
199144
errors: List[GraphQLError],
200145
middleware_manager: Optional[MiddlewareManager],
146+
is_awaitable: Optional[Callable[[Any], bool]],
201147
) -> None:
202148
self.schema = schema
203149
self.fragments = fragments
@@ -209,6 +155,8 @@ def __init__(
209155
self.type_resolver = type_resolver # type: ignore
210156
self.errors = errors
211157
self.middleware_manager = middleware_manager
158+
if is_awaitable:
159+
self.is_awaitable = is_awaitable
212160
self._subfields_cache: Dict[
213161
Tuple[GraphQLObjectType, int], Dict[str, List[FieldNode]]
214162
] = {}
@@ -225,6 +173,7 @@ def build(
225173
field_resolver: Optional[GraphQLFieldResolver] = None,
226174
type_resolver: Optional[GraphQLTypeResolver] = None,
227175
middleware: Optional[Middleware] = None,
176+
is_awaitable: Optional[Callable[[Any], bool]] = None,
228177
) -> Union[List[GraphQLError], "ExecutionContext"]:
229178
"""Build an execution context
230179
@@ -292,6 +241,7 @@ def build(
292241
type_resolver or default_type_resolver,
293242
[],
294243
middleware_manager,
244+
is_awaitable,
295245
)
296246

297247
def build_response(
@@ -302,7 +252,7 @@ def build_response(
302252
Given a completed execution context and data, build the (data, errors) response
303253
defined by the "Response" section of the GraphQL spec.
304254
"""
305-
if isawaitable(data):
255+
if self.is_awaitable(data):
306256

307257
async def build_response_async():
308258
return self.build_response(await data) # type: ignore
@@ -346,7 +296,7 @@ def execute_operation(
346296
self.errors.append(error)
347297
return None
348298
else:
349-
if isawaitable(result):
299+
if self.is_awaitable(result):
350300
# noinspection PyShadowingNames
351301
async def await_result():
352302
try:
@@ -369,27 +319,28 @@ def execute_fields_serially(
369319
Implements the "Evaluating selection sets" section of the spec for "write" mode.
370320
"""
371321
results: Dict[str, Any] = {}
322+
is_awaitable = self.is_awaitable
372323
for response_name, field_nodes in fields.items():
373324
field_path = Path(path, response_name)
374325
result = self.resolve_field(
375326
parent_type, source_value, field_nodes, field_path
376327
)
377328
if result is Undefined:
378329
continue
379-
if isawaitable(results):
330+
if is_awaitable(results):
380331
# noinspection PyShadowingNames
381332
async def await_and_set_result(results, response_name, result):
382333
awaited_results = await results
383334
awaited_results[response_name] = (
384-
await result if isawaitable(result) else result
335+
await result if is_awaitable(result) else result
385336
)
386337
return awaited_results
387338

388339
# noinspection PyTypeChecker
389340
results = await_and_set_result(
390341
cast(Awaitable, results), response_name, result
391342
)
392-
elif isawaitable(result):
343+
elif is_awaitable(result):
393344
# noinspection PyShadowingNames
394345
async def set_result(results, response_name, result):
395346
results[response_name] = await result
@@ -399,7 +350,7 @@ async def set_result(results, response_name, result):
399350
results = set_result(results, response_name, result)
400351
else:
401352
results[response_name] = result
402-
if isawaitable(results):
353+
if is_awaitable(results):
403354
# noinspection PyShadowingNames
404355
async def get_results():
405356
return await cast(Awaitable, results)
@@ -419,6 +370,7 @@ def execute_fields(
419370
Implements the "Evaluating selection sets" section of the spec for "read" mode.
420371
"""
421372
results = {}
373+
is_awaitable = self.is_awaitable
422374
awaitable_fields: List[str] = []
423375
append_awaitable = awaitable_fields.append
424376
for response_name, field_nodes in fields.items():
@@ -428,7 +380,7 @@ def execute_fields(
428380
)
429381
if result is not Undefined:
430382
results[response_name] = result
431-
if isawaitable(result):
383+
if is_awaitable(result):
432384
append_awaitable(response_name)
433385

434386
# If there are no coroutines, we can just return the object
@@ -564,6 +516,7 @@ def build_resolve_info(
564516
self.operation,
565517
self.variable_values,
566518
self.context_value,
519+
self.is_awaitable,
567520
)
568521

569522
def resolve_field(
@@ -626,7 +579,7 @@ def resolve_field_value_or_error(
626579
# Note that contrary to the JavaScript implementation, we pass the context
627580
# value as part of the resolve info.
628581
result = resolve_fn(source, info, **args)
629-
if isawaitable(result):
582+
if self.is_awaitable(result):
630583
# noinspection PyShadowingNames
631584
async def await_result():
632585
try:
@@ -657,13 +610,13 @@ def complete_value_catching_error(
657610
the execution context.
658611
"""
659612
try:
660-
if isawaitable(result):
613+
if self.is_awaitable(result):
661614

662615
async def await_result():
663616
value = self.complete_value(
664617
return_type, field_nodes, info, path, await result
665618
)
666-
if isawaitable(value):
619+
if self.is_awaitable(value):
667620
return await value
668621
return value
669622

@@ -672,7 +625,7 @@ async def await_result():
672625
completed = self.complete_value(
673626
return_type, field_nodes, info, path, result
674627
)
675-
if isawaitable(completed):
628+
if self.is_awaitable(completed):
676629
# noinspection PyShadowingNames
677630
async def await_completed():
678631
try:
@@ -810,6 +763,7 @@ def complete_list_value(
810763
# the list contains no coroutine objects by avoiding creating another coroutine
811764
# object.
812765
item_type = return_type.of_type
766+
is_awaitable = self.is_awaitable
813767
awaitable_indices: List[int] = []
814768
append_awaitable = awaitable_indices.append
815769
completed_results: List[Any] = []
@@ -822,7 +776,7 @@ def complete_list_value(
822776
item_type, field_nodes, info, field_path, item
823777
)
824778

825-
if isawaitable(completed_item):
779+
if is_awaitable(completed_item):
826780
append_awaitable(index)
827781
append_result(completed_item)
828782

@@ -873,7 +827,7 @@ def complete_abstract_value(
873827
resolve_type_fn = return_type.resolve_type or self.type_resolver
874828
runtime_type = resolve_type_fn(result, info, return_type) # type: ignore
875829

876-
if isawaitable(runtime_type):
830+
if self.is_awaitable(runtime_type):
877831

878832
async def await_complete_object_value():
879833
value = self.complete_object_value(
@@ -889,7 +843,7 @@ async def await_complete_object_value():
889843
path,
890844
result,
891845
)
892-
if isawaitable(value):
846+
if self.is_awaitable(value):
893847
return await value # type: ignore
894848
return value
895849

@@ -957,7 +911,7 @@ def complete_object_value(
957911
if return_type.is_type_of:
958912
is_type_of = return_type.is_type_of(result, info)
959913

960-
if isawaitable(is_type_of):
914+
if self.is_awaitable(is_type_of):
961915

962916
async def collect_and_execute_subfields_async():
963917
if not await is_type_of: # type: ignore
@@ -1018,6 +972,66 @@ def collect_subfields(
1018972
return sub_field_nodes
1019973

1020974

975+
def execute(
976+
schema: GraphQLSchema,
977+
document: DocumentNode,
978+
root_value: Any = None,
979+
context_value: Any = None,
980+
variable_values: Optional[Dict[str, Any]] = None,
981+
operation_name: Optional[str] = None,
982+
field_resolver: Optional[GraphQLFieldResolver] = None,
983+
type_resolver: Optional[GraphQLTypeResolver] = None,
984+
middleware: Optional[Middleware] = None,
985+
execution_context_class: Optional[Type["ExecutionContext"]] = None,
986+
is_awaitable: Optional[Callable[[Any], bool]] = None,
987+
) -> AwaitableOrValue[ExecutionResult]:
988+
"""Execute a GraphQL operation.
989+
990+
Implements the "Evaluating requests" section of the GraphQL specification.
991+
992+
Returns an ExecutionResult (if all encountered resolvers are synchronous),
993+
or a coroutine object eventually yielding an ExecutionResult.
994+
995+
If the arguments to this function do not result in a legal execution context,
996+
a GraphQLError will be thrown immediately explaining the invalid input.
997+
"""
998+
# If arguments are missing or incorrect, throw an error.
999+
assert_valid_execution_arguments(schema, document, variable_values)
1000+
1001+
if execution_context_class is None:
1002+
execution_context_class = ExecutionContext
1003+
1004+
# If a valid execution context cannot be created due to incorrect arguments,
1005+
# a "Response" with only errors is returned.
1006+
exe_context = execution_context_class.build(
1007+
schema,
1008+
document,
1009+
root_value,
1010+
context_value,
1011+
variable_values,
1012+
operation_name,
1013+
field_resolver,
1014+
type_resolver,
1015+
middleware,
1016+
is_awaitable,
1017+
)
1018+
1019+
# Return early errors if execution context failed.
1020+
if isinstance(exe_context, list):
1021+
return ExecutionResult(data=None, errors=exe_context)
1022+
1023+
# Return a possible coroutine object that will eventually yield the data described
1024+
# by the "Response" section of the GraphQL specification.
1025+
#
1026+
# If errors are encountered while executing a GraphQL field, only that field and
1027+
# its descendants will be omitted, and sibling fields will still be executed. An
1028+
# execution which encounters errors will still result in a coroutine object that
1029+
# can be executed without errors.
1030+
1031+
data = exe_context.execute_operation(exe_context.operation, root_value)
1032+
return exe_context.build_response(data)
1033+
1034+
10211035
def assert_valid_execution_arguments(
10221036
schema: GraphQLSchema,
10231037
document: DocumentNode,
@@ -1116,6 +1130,7 @@ def default_type_resolver(
11161130

11171131
# Otherwise, test each possible type.
11181132
possible_types = info.schema.get_possible_types(abstract_type)
1133+
is_awaitable = info.is_awaitable
11191134
awaitable_is_type_of_results: List[Awaitable] = []
11201135
append_awaitable_results = awaitable_is_type_of_results.append
11211136
awaitable_types: List[GraphQLObjectType] = []
@@ -1125,7 +1140,7 @@ def default_type_resolver(
11251140
if type_.is_type_of:
11261141
is_type_of_result = type_.is_type_of(value, info)
11271142

1128-
if isawaitable(is_type_of_result):
1143+
if is_awaitable(is_type_of_result):
11291144
append_awaitable_results(cast(Awaitable, is_type_of_result))
11301145
append_awaitable_types(type_)
11311146
elif is_type_of_result:

0 commit comments

Comments
 (0)