Skip to content

Commit 1062cbb

Browse files
committed
Optimize parallel execution when it's only one task
See discussion in issue #190. Also, run coverage in Python 3.10, since running in Python 3.11 reports false negatives in test_lists (probably bug in coverage).
1 parent 7fd3ce4 commit 1062cbb

File tree

5 files changed

+83
-18
lines changed

5 files changed

+83
-18
lines changed

src/graphql/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@
313313
# Validate GraphQL schema.
314314
validate_schema,
315315
assert_valid_schema,
316-
# Uphold the spec rules about naming
316+
# Uphold the spec rules about naming
317317
assert_name,
318318
assert_enum_value_name,
319319
# Types

src/graphql/execution/execute.py

+25-15
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ def execute_fields(
441441
if is_awaitable(result):
442442
append_awaitable(response_name)
443443

444-
# If there are no coroutines, we can just return the object
444+
# If there are no coroutines, we can just return the object.
445445
if not awaitable_fields:
446446
return results
447447

@@ -450,12 +450,17 @@ def execute_fields(
450450
# will yield this same map, but with any coroutines awaited in parallel and
451451
# replaced with the values they yielded.
452452
async def get_results() -> Dict[str, Any]:
453-
results.update(
454-
zip(
455-
awaitable_fields,
456-
await gather(*(results[field] for field in awaitable_fields)),
453+
if len(awaitable_fields) == 1:
454+
# If there is only one field, avoid the overhead of parallelization.
455+
field = awaitable_fields[0]
456+
results[field] = await results[field]
457+
else:
458+
results.update(
459+
zip(
460+
awaitable_fields,
461+
await gather(*(results[field] for field in awaitable_fields)),
462+
)
457463
)
458-
)
459464
return results
460465

461466
return get_results()
@@ -758,13 +763,18 @@ async def await_completed(item: Any, item_path: Path) -> Any:
758763

759764
# noinspection PyShadowingNames
760765
async def get_completed_results() -> List[Any]:
761-
for index, result in zip(
762-
awaitable_indices,
763-
await gather(
764-
*(completed_results[index] for index in awaitable_indices)
765-
),
766-
):
767-
completed_results[index] = result
766+
if len(awaitable_indices) == 1:
767+
# If there is only one index, avoid the overhead of parallelization.
768+
index = awaitable_indices[0]
769+
completed_results[index] = await completed_results[index]
770+
else:
771+
for index, result in zip(
772+
awaitable_indices,
773+
await gather(
774+
*(completed_results[index] for index in awaitable_indices)
775+
),
776+
):
777+
completed_results[index] = result
768778
return completed_results
769779

770780
return get_completed_results()
@@ -907,7 +917,7 @@ def complete_object_value(
907917

908918
# If there is an `is_type_of()` predicate function, call it with the current
909919
# result. If `is_type_of()` returns False, then raise an error rather than
910-
# continuing execution.
920+
# continuing execution.
911921
if return_type.is_type_of:
912922
is_type_of = return_type.is_type_of(result, info)
913923

@@ -943,7 +953,7 @@ def collect_subfields(
943953
# We cannot use the field_nodes themselves as key for the cache, since they
944954
# are not hashable as a list. We also do not want to use the field_nodes
945955
# themselves (converted to a tuple) as keys, since hashing them is slow.
946-
# Therefore we use the ids of the field_nodes as keys. Note that we do not
956+
# Therefore, we use the ids of the field_nodes as keys. Note that we do not
947957
# use the id of the list, since we want to hit the cache for all lists of
948958
# the same nodes, not only for the same list of nodes. Also, the list id may
949959
# even be reused, in which case we would get wrong results from the cache.

tests/execution/test_lists.py

+15
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@ def _complete(list_field):
2525
Data(list_field),
2626
)
2727

28+
def accepts_a_list_as_a_list_value():
29+
result = _complete([])
30+
assert result == ({"listField": []}, None)
31+
list_field = ["just an apple"]
32+
result = _complete(list_field)
33+
assert result == ({"listField": list_field}, None)
34+
list_field = ["apple", "banana", "coconut"]
35+
result = _complete(list_field)
36+
assert result == ({"listField": list_field}, None)
37+
38+
def accepts_a_tuple_as_a_list_value():
39+
list_field = ("apple", "banana", "coconut")
40+
result = _complete(list_field)
41+
assert result == ({"listField": list(list_field)}, None)
42+
2843
def accepts_a_set_as_a_list_value():
2944
# Note that sets are not ordered in Python.
3045
list_field = {"apple", "banana", "coconut"}

tests/execution/test_parallel.py

+40
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,27 @@ async def wait(self) -> bool:
3232

3333

3434
def describe_parallel_execution():
35+
@mark.asyncio
36+
async def resolve_single_field():
37+
# make sure that the special case of resolving a single field works
38+
async def resolve(*_args):
39+
return True
40+
41+
schema = GraphQLSchema(
42+
GraphQLObjectType(
43+
"Query",
44+
{
45+
"foo": GraphQLField(GraphQLBoolean, resolve=resolve),
46+
},
47+
)
48+
)
49+
50+
awaitable_result = execute(schema, parse("{foo}"))
51+
assert isinstance(awaitable_result, Awaitable)
52+
result = await awaitable_result
53+
54+
assert result == ({"foo": True}, None)
55+
3556
@mark.asyncio
3657
async def resolve_fields_in_parallel():
3758
barrier = Barrier(2)
@@ -58,6 +79,25 @@ async def resolve(*_args):
5879

5980
assert result == ({"foo": True, "bar": True}, None)
6081

82+
@mark.asyncio
83+
async def resolve_single_element_list():
84+
# make sure that the special case of resolving a single element list works
85+
async def resolve(*_args):
86+
return [True]
87+
88+
schema = GraphQLSchema(
89+
GraphQLObjectType(
90+
"Query",
91+
{"foo": GraphQLField(GraphQLList(GraphQLBoolean), resolve=resolve)},
92+
)
93+
)
94+
95+
awaitable_result = execute(schema, parse("{foo}"))
96+
assert isinstance(awaitable_result, Awaitable)
97+
result = await awaitable_result
98+
99+
assert result == ({"foo": [True]}, None)
100+
61101
@mark.asyncio
62102
async def resolve_list_in_parallel():
63103
barrier = Barrier(2)

tox.ini

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,5 @@ deps =
6262
commands =
6363
# to also run the time-consuming tests: tox -e py310 -- --run-slow
6464
# to run the benchmarks: tox -e py310 -- -k benchmarks --benchmark-enable
65-
py37,py38.py39,py310,pypy39: pytest tests {posargs}
66-
py311: pytest tests {posargs: --cov-report=term-missing --cov=graphql --cov=tests --cov-fail-under=100}
65+
py37,py38.py39,py311,pypy39: pytest tests {posargs}
66+
py310: pytest tests {posargs: --cov-report=term-missing --cov=graphql --cov=tests --cov-fail-under=100}

0 commit comments

Comments
 (0)