From c6364f65dbe1880aeb389a908f293f8ab8c53b5f Mon Sep 17 00:00:00 2001 From: youdie006 Date: Thu, 29 Oct 2026 17:57:14 +0900 Subject: [PATCH] fix(execution): assume no async iterables in execute_sync 333d0d71 made graphql_sync pass assume_not_async_iterable when check_sync is off (#258). execute_sync was not updated, so an object that is both iterable and async-iterable, such as a Django QuerySet, took the async list path and execute_sync returned a coroutine in data with no error. --- src/graphql/execution/execute.py | 10 +++++ tests/execution/test_sync.py | 72 +++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/graphql/execution/execute.py b/src/graphql/execution/execute.py index 4fbddb8c..094caf6d 100644 --- a/src/graphql/execution/execute.py +++ b/src/graphql/execution/execute.py @@ -465,6 +465,14 @@ def assume_not_awaitable(_value: Any) -> TypeGuard[Awaitable]: return False +def assume_not_async_iterable(_value: Any) -> TypeGuard[AsyncIterable]: + """Replacement for is_async_iterable if everything is assumed to be synchronous. + + :meta private: + """ + return False + + def execute_sync( schema: GraphQLSchema, document: DocumentNode, @@ -549,6 +557,7 @@ def execute_sync( if callable(check_sync) else (None if check_sync else assume_not_awaitable) ) + is_async_iterable = assume_not_async_iterable if not check_sync else None result = experimental_execute_incrementally( schema, @@ -565,6 +574,7 @@ def execute_sync( middleware, executor_class, is_awaitable, + is_async_iterable, hide_suggestions=hide_suggestions, abort_signal=abort_signal, hooks=hooks, diff --git a/tests/execution/test_sync.py b/tests/execution/test_sync.py index 1fcdd59f..5555add1 100644 --- a/tests/execution/test_sync.py +++ b/tests/execution/test_sync.py @@ -4,7 +4,13 @@ from graphql.execution import execute, execute_sync from graphql.language import parse from graphql.pyutils import is_awaitable -from graphql.type import GraphQLField, GraphQLObjectType, GraphQLSchema, GraphQLString +from graphql.type import ( + GraphQLField, + GraphQLList, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, +) from graphql.validation import validate from ..fixtures import cleanup @@ -36,6 +42,29 @@ async def _resolve_async(root_value, _info): ), ) + list_schema = GraphQLSchema( + GraphQLObjectType( + "Query", + { + "listField": GraphQLField( + GraphQLList(GraphQLString), resolve=_resolve_sync + ) + }, + ) + ) + + class _SyncAndAsyncIterable: + """An iterable that is also an async iterable, like a Django QuerySet.""" + + def __iter__(self): + return iter(["sync"]) + + def __aiter__(self): + return self + + async def __anext__(self): # pragma: no cover + raise StopAsyncIteration + def does_not_return_an_awaitable_for_initial_errors(): doc = "fragment Example on Query { syncField }" assert execute(schema, parse(doc), "rootValue") == ( @@ -156,6 +185,47 @@ async def throws_if_encountering_async_iterable_execution_without_check_sync(): del exc_info cleanup() + def iterates_sync_and_async_iterables_synchronously_without_check_sync(): + doc = "query Example { listField }" + result = execute_sync( + list_schema, document=parse(doc), root_value=_SyncAndAsyncIterable() + ) + assert result == ({"listField": ["sync"]}, None) + + async def throws_if_encountering_sync_and_async_iterable_with_check_sync(): + doc = "query Example { listField }" + with pytest.raises(RuntimeError) as exc_info: + execute_sync( + list_schema, + document=parse(doc), + root_value=_SyncAndAsyncIterable(), + check_sync=True, + ) + msg = str(exc_info.value) + assert msg == "GraphQL execution failed to complete synchronously." + del exc_info + cleanup() + + def reports_async_iterables_as_errors_without_check_sync(): + async def _async_iterable(): + yield "async" # pragma: no cover + + doc = "query Example { listField }" + result = execute_sync( + list_schema, document=parse(doc), root_value=_async_iterable() + ) + assert result == ( + {"listField": None}, + [ + { + "message": "Expected Iterable, but did not find one" + " for field 'Query.listField'.", + "locations": [(1, 17)], + "path": ["listField"], + } + ], + ) + def describe_graphql_sync(): def reports_errors_raised_during_schema_validation(): bad_schema = GraphQLSchema()