Skip to content

Commit ee1ea63

Browse files
committed
Experimental support for async iterables as list values (#123)
1 parent 02f349b commit ee1ea63

File tree

2 files changed

+79
-2
lines changed

2 files changed

+79
-2
lines changed

src/graphql/execution/execute.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from inspect import isawaitable
44
from typing import (
55
Any,
6+
AsyncIterable,
67
Awaitable,
78
Callable,
89
Dict,
@@ -670,17 +671,31 @@ def complete_list_value(
670671
field_nodes: List[FieldNode],
671672
info: GraphQLResolveInfo,
672673
path: Path,
673-
result: Iterable[Any],
674+
result: Union[AsyncIterable[Any], Iterable[Any]],
674675
) -> AwaitableOrValue[List[Any]]:
675676
"""Complete a list value.
676677
677678
Complete a list value by completing each item in the list with the inner type.
678679
"""
679680
if not is_iterable(result):
681+
# experimental: allow async iterables
682+
if isinstance(result, AsyncIterable):
683+
# noinspection PyShadowingNames
684+
async def async_iterable_to_list(
685+
async_result: AsyncIterable[Any],
686+
) -> Any:
687+
sync_result = [item async for item in async_result]
688+
return self.complete_list_value(
689+
return_type, field_nodes, info, path, sync_result
690+
)
691+
692+
return async_iterable_to_list(result)
693+
680694
raise GraphQLError(
681695
"Expected Iterable, but did not find one for field"
682696
f" '{info.parent_type.name}.{info.field_name}'."
683697
)
698+
result = cast(Iterable[Any], result)
684699

685700
# This is specified as a simple map, however we're optimizing the path where
686701
# the list contains no coroutine objects by avoiding creating another coroutine

tests/execution/test_lists.py

+63-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
from typing import Any
1+
from typing import cast, Any, Awaitable
22

33
from pytest import mark
44

55
from graphql.execution import execute, execute_sync, ExecutionResult
66
from graphql.language import parse
7+
from graphql.pyutils import is_awaitable
78
from graphql.utilities import build_schema
89

910

@@ -45,6 +46,24 @@ def list_field():
4546
None,
4647
)
4748

49+
def accepts_a_custom_iterable_as_a_list_value():
50+
class ListField:
51+
def __iter__(self):
52+
self.last = "hello"
53+
return self
54+
55+
def __next__(self):
56+
last = self.last
57+
if last == "stop":
58+
raise StopIteration
59+
self.last = "world" if last == "hello" else "stop"
60+
return last
61+
62+
assert _complete(ListField()) == (
63+
{"listField": ["hello", "world"]},
64+
None,
65+
)
66+
4867
def accepts_function_arguments_as_a_list_value():
4968
def get_args(*args):
5069
return args # actually just a tuple, nothing special in Python
@@ -195,3 +214,46 @@ async def results_in_errors():
195214
None,
196215
errors,
197216
)
217+
218+
219+
def describe_experimental_execute_accepts_async_iterables_as_list_value():
220+
async def _complete(list_field):
221+
result = execute(
222+
build_schema("type Query { listField: [String] }"),
223+
parse("{ listField }"),
224+
Data(list_field),
225+
)
226+
assert is_awaitable(result)
227+
result = cast(Awaitable, result)
228+
return await result
229+
230+
@mark.asyncio
231+
async def accepts_an_async_generator_as_a_list_value():
232+
async def list_field():
233+
yield "one"
234+
yield 2
235+
yield True
236+
237+
assert await _complete(list_field()) == (
238+
{"listField": ["one", "2", "true"]},
239+
None,
240+
)
241+
242+
@mark.asyncio
243+
async def accepts_a_custom_async_iterable_as_a_list_value():
244+
class ListField:
245+
def __aiter__(self):
246+
self.last = "hello"
247+
return self
248+
249+
async def __anext__(self):
250+
last = self.last
251+
if last == "stop":
252+
raise StopAsyncIteration
253+
self.last = "world" if last == "hello" else "stop"
254+
return last
255+
256+
assert await _complete(ListField()) == (
257+
{"listField": ["hello", "world"]},
258+
None,
259+
)

0 commit comments

Comments
 (0)