From d04629b7ce51575e8562a60f6bb6c5d226e17021 Mon Sep 17 00:00:00 2001 From: Mikhail Efimov Date: Tue, 10 Jun 2025 17:38:25 +0300 Subject: [PATCH 1/4] [3.14] GH-135171: Revert async generator expressions behavior --- Lib/test/test_asyncgen.py | 7 +++++++ .../2025-06-10-17-37-11.gh-issue-135171.P9UDfS.rst | 2 ++ Python/codegen.c | 12 ++++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-06-10-17-37-11.gh-issue-135171.P9UDfS.rst diff --git a/Lib/test/test_asyncgen.py b/Lib/test/test_asyncgen.py index 2c44647bf3e2f9..b0424e67b673af 100644 --- a/Lib/test/test_asyncgen.py +++ b/Lib/test/test_asyncgen.py @@ -1835,6 +1835,13 @@ async def run(): res = self.loop.run_until_complete(run()) self.assertEqual(res, [i * 2 for i in range(1, 10)]) + def test_async_gen_expression_incorrect(self): + err_msg_async = "'async for' requires an object with " \ + "__aiter__ method, got .*" + + with self.assertRaisesRegex(TypeError, err_msg_async): + (x async for x in None) + def test_asyncgen_nonstarted_hooks_are_cancellable(self): # See https://bugs.python.org/issue38013 messages = [] diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-06-10-17-37-11.gh-issue-135171.P9UDfS.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-10-17-37-11.gh-issue-135171.P9UDfS.rst new file mode 100644 index 00000000000000..ae2731d393a9d0 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-10-17-37-11.gh-issue-135171.P9UDfS.rst @@ -0,0 +1,2 @@ +Reverts the behavior of async generator expressions when created with object +w/o __aiter__ method to the pre-3.13 behavior of raising a TypeError. diff --git a/Python/codegen.c b/Python/codegen.c index 93c3a21266cb7d..a4dd04aeba10f9 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -4544,9 +4544,9 @@ codegen_async_comprehension_generator(compiler *c, location loc, else { /* Sub-iter - calculate on the fly */ VISIT(c, expr, gen->iter); + ADDOP(c, LOC(gen->iter), GET_AITER); } } - ADDOP(c, LOC(gen->iter), GET_AITER); USE_LABEL(c, start); /* Runtime will push a block here, so we need to account for that */ @@ -4776,7 +4776,6 @@ codegen_comprehension(compiler *c, expr_ty e, int type, location loc = LOC(e); outermost = (comprehension_ty) asdl_seq_GET(generators, 0); - int is_sync_genexpr = type == COMP_GENEXP && !outermost->is_async; if (is_inlined) { VISIT(c, expr, outermost->iter); if (push_inlined_comprehension_state(c, loc, entry, &inline_state)) { @@ -4853,8 +4852,13 @@ codegen_comprehension(compiler *c, expr_ty e, int type, Py_CLEAR(co); VISIT(c, expr, outermost->iter); - if (is_sync_genexpr) { - ADDOP(c, loc, GET_ITER); + if (type == COMP_GENEXP) { + if (outermost->is_async) { + ADDOP(c, loc, GET_AITER); + } + else { + ADDOP(c, loc, GET_ITER); + } } ADDOP_I(c, loc, CALL, 0); From 53bd362ae2bb08f7614b544beeed2e0d0bb89027 Mon Sep 17 00:00:00 2001 From: Mikhail Efimov Date: Tue, 10 Jun 2025 18:21:24 +0300 Subject: [PATCH 2/4] [3.14] GH-135171: Bug fix, test improved --- Lib/test/test_asyncgen.py | 9 ++++++++- Python/codegen.c | 28 +++++++++++++++++++--------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_asyncgen.py b/Lib/test/test_asyncgen.py index b0424e67b673af..2f2865bad2cdb1 100644 --- a/Lib/test/test_asyncgen.py +++ b/Lib/test/test_asyncgen.py @@ -1836,11 +1836,18 @@ async def run(): self.assertEqual(res, [i * 2 for i in range(1, 10)]) def test_async_gen_expression_incorrect(self): + async def ag(): + yield 42 + + async def run(arg): + (x async for x in arg) + err_msg_async = "'async for' requires an object with " \ "__aiter__ method, got .*" + self.loop.run_until_complete(run(ag())) with self.assertRaisesRegex(TypeError, err_msg_async): - (x async for x in None) + self.loop.run_until_complete(run(None)) def test_asyncgen_nonstarted_hooks_are_cancellable(self): # See https://bugs.python.org/issue38013 diff --git a/Python/codegen.c b/Python/codegen.c index a4dd04aeba10f9..e7e055e6c85422 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -4758,6 +4758,19 @@ pop_inlined_comprehension_state(compiler *c, location loc, return SUCCESS; } +static inline int +codegen_comprehension_iter(compiler *c, comprehension_ty comp) +{ + VISIT(c, expr, comp->iter); + if (comp->is_async) { + ADDOP(c, LOC(comp->iter), GET_AITER); + } + else { + ADDOP(c, LOC(comp->iter), GET_ITER); + } + return SUCCESS; +} + static int codegen_comprehension(compiler *c, expr_ty e, int type, identifier name, asdl_comprehension_seq *generators, expr_ty elt, @@ -4777,7 +4790,9 @@ codegen_comprehension(compiler *c, expr_ty e, int type, outermost = (comprehension_ty) asdl_seq_GET(generators, 0); if (is_inlined) { - VISIT(c, expr, outermost->iter); + if (codegen_comprehension_iter(c, outermost)) { + goto error; + } if (push_inlined_comprehension_state(c, loc, entry, &inline_state)) { goto error; } @@ -4851,15 +4866,10 @@ codegen_comprehension(compiler *c, expr_ty e, int type, } Py_CLEAR(co); - VISIT(c, expr, outermost->iter); - if (type == COMP_GENEXP) { - if (outermost->is_async) { - ADDOP(c, loc, GET_AITER); - } - else { - ADDOP(c, loc, GET_ITER); - } + if (codegen_comprehension_iter(c, outermost)) { + goto error; } + ADDOP_I(c, loc, CALL, 0); if (is_async_comprehension && type != COMP_GENEXP) { From 75e6184c8b924178fbbbb9fea32f201573b0840f Mon Sep 17 00:00:00 2001 From: Mikhail Efimov Date: Tue, 10 Jun 2025 18:56:55 +0300 Subject: [PATCH 3/4] [3.14] GH-135171: Bug fix 2, all related changes in codegen.c are reverted --- Python/codegen.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Python/codegen.c b/Python/codegen.c index e7e055e6c85422..d708e59ed67baf 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -4411,7 +4411,6 @@ codegen_sync_comprehension_generator(compiler *c, location loc, comprehension_ty gen = (comprehension_ty)asdl_seq_GET(generators, gen_index); - int is_outer_genexpr = gen_index == 0 && type == COMP_GENEXP; if (!iter_on_stack) { if (gen_index == 0) { assert(METADATA(c)->u_argcount == 1); @@ -4442,15 +4441,13 @@ codegen_sync_comprehension_generator(compiler *c, location loc, } if (IS_JUMP_TARGET_LABEL(start)) { VISIT(c, expr, gen->iter); + ADDOP(c, LOC(gen->iter), GET_ITER); } } } if (IS_JUMP_TARGET_LABEL(start)) { depth++; - if (!is_outer_genexpr) { - ADDOP(c, LOC(gen->iter), GET_ITER); - } USE_LABEL(c, start); ADDOP_JUMP(c, LOC(gen->iter), FOR_ITER, anchor); } From 549cd1145ed46ce68182b1b626db52dc0fe18455 Mon Sep 17 00:00:00 2001 From: Mikhail Efimov Date: Wed, 11 Jun 2025 20:30:38 +0300 Subject: [PATCH 4/4] Test for async generator is added --- Lib/test/test_coroutines.py | 13 +++++++++---- Lib/test/test_generators.py | 2 +- Lib/test/test_listcomps.py | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_coroutines.py b/Lib/test/test_coroutines.py index c8360a834482e1..954003ab14df93 100644 --- a/Lib/test/test_coroutines.py +++ b/Lib/test/test_coroutines.py @@ -2267,7 +2267,7 @@ def c(): def test_call_aiter_once_in_comprehension(self): - class Iterator: + class AsyncIterator: def __init__(self): self.val = 0 @@ -2283,12 +2283,17 @@ async def __anext__(self): class C: def __aiter__(self): - return Iterator() + return AsyncIterator() - async def run(): + async def run_listcomp(): return [i async for i in C()] - self.assertEqual(run_async(run()), ([], [1,2])) + async def run_asyncgen(): + ag = (i async for i in C()) + return [i async for i in ag] + + self.assertEqual(run_async(run_listcomp()), ([], [1, 2])) + self.assertEqual(run_async(run_asyncgen()), ([], [1, 2])) @unittest.skipIf( diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index 6c056b63f88480..cb73179160d10d 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -288,7 +288,7 @@ class C: def __iter__(self): return Iterator() - self.assertEqual([1,2], list(i for i in C())) + self.assertEqual([1, 2], list(i for i in C())) class ModifyUnderlyingIterableTest(unittest.TestCase): diff --git a/Lib/test/test_listcomps.py b/Lib/test/test_listcomps.py index 38a36d3c9925a2..ef8f3d492dfc46 100644 --- a/Lib/test/test_listcomps.py +++ b/Lib/test/test_listcomps.py @@ -770,7 +770,7 @@ class C: def __iter__(self): return Iterator() - self.assertEqual([1,2], [i for i in C()]) + self.assertEqual([1, 2], [i for i in C()]) __test__ = {'doctests' : doctests}