Skip to content

Commit 91a58d7

Browse files
authored
fix: prevent async iterable detection for sync execution in gql-core 3.3 (#4267)
* fix: prevent async iterable detection for sync execution in gql-core 3.3 * chore: bump graphql-core 3.3 version to 3.3.0a12 in CI * fix: use tuples for AST node collections for gql-core 3.3 compat graphql-core 3.3.0a12 requires tuples instead of lists for AST node collection fields (e.g. ObjectValueNode.fields). Convert the list comprehension in ast_from_leaf_type to a tuple. * chore: add RELEASE.md for patch release * chore: update RELEASE.md description * chore: mention minimum gql-core 3.3.0a12 in RELEASE.md * test: add regression test for execute_sync with __aiter__ objects * test: improve docstring for __aiter__ sync execution test
1 parent 3c882cb commit 91a58d7

File tree

7 files changed

+64
-7
lines changed

7 files changed

+64
-7
lines changed

.github/workflows/e2e-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
- name: Install Python dependencies
4646
run: |
4747
poetry install --extras cli
48-
poetry run pip install graphql-core==3.3.0a9
48+
poetry run pip install graphql-core==3.3.0a12
4949
5050
- name: Start Strawberry server
5151
run: |

RELEASE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Release type: patch
2+
3+
Fix sync execution crash with graphql-core 3.3 where `execute_sync()` would return a coroutine
4+
instead of an `ExecutionResult`, causing `RuntimeError: There is no current event loop`,
5+
because graphql-core 3.3's `is_async_iterable` default treats objects with `__aiter__`
6+
(like Django QuerySets) as async iterables.
7+
8+
Now passes `is_async_iterable=lambda _x: False` during sync execution to prevent this.
9+
10+
Note: graphql-core >= 3.3.0a12 is now the minimum required version for the 3.3.x series.

noxfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
GQL_CORE_VERSIONS = [
1515
"3.2.6",
16-
"3.3.0a9",
16+
"3.3.0a12",
1717
]
1818

1919
COMMON_PYTEST_OPTIONS = [

strawberry/printer/ast_from_value.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ def ast_from_leaf_type(serialized: object, type_: GraphQLInputType | None) -> Va
7070

7171
if isinstance(serialized, dict):
7272
return ObjectValueNode(
73-
fields=[
73+
fields=tuple(
7474
ObjectFieldNode(
7575
name=NameNode(value=key),
7676
value=ast_from_leaf_type(value, None),
7777
)
7878
for key, value in serialized.items()
79-
]
79+
)
8080
)
8181

8282
raise TypeError(

strawberry/schema/schema.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
from strawberry.types.scalar import ScalarDefinition, ScalarWrapper
9696
from strawberry.types.union import StrawberryUnion
9797

98+
9899
SubscriptionResult: TypeAlias = AsyncGenerator[
99100
PreExecutionError | ExecutionResult, None
100101
]
@@ -398,12 +399,18 @@ def create_extensions_runner(
398399
)
399400

400401
def _get_custom_context_kwargs(
401-
self, operation_extensions: dict[str, Any] | None = None
402+
self,
403+
operation_extensions: dict[str, Any] | None = None,
404+
*,
405+
sync: bool = False,
402406
) -> dict[str, Any]:
403407
if not IS_GQL_33:
404408
return {}
405409

406-
return {"operation_extensions": operation_extensions}
410+
kwargs: dict[str, Any] = {"operation_extensions": operation_extensions}
411+
if sync:
412+
kwargs["is_async_iterable"] = lambda _x: False
413+
return kwargs
407414

408415
def _get_middleware_manager(
409416
self, extensions: list[SchemaExtension], *, cached: bool = True
@@ -705,7 +712,9 @@ def execute_sync(
705712
"Incremental execution is enabled but experimental_execute_incrementally is not available, "
706713
"please install graphql-core>=3.3.0"
707714
)
708-
custom_context_kwargs = self._get_custom_context_kwargs(operation_extensions)
715+
custom_context_kwargs = self._get_custom_context_kwargs(
716+
operation_extensions, sync=True
717+
)
709718

710719
try:
711720
with extensions_runner.operation():

strawberry/utils/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,9 @@
22

33
IS_GQL_33 = version_info >= VersionInfo.from_str("3.3.0a0")
44
IS_GQL_32 = not IS_GQL_33
5+
6+
if IS_GQL_33 and version_info < VersionInfo.from_str("3.3.0a12"):
7+
raise ImportError(
8+
f"graphql-core {version_info} is not supported. "
9+
"Please upgrade to graphql-core >= 3.3.0a12."
10+
)

tests/schema/test_basic.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,3 +578,35 @@ class FooBar2:
578578
FooBar(foo=1)
579579
FooBar(bar=2)
580580
FooBar(foo=1, bar=2)
581+
582+
583+
def test_execute_sync_with_aiter_object():
584+
"""Sync execution works with objects that define __aiter__.
585+
586+
graphql-core 3.3's is_async_iterable default treats any object with
587+
__aiter__ (e.g. Django QuerySets) as async, causing execute_sync to
588+
return a coroutine instead of an ExecutionResult.
589+
"""
590+
591+
class FakeQuerySet:
592+
def __aiter__(self):
593+
raise NotImplementedError
594+
595+
def __iter__(self):
596+
yield from [{"name": "Alice"}, {"name": "Bob"}]
597+
598+
@strawberry.type
599+
class User:
600+
name: str
601+
602+
@strawberry.type
603+
class Query:
604+
@strawberry.field
605+
def users(self) -> list[User]:
606+
return [User(name=u["name"]) for u in FakeQuerySet()]
607+
608+
schema = strawberry.Schema(query=Query)
609+
result = schema.execute_sync("{ users { name } }")
610+
611+
assert not result.errors
612+
assert result.data == {"users": [{"name": "Alice"}, {"name": "Bob"}]}

0 commit comments

Comments
 (0)