Skip to content

Commit 39b20ea

Browse files
committed
Resolve list elements in parallel
1 parent 4765ebf commit 39b20ea

File tree

3 files changed

+69
-30
lines changed

3 files changed

+69
-30
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ a query language for APIs created by Facebook.
1212
[![Python 3 Status](https://pyup.io/repos/github/graphql-python/graphql-core-next/python-3-shield.svg)](https://pyup.io/repos/github/graphql-python/graphql-core-next/)
1313

1414
The current version 1.0.1 of GraphQL-core-next is up-to-date with GraphQL.js version
15-
14.0.2. All parts of the API are covered by an extensive test suite of currently 1616
15+
14.0.2. All parts of the API are covered by an extensive test suite of currently 1617
1616
unit tests.
1717

1818

graphql/execution/execute.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,8 @@ def execute_fields(
425425
for "read" mode.
426426
"""
427427
results = {}
428-
awaitable_fields = []
428+
awaitable_fields: List[str] = []
429+
append_awaitable = awaitable_fields.append
429430
for response_name, field_nodes in fields.items():
430431
field_path = add_path(path, response_name)
431432
result = self.resolve_field(
@@ -434,7 +435,7 @@ def execute_fields(
434435
if result is not INVALID:
435436
results[response_name] = result
436437
if isawaitable(result):
437-
awaitable_fields.append(response_name)
438+
append_awaitable(response_name)
438439

439440
# If there are no coroutines, we can just return the object
440441
if not awaitable_fields:
@@ -805,9 +806,10 @@ def complete_list_value(
805806
# where the list contains no coroutine objects by avoiding creating
806807
# another coroutine object.
807808
item_type = return_type.of_type
808-
is_async = False
809+
awaitable_indices: List[int] = []
810+
append_awaitable = awaitable_indices.append
809811
completed_results: List[Any] = []
810-
append = completed_results.append
812+
append_result = completed_results.append
811813
for index, item in enumerate(result):
812814
# No need to modify the info object containing the path,
813815
# since from here on it is not ever accessed by resolver functions.
@@ -816,20 +818,25 @@ def complete_list_value(
816818
item_type, field_nodes, info, field_path, item
817819
)
818820

819-
if not is_async and isawaitable(completed_item):
820-
is_async = True
821-
append(completed_item)
821+
if isawaitable(completed_item):
822+
append_awaitable(index)
823+
append_result(completed_item)
822824

823-
if is_async:
825+
if not awaitable_indices:
826+
return completed_results
824827

825-
async def get_completed_results():
826-
return [
827-
await value if isawaitable(value) else value
828-
for value in completed_results
829-
]
830-
831-
return get_completed_results()
832-
return completed_results
828+
# noinspection PyShadowingNames
829+
async def get_completed_results():
830+
for index, result in zip(
831+
awaitable_indices,
832+
await gather(
833+
*(completed_results[index] for index in awaitable_indices)
834+
),
835+
):
836+
completed_results[index] = result
837+
return completed_results
838+
839+
return get_completed_results()
833840

834841
@staticmethod
835842
def complete_leaf_value(return_type: GraphQLLeafType, result: Any) -> Any:

tests/execution/test_executor.py

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,8 @@ def executes_ignoring_invalid_non_executable_definitions():
831831

832832
assert execute(schema, query) == ({"foo": None}, None)
833833

834+
835+
def describe_customize_execution():
834836
def uses_a_custom_field_resolver():
835837
query = parse("{ foo }")
836838

@@ -867,29 +869,31 @@ def resolve_field(self, parent_type, source, field_nodes, path):
867869
None,
868870
)
869871

870-
@mark.asyncio
871-
async def resolve_fields_in_parallel():
872-
class Barrier:
873-
"""Barrier that makes progress only after a certain number of waits."""
874872

875-
def __init__(self, number):
876-
self.event = asyncio.Event()
877-
self.number = number
873+
def describe_parallel_execution():
874+
class Barrier:
875+
"""Barrier that makes progress only after a certain number of waits."""
876+
877+
def __init__(self, number: int) -> None:
878+
self.event = asyncio.Event()
879+
self.number = number
878880

879-
async def wait(self):
880-
self.number -= 1
881-
if not self.number:
882-
self.event.set()
883-
return await self.event.wait()
881+
async def wait(self) -> bool:
882+
self.number -= 1
883+
if not self.number:
884+
self.event.set()
885+
return await self.event.wait()
884886

887+
@mark.asyncio
888+
async def resolve_fields_in_parallel():
885889
barrier = Barrier(2)
886890

887891
async def resolve(*_args):
888892
return await barrier.wait()
889893

890894
schema = GraphQLSchema(
891895
GraphQLObjectType(
892-
"Object",
896+
"Query",
893897
{
894898
"foo": GraphQLField(GraphQLBoolean, resolve=resolve),
895899
"bar": GraphQLField(GraphQLBoolean, resolve=resolve),
@@ -898,6 +902,34 @@ async def resolve(*_args):
898902
)
899903

900904
ast = parse("{foo, bar}")
905+
# raises TimeoutError if not parallel
901906
result = await asyncio.wait_for(execute(schema, ast), 1.0)
902907

903908
assert result == ({"foo": True, "bar": True}, None)
909+
910+
@mark.asyncio
911+
async def resolve_list_in_parallel():
912+
barrier = Barrier(2)
913+
914+
async def resolve(*_args):
915+
return await barrier.wait()
916+
917+
async def resolve_list(*args):
918+
return [resolve(*args), resolve(*args)]
919+
920+
schema = GraphQLSchema(
921+
GraphQLObjectType(
922+
"Query",
923+
{
924+
"foo": GraphQLField(
925+
GraphQLList(GraphQLBoolean), resolve=resolve_list
926+
)
927+
},
928+
)
929+
)
930+
931+
ast = parse("{foo}")
932+
# raises TimeoutError if not parallel
933+
result = await asyncio.wait_for(execute(schema, ast), 1.0)
934+
935+
assert result == ({"foo": [True, True]}, None)

0 commit comments

Comments
 (0)