From 43d9a7b8e4d71b22300135aa2957377084c9e6f6 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 3 May 2026 15:47:15 +0200 Subject: [PATCH 01/10] Cross-reference two tests suites in `test_yield_from` and `test_async_yield_from` --- Lib/test/test_async_yield_from.py | 27 +++------------------------ Lib/test/test_yield_from.py | 2 ++ 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/Lib/test/test_async_yield_from.py b/Lib/test/test_async_yield_from.py index 25ea7ead73755b..21072e6e92bae2 100644 --- a/Lib/test/test_async_yield_from.py +++ b/Lib/test/test_async_yield_from.py @@ -1,7 +1,8 @@ """ -Test suite for PEP 828 implementation +Test suite for PEP 828 implementation. -Adapted from the 'yield from' tests. +The tests below were adapted from the 'yield from' tests. +For more context on any particular test, try searching for an analogue in `test_yield_from`. """ import unittest @@ -1018,7 +1019,6 @@ async def eggs(g): @_async_test async def test_custom_iterator_return(self): - # See issue #15568 class MyIter: def __aiter__(self): return self @@ -1033,24 +1033,6 @@ async def gen(): @_async_test async def test_close_with_cleared_frame(self): - # See issue #17669. - # - # Create a stack of generators: outer() delegating to inner() - # delegating to innermost(). The key point is that the instance of - # inner is created first: this ensures that its frame appears before - # the instance of outer in the GC linked list. - # - # At the gc.collect call: - # - frame_clear is called on the inner_gen frame. - # - gen_dealloc is called on the outer_gen generator (the only - # reference is in the frame's locals). - # - gen_close is called on the outer_gen generator. - # - gen_close_iter is called to close the inner_gen generator, which - # in turn calls gen_close, and gen_yf. - # - # Previously, gen_yf would crash since inner_gen's frame had been - # cleared (and in particular f_stacktop was NULL). - async def innermost(): yield async def inner(): @@ -1073,7 +1055,6 @@ async def outer(): @_async_test async def test_send_tuple_with_custom_generator(self): - # See issue #21209. class MyGen: def __aiter__(self): return self @@ -1626,8 +1607,6 @@ async def outer(): @_async_test async def test_throws_in_iter(self): - # See GH-126366: NULL pointer dereference if __iter__ - # threw an exception. class Silly: async def __aiter__(self): yield from () diff --git a/Lib/test/test_yield_from.py b/Lib/test/test_yield_from.py index 74c9fa16987638..19aa89db1d0843 100644 --- a/Lib/test/test_yield_from.py +++ b/Lib/test/test_yield_from.py @@ -5,6 +5,8 @@ adapted from original tests written by Greg Ewing see + +See also `test_async_yield_from'. Where applicable, consider keeping the two test suites in sync. """ import unittest From 2129c7a03840b714899e27fa10088f64e51f59a9 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 3 May 2026 15:50:23 +0200 Subject: [PATCH 02/10] Replace "consistent with" with "analogous to" --- Lib/test/test_async_yield_from.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_async_yield_from.py b/Lib/test/test_async_yield_from.py index 21072e6e92bae2..9155b9e5aa8f4c 100644 --- a/Lib/test/test_async_yield_from.py +++ b/Lib/test/test_async_yield_from.py @@ -1169,7 +1169,7 @@ async def outer(): g = outer() self.assertIs(await anext(g), yielded_first) raised = GeneratorExit() - # GeneratorExit is suppressed. This is consistent with PEP 342: + # GeneratorExit is suppressed. This is analogous to PEP 342: # https://peps.python.org/pep-0342/#new-generator-method-close await g.aclose() await self.assert_stop_iteration(g) @@ -1182,7 +1182,7 @@ async def outer(): with self.assertRaises(GeneratorExit) as caught: await g.athrow(thrown) # The raised GeneratorExit is suppressed, but the thrown one - # propagates. This is consistent with PEP 380: + # propagates. This is analogous to PEP 380: # https://peps.python.org/pep-0380/#proposal self.assertIs(caught.exception, thrown) self.assertIsNone(caught.exception.__context__) @@ -1486,7 +1486,7 @@ async def outer(): with self.subTest("close"): g = outer() self.assertIs(await anext(g), yielded_first) - # No chaining happens. This is consistent with PEP 342: + # No chaining happens. This is analogous to PEP 342: # https://peps.python.org/pep-0342/#new-generator-method-close with self.assert_generator_ignored_generator_exit() as caught: await g.aclose() @@ -1497,7 +1497,7 @@ async def outer(): g = outer() self.assertIs(await anext(g), yielded_first) thrown = GeneratorExit() - # No chaining happens. This is consistent with PEP 342: + # No chaining happens. This is analogous to PEP 342: # https://peps.python.org/pep-0342/#new-generator-method-close with self.assert_generator_ignored_generator_exit() as caught: await g.athrow(thrown) @@ -1558,7 +1558,7 @@ async def outer(): with self.subTest("close"): g = outer() self.assertIs(await anext(g), yielded_first) - # StopAsyncIteration is suppressed. This is consistent with PEP 342: + # StopAsyncIteration is suppressed. This is analogous to PEP 342: # https://peps.python.org/pep-0342/#new-generator-method-close await g.aclose() await self.assert_stop_iteration(g) @@ -1567,7 +1567,7 @@ async def outer(): g = outer() self.assertIs(await anext(g), yielded_first) thrown = GeneratorExit() - # StopAsyncIteration is suppressed. This is consistent with PEP 342: + # StopAsyncIteration is suppressed. This is analogous to PEP 342: # https://peps.python.org/pep-0342/#new-generator-method-close with self.assertRaises(GeneratorExit) as caught: await g.athrow(thrown) From 2d855eb23df9ca1cd6ec6bf0f5e8762a1c78ed9f Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 3 May 2026 17:14:53 +0200 Subject: [PATCH 03/10] Enforce 1:1 parity between `test_async_yield_from` and `test_yield_from` Rename every test in `TestPEP828Operation` and `TestInterestingEdgeCases` to its `test_yield_from` sibling's name with an `_ayf` suffix, so the two suites map onto each other mechanically. Add `TestParityWithPEP380`, which uses `assert_parity` to enforce that each mirrored class is in symmetric parity: every PEP 380 test has a corresponding `_ayf` variant, and every variant has a PEP 380 base. A missing variant or an unmatched extra both fail the parity test. Move `test_delegate_exception` (the only PEP 828-only test) into a new `TestPEP828Extras` class. Tests that have no PEP 380 analogue belong there; the parity classes stay strictly mirrored. Lazy-import `test_yield_from` so the parity machinery doesn't pull it in unless the parity tests actually run. --- Lib/test/test_async_yield_from.py | 146 +++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 43 deletions(-) diff --git a/Lib/test/test_async_yield_from.py b/Lib/test/test_async_yield_from.py index 9155b9e5aa8f4c..ee7dc7356b3363 100644 --- a/Lib/test/test_async_yield_from.py +++ b/Lib/test/test_async_yield_from.py @@ -9,6 +9,7 @@ import inspect from functools import partial +lazy from test import test_yield_from from test.support import captured_stderr, disable_gc, gc_collect, run_yielding_async_fn, catch_unraisable_exception _async_test = partial(partial, run_yielding_async_fn) @@ -20,7 +21,7 @@ class TestPEP828Operation(unittest.TestCase): """ @_async_test - async def test_delegation_of_initial_anext_to_subgenerator(self): + async def test_delegation_of_initial_next_to_subgenerator_ayf(self): """ Test delegation of initial anext() call to subgenerator """ @@ -44,7 +45,7 @@ async def g2(): ]) @_async_test - async def test_raising_exception_in_initial_anext_call(self): + async def test_raising_exception_in_initial_next_call_ayf(self): """ Test raising exception in initial anext() call """ @@ -77,7 +78,7 @@ async def g2(): ]) @_async_test - async def test_delegation_of_anext_call_to_subgenerator(self): + async def test_delegation_of_next_call_to_subgenerator_ayf(self): """ Test delegation of anext() call to subgenerator """ @@ -107,7 +108,7 @@ async def g2(): ]) @_async_test - async def test_raising_exception_in_delegated_anext_call(self): + async def test_raising_exception_in_delegated_next_call_ayf(self): """ Test raising exception in delegated anext() call """ @@ -145,7 +146,7 @@ async def g2(): ]) @_async_test - async def test_delegation_of_asend(self): + async def test_delegation_of_send_ayf(self): """ Test delegation of send() """ @@ -190,7 +191,7 @@ async def g2(): ]) @_async_test - async def test_handling_exception_while_delegating_send(self): + async def test_handling_exception_while_delegating_send_ayf(self): """ Test handling exception while delegating 'send' """ @@ -233,7 +234,7 @@ async def run(): ]) @_async_test - async def test_delegating_aclose(self): + async def test_delegating_close_ayf(self): """ Test delegating 'aclose' """ @@ -268,7 +269,7 @@ async def g2(): ]) @_async_test - async def test_handing_exception_while_delegating_close(self): + async def test_handing_exception_while_delegating_close_ayf(self): """ Test handling exception while delegating 'close' """ @@ -310,7 +311,7 @@ async def g2(): ]) @_async_test - async def test_delegating_throw(self): + async def test_delegating_throw_ayf(self): """ Test delegating 'throw' """ @@ -351,7 +352,7 @@ async def g2(): ]) @_async_test - async def test_value_attribute_of_StopAsyncIteration_exception(self): + async def test_value_attribute_of_StopIteration_exception_ayf(self): """ Test 'value' attribute of StopAsyncIteration exception """ @@ -375,7 +376,7 @@ async def pex(e): ]) @_async_test - async def test_exception_value_crash(self): + async def test_exception_value_crash_ayf(self): # There used to be a refcount error when the return value # stored in the StopAsyncIteration has a refcount of 1. async def g1(): @@ -386,7 +387,7 @@ async def g2(): self.assertEqual([x async for x in g1()], ["g2"]) @_async_test - async def test_generator_return_value(self): + async def test_generator_return_value_ayf(self): """ Test generator return value """ @@ -438,7 +439,7 @@ async def g2(v = None): ]) @_async_test - async def test_delegation_of_anext_to_non_generator(self): + async def test_delegation_of_next_to_non_generator_ayf(self): """ Test delegation of anext() to non-generator """ @@ -454,7 +455,7 @@ async def g(): ]) @_async_test - async def test_conversion_of_asendNone_to_next(self): + async def test_conversion_of_sendNone_to_next_ayf(self): """ Test conversion of asend(None) to next() """ @@ -472,7 +473,7 @@ async def g(): ]) @_async_test - async def test_delegation_of_close_to_non_generator(self): + async def test_delegation_of_close_to_non_generator_ayf(self): """ Test delegation of close() to non-generator """ @@ -495,7 +496,7 @@ async def g(): ]) @_async_test - async def test_delegating_throw_to_non_generator(self): + async def test_delegating_throw_to_non_generator_ayf(self): """ Test delegating 'throw' to non-generator """ @@ -528,7 +529,7 @@ async def g(): ]) @_async_test - async def test_attempting_to_send_to_non_generator(self): + async def test_attempting_to_send_to_non_generator_ayf(self): """ Test attempting to send to non-generator """ @@ -556,7 +557,7 @@ async def g(): ]) @_async_test - async def test_broken_getattr_handling(self): + async def test_broken_getattr_handling_ayf(self): """ Test subiterator with a broken getattr implementation """ @@ -589,7 +590,7 @@ async def g(): self.assertEqual(ZeroDivisionError, cm.unraisable.exc_type) @_async_test - async def test_exception_in_initial_next_call(self): + async def test_exception_in_initial_next_call_ayf(self): """ Test exception in initial next() call """ @@ -610,7 +611,7 @@ async def run(): ]) @_async_test - async def test_attempted_async_yield_from_loop(self): + async def test_attempted_yield_from_loop_ayf(self): """ Test attempted yield-from loop """ @@ -646,7 +647,7 @@ async def g2(): ]) @_async_test - async def test_returning_value_from_delegated_throw(self): + async def test_returning_value_from_delegated_throw_ayf(self): """ Test returning value from delegated 'throw' """ @@ -690,7 +691,7 @@ class LunchError(Exception): ]) @_async_test - async def test_anext_and_return_with_value(self): + async def test_next_and_return_with_value_ayf(self): """ Test next and return with value """ @@ -733,7 +734,7 @@ async def g(r): ]) @_async_test - async def test_send_and_return_with_value(self): + async def test_send_and_return_with_value_ayf(self): """ Test send and return with value """ @@ -781,7 +782,7 @@ async def g(r): ]) @_async_test - async def test_catching_exception_from_subgen_and_returning(self): + async def test_catching_exception_from_subgen_and_returning_ayf(self): """ Test catching an exception thrown into a subgenerator and returning a value @@ -811,7 +812,7 @@ async def outer(): ]) @_async_test - async def test_throwing_GeneratorExit_into_subgen_that_returns(self): + async def test_throwing_GeneratorExit_into_subgen_that_returns_ayf(self): """ Test throwing GeneratorExit into a subgenerator that catches it and returns normally. @@ -842,7 +843,7 @@ async def g(): ]) @_async_test - async def test_throwing_GeneratorExit_into_subgenerator_that_yields(self): + async def test_throwing_GeneratorExit_into_subgenerator_that_yields_ayf(self): """ Test throwing GeneratorExit into a subgenerator that catches it and yields. @@ -873,7 +874,7 @@ async def g(): ]) @_async_test - async def test_throwing_GeneratorExit_into_subgen_that_raises(self): + async def test_throwing_GeneratorExit_into_subgen_that_raises_ayf(self): """ Test throwing GeneratorExit into a subgenerator that catches it and raises a different exception. @@ -905,14 +906,14 @@ async def g(): ]) @_async_test - async def test_yield_from_empty(self): + async def test_yield_from_empty_ayf(self): async def g(): yield from () with self.assertRaises(StopAsyncIteration): await anext(g()) @_async_test - async def test_delegating_generators_claim_to_be_running(self): + async def test_delegating_generators_claim_to_be_running_ayf(self): # Check with basic iteration async def one(): yield 0 @@ -939,7 +940,7 @@ async def two(): self.assertEqual(res, [0, 1, 2, 3]) @_async_test - async def test_delegating_generators_claim_to_be_running_with_throw(self): + async def test_delegating_generators_claim_to_be_running_with_throw_ayf(self): # Check with throw class MyErr(Exception): pass @@ -978,7 +979,7 @@ async def two(): raise @_async_test - async def test_delegating_generators_claim_to_be_running_with_aclose(self): + async def test_delegating_generators_claim_to_be_running_with_close_ayf(self): # Check with close class MyIt: def __aiter__(self): @@ -996,7 +997,7 @@ async def one(): await g1.aclose() @_async_test - async def test_delegator_is_visible_to_debugger(self): + async def test_delegator_is_visible_to_debugger_ayf(self): async def call_stack(): return [f[3] for f in inspect.stack()] @@ -1018,7 +1019,7 @@ async def eggs(g): self.assertTrue('spam' in stack and 'eggs' in stack) @_async_test - async def test_custom_iterator_return(self): + async def test_custom_iterator_return_ayf(self): class MyIter: def __aiter__(self): return self @@ -1032,7 +1033,7 @@ async def gen(): self.assertEqual(ret, 42) @_async_test - async def test_close_with_cleared_frame(self): + async def test_close_with_cleared_frame_ayf(self): async def innermost(): yield async def inner(): @@ -1054,7 +1055,7 @@ async def outer(): gc_collect() @_async_test - async def test_send_tuple_with_custom_generator(self): + async def test_send_tuple_with_custom_generator_ayf(self): class MyGen: def __aiter__(self): return self @@ -1087,7 +1088,7 @@ def assert_generator_ignored_generator_exit(self): return self.assertRaisesRegex(RuntimeError, r"^async generator ignored GeneratorExit$") @_async_test - async def test_close_and_throw_work(self): + async def test_close_and_throw_work_ayf(self): yielded_first = object() yielded_second = object() @@ -1148,7 +1149,7 @@ async def outer(): await self.assert_stop_iteration(g) @_async_test - async def test_close_and_throw_raise_generator_exit(self): + async def test_close_and_throw_raise_generator_exit_ayf(self): yielded_first = object() yielded_second = object() @@ -1225,7 +1226,7 @@ async def outer(): await self.assert_stop_iteration(g) @_async_test - async def test_close_and_throw_raise_stop_iteration(self): + async def test_close_and_throw_raise_stop_iteration_ayf(self): yielded_first = object() yielded_second = object() @@ -1309,7 +1310,7 @@ async def outer(): await self.assert_stop_iteration(g) @_async_test - async def test_close_and_throw_raise_base_exception(self): + async def test_close_and_throw_raise_base_exception_ayf(self): yielded_first = object() yielded_second = object() @@ -1388,7 +1389,7 @@ async def outer(): await self.assert_stop_iteration(g) @_async_test - async def test_close_and_throw_raise_exception(self): + async def test_close_and_throw_raise_exception_ayf(self): yielded_first = object() yielded_second = object() @@ -1467,7 +1468,7 @@ async def outer(): await self.assert_stop_iteration(g) @_async_test - async def test_close_and_throw_yield(self): + async def test_close_and_throw_yield_ayf(self): yielded_first = object() yielded_second = object() @@ -1539,7 +1540,7 @@ async def outer(): await self.assert_stop_iteration(g) @_async_test - async def test_close_and_throw_return(self): + async def test_close_and_throw_return_ayf(self): yielded_first = object() yielded_second = object() returned = object() @@ -1606,7 +1607,7 @@ async def outer(): await self.assert_stop_iteration(g) @_async_test - async def test_throws_in_iter(self): + async def test_throws_in_iter_ayf(self): class Silly: async def __aiter__(self): yield from () @@ -1618,6 +1619,65 @@ async def my_generator(): with self.assertRaisesRegex(RuntimeError, "nobody expects the spanish inquisition"): await anext(my_generator()) + +class TestParityWithPEP380(unittest.TestCase): + """Enforce PEP 828 tests cover every PEP 380 test.""" + + def assert_parity(self, base_class, variant_class, *, suffix): + """Assert variant_class is in 1:1 parity with base_class via ``suffix``. + + Every method ``test_xxx`` on ``base_class`` must have a counterpart + ``test_xxx`` on ``variant_class`` and vice versa. Variant-only + tests belong in a separate TestCase class. + """ + base_tests = { + n for n in dir(base_class) + if n.startswith("test_") and callable(getattr(base_class, n)) + } + variant_tests = { + n for n in dir(variant_class) + if n.startswith("test_") and callable(getattr(variant_class, n)) + } + expected = {n + suffix for n in base_tests} + missing = sorted(expected - variant_tests) + extra = sorted(variant_tests - expected) + problems = [] + if missing: + problems.append( + f"{variant_class.__name__} missing variants of " + f"{base_class.__name__} tests (suffix {suffix!r}): {missing}" + ) + if extra: + problems.append( + f"{variant_class.__name__} has tests with no counterpart in " + f"{base_class.__name__} (suffix {suffix!r}): {extra}" + ) + self.assertEqual(problems, [], "\n".join(problems)) + + def test_TestPEP828Operation(self): + self.assert_parity( + test_yield_from.TestPEP380Operation, + TestPEP828Operation, + suffix="_ayf", + ) + + def test_TestInterestingEdgeCases(self): + self.assert_parity( + test_yield_from.TestInterestingEdgeCases, + TestInterestingEdgeCases, + suffix="_ayf", + ) + + +class TestPEP828Extras(unittest.TestCase): + """Tests with no PEP 380 counterpart. + + Anything added here describes behaviour specific to ``async yield from``. + Tests that have a logical equivalent in plain ``yield from`` belong in + ``TestPEP828Operation`` or ``TestInterestingEdgeCases`` and are + parity-checked against ``test_yield_from``. + """ + @_async_test async def test_delegate_exception(self): yielded_first = object() From 68e47ec567cfbdd5f2043d79be66dd19dbccf78f Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 3 May 2026 17:18:07 +0200 Subject: [PATCH 04/10] Refresh `test_async_yield_from` module docstring Note the `_ayf` naming convention, the 1:1 parity check enforced by `TestParityWithPEP380`, and where PEP 828-only tests belong. --- Lib/test/test_async_yield_from.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_async_yield_from.py b/Lib/test/test_async_yield_from.py index ee7dc7356b3363..bfa7f7c2b447af 100644 --- a/Lib/test/test_async_yield_from.py +++ b/Lib/test/test_async_yield_from.py @@ -1,8 +1,9 @@ """ -Test suite for PEP 828 implementation. +Test suite for PEP 828 (`async yield from`) -The tests below were adapted from the 'yield from' tests. -For more context on any particular test, try searching for an analogue in `test_yield_from`. +Adapted from `test_yield_from`. Each adapted test mirrors its PEP 380 +counterpart by name with an `_ayf` suffix; `TestParityWithPEP380` enforces +the 1:1 mapping. Tests with no PEP 380 analogue go in `TestPEP828Extras`. """ import unittest From 14a6a78062917bd9958f300c9a9d6502d897690f Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 3 May 2026 17:26:08 +0200 Subject: [PATCH 05/10] Use async API names in test descriptions and subTests Mirrored test method names already match their PEP 380 counterparts via the `_ayf` suffix. The descriptions and subTest labels now use the async API names (`asend`, `aclose`, `athrow`, `anext`) so it is clear which operation each test exercises. Class docstrings on `TestPEP828Operation` and `TestInterestingEdgeCases` now state that they mirror their `test_yield_from` counterparts. --- Lib/test/test_async_yield_from.py | 109 +++++++++++++++--------------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/Lib/test/test_async_yield_from.py b/Lib/test/test_async_yield_from.py index bfa7f7c2b447af..8f3fb36537d57c 100644 --- a/Lib/test/test_async_yield_from.py +++ b/Lib/test/test_async_yield_from.py @@ -17,9 +17,7 @@ class TestPEP828Operation(unittest.TestCase): - """ - Test semantics. - """ + """Test semantics. Mirrors `TestPEP380Operation` in `test_yield_from`.""" @_async_test async def test_delegation_of_initial_next_to_subgenerator_ayf(self): @@ -149,7 +147,7 @@ async def g2(): @_async_test async def test_delegation_of_send_ayf(self): """ - Test delegation of send() + Test delegation of asend() """ trace = [] async def g1(): @@ -194,7 +192,7 @@ async def g2(): @_async_test async def test_handling_exception_while_delegating_send_ayf(self): """ - Test handling exception while delegating 'send' + Test handling exception while delegating 'asend' """ trace = [] async def g1(): @@ -272,7 +270,7 @@ async def g2(): @_async_test async def test_handing_exception_while_delegating_close_ayf(self): """ - Test handling exception while delegating 'close' + Test handling exception while delegating 'aclose' """ trace = [] async def g1(): @@ -314,7 +312,7 @@ async def g2(): @_async_test async def test_delegating_throw_ayf(self): """ - Test delegating 'throw' + Test delegating 'athrow' """ trace = [] async def g1(): @@ -458,7 +456,7 @@ async def g(): @_async_test async def test_conversion_of_sendNone_to_next_ayf(self): """ - Test conversion of asend(None) to next() + Test conversion of asend(None) to anext() """ trace = [] async def g(): @@ -476,7 +474,7 @@ async def g(): @_async_test async def test_delegation_of_close_to_non_generator_ayf(self): """ - Test delegation of close() to non-generator + Test delegation of aclose() to non-generator """ trace = [] async def g(): @@ -499,7 +497,7 @@ async def g(): @_async_test async def test_delegating_throw_to_non_generator_ayf(self): """ - Test delegating 'throw' to non-generator + Test delegating 'athrow' to non-generator """ trace = [] async def g(): @@ -532,7 +530,7 @@ async def g(): @_async_test async def test_attempting_to_send_to_non_generator_ayf(self): """ - Test attempting to send to non-generator + Test attempting to asend to non-generator """ trace = [] async def g(): @@ -593,7 +591,7 @@ async def g(): @_async_test async def test_exception_in_initial_next_call_ayf(self): """ - Test exception in initial next() call + Test exception in initial anext() call """ trace = [] async def g1(): @@ -614,7 +612,7 @@ async def run(): @_async_test async def test_attempted_yield_from_loop_ayf(self): """ - Test attempted yield-from loop + Test attempted `async yield from` loop """ trace = [] async def g1(): @@ -650,7 +648,7 @@ async def g2(): @_async_test async def test_returning_value_from_delegated_throw_ayf(self): """ - Test returning value from delegated 'throw' + Test returning value from delegated 'athrow' """ trace = [] async def g1(): @@ -694,7 +692,7 @@ class LunchError(Exception): @_async_test async def test_next_and_return_with_value_ayf(self): """ - Test next and return with value + Test anext and return with value """ trace = [] async def f(r): @@ -737,7 +735,7 @@ async def g(r): @_async_test async def test_send_and_return_with_value_ayf(self): """ - Test send and return with value + Test asend and return with value """ trace = [] async def f(r): @@ -785,7 +783,7 @@ async def g(r): @_async_test async def test_catching_exception_from_subgen_and_returning_ayf(self): """ - Test catching an exception thrown into a + Test catching an exception athrown into a subgenerator and returning a value """ async def inner(): @@ -815,7 +813,7 @@ async def outer(): @_async_test async def test_throwing_GeneratorExit_into_subgen_that_returns_ayf(self): """ - Test throwing GeneratorExit into a subgenerator that + Test athrow(GeneratorExit) into a subgenerator that catches it and returns normally. """ trace = [] @@ -846,7 +844,7 @@ async def g(): @_async_test async def test_throwing_GeneratorExit_into_subgenerator_that_yields_ayf(self): """ - Test throwing GeneratorExit into a subgenerator that + Test athrow(GeneratorExit) into a subgenerator that catches it and yields. """ trace = [] @@ -877,7 +875,7 @@ async def g(): @_async_test async def test_throwing_GeneratorExit_into_subgen_that_raises_ayf(self): """ - Test throwing GeneratorExit into a subgenerator that + Test athrow(GeneratorExit) into a subgenerator that catches it and raises a different exception. """ trace = [] @@ -1075,6 +1073,7 @@ async def outer(): self.assertEqual(v, (1, 2, 3, 4)) class TestInterestingEdgeCases(unittest.TestCase): + """Interesting edge cases. Mirrors `TestInterestingEdgeCases` in `test_yield_from`.""" async def assert_stop_iteration(self, iterator): with self.assertRaises(StopAsyncIteration) as caught: @@ -1103,13 +1102,13 @@ async def inner(): async def outer(): return (async yield from inner()) - with self.subTest("close"): + with self.subTest("aclose"): g = outer() self.assertIs(await anext(g), yielded_first) await g.aclose() await self.assert_stop_iteration(g) - with self.subTest("throw GeneratorExit"): + with self.subTest("athrow GeneratorExit"): g = outer() self.assertIs(await anext(g), yielded_first) thrown = GeneratorExit() @@ -1119,7 +1118,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw StopAsyncIteration"): + with self.subTest("athrow StopAsyncIteration"): g = outer() self.assertIs(await anext(g), yielded_first) thrown = StopAsyncIteration() @@ -1129,7 +1128,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw BaseException"): + with self.subTest("athrow BaseException"): g = outer() self.assertIs(await anext(g), yielded_first) thrown = BaseException() @@ -1139,7 +1138,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw Exception"): + with self.subTest("athrow Exception"): g = outer() self.assertIs(await anext(g), yielded_first) thrown = Exception() @@ -1167,7 +1166,7 @@ async def inner(): async def outer(): return (async yield from inner()) - with self.subTest("close"): + with self.subTest("aclose"): g = outer() self.assertIs(await anext(g), yielded_first) raised = GeneratorExit() @@ -1176,7 +1175,7 @@ async def outer(): await g.aclose() await self.assert_stop_iteration(g) - with self.subTest("throw GeneratorExit"): + with self.subTest("athrow GeneratorExit"): g = outer() self.assertIs(await anext(g), yielded_first) raised = GeneratorExit() @@ -1190,7 +1189,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw StopAsyncIteration"): + with self.subTest("athrow StopAsyncIteration"): g = outer() self.assertIs(await anext(g), yielded_first) raised = GeneratorExit() @@ -1202,7 +1201,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw BaseException"): + with self.subTest("athrow BaseException"): g = outer() self.assertIs(await anext(g), yielded_first) raised = GeneratorExit() @@ -1214,7 +1213,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw Exception"): + with self.subTest("athrow Exception"): g = outer() self.assertIs(await anext(g), yielded_first) raised = GeneratorExit() @@ -1244,7 +1243,7 @@ async def inner(): async def outer(): return (async yield from inner()) - with self.subTest("close"): + with self.subTest("aclose"): g = outer() self.assertIs(await anext(g), yielded_first) raised = StopAsyncIteration() @@ -1256,7 +1255,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw GeneratorExit"): + with self.subTest("athrow GeneratorExit"): g = outer() self.assertIs(await anext(g), yielded_first) raised = StopAsyncIteration() @@ -1271,7 +1270,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw StopAsyncIteration"): + with self.subTest("athrow StopAsyncIteration"): g = outer() self.assertIs(await anext(g), yielded_first) raised = StopAsyncIteration() @@ -1284,7 +1283,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw BaseException"): + with self.subTest("athrow BaseException"): g = outer() self.assertIs(await anext(g), yielded_first) raised = StopAsyncIteration() @@ -1297,7 +1296,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw Exception"): + with self.subTest("athrow Exception"): g = outer() self.assertIs(await anext(g), yielded_first) raised = StopAsyncIteration() @@ -1328,7 +1327,7 @@ async def inner(): async def outer(): return (async yield from inner()) - with self.subTest("close"): + with self.subTest("aclose"): g = outer() self.assertIs(await anext(g), yielded_first) raised = BaseException() @@ -1339,7 +1338,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw GeneratorExit"): + with self.subTest("athrow GeneratorExit"): g = outer() self.assertIs(await anext(g), yielded_first) raised = BaseException() @@ -1353,7 +1352,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw StopAsyncIteration"): + with self.subTest("athrow StopAsyncIteration"): g = outer() self.assertIs(await anext(g), yielded_first) raised = BaseException() @@ -1365,7 +1364,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw BaseException"): + with self.subTest("athrow BaseException"): g = outer() self.assertIs(await anext(g), yielded_first) raised = BaseException() @@ -1377,7 +1376,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw Exception"): + with self.subTest("athrow Exception"): g = outer() self.assertIs(await anext(g), yielded_first) raised = BaseException() @@ -1407,7 +1406,7 @@ async def inner(): async def outer(): return (async yield from inner()) - with self.subTest("close"): + with self.subTest("aclose"): g = outer() self.assertIs(await anext(g), yielded_first) raised = Exception() @@ -1418,7 +1417,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw GeneratorExit"): + with self.subTest("athrow GeneratorExit"): g = outer() self.assertIs(await anext(g), yielded_first) raised = Exception() @@ -1432,7 +1431,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw StopAsyncIteration"): + with self.subTest("athrow StopAsyncIteration"): g = outer() self.assertIs(await anext(g), yielded_first) raised = Exception() @@ -1444,7 +1443,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw BaseException"): + with self.subTest("athrow BaseException"): g = outer() self.assertIs(await anext(g), yielded_first) raised = Exception() @@ -1456,7 +1455,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw Exception"): + with self.subTest("athrow Exception"): g = outer() self.assertIs(await anext(g), yielded_first) raised = Exception() @@ -1485,7 +1484,7 @@ async def inner(): async def outer(): return (async yield from inner()) - with self.subTest("close"): + with self.subTest("aclose"): g = outer() self.assertIs(await anext(g), yielded_first) # No chaining happens. This is analogous to PEP 342: @@ -1495,7 +1494,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw GeneratorExit"): + with self.subTest("athrow GeneratorExit"): g = outer() self.assertIs(await anext(g), yielded_first) thrown = GeneratorExit() @@ -1506,7 +1505,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw StopAsyncIteration"): + with self.subTest("athrow StopAsyncIteration"): g = outer() self.assertIs(await anext(g), yielded_first) thrown = StopAsyncIteration() @@ -1518,7 +1517,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw BaseException"): + with self.subTest("athrow BaseException"): g = outer() self.assertIs(await anext(g), yielded_first) thrown = BaseException() @@ -1529,7 +1528,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw Exception"): + with self.subTest("athrow Exception"): g = outer() self.assertIs(await anext(g), yielded_first) thrown = Exception() @@ -1557,7 +1556,7 @@ async def inner(): async def outer(): return (async yield from inner()) - with self.subTest("close"): + with self.subTest("aclose"): g = outer() self.assertIs(await anext(g), yielded_first) # StopAsyncIteration is suppressed. This is analogous to PEP 342: @@ -1565,7 +1564,7 @@ async def outer(): await g.aclose() await self.assert_stop_iteration(g) - with self.subTest("throw GeneratorExit"): + with self.subTest("athrow GeneratorExit"): g = outer() self.assertIs(await anext(g), yielded_first) thrown = GeneratorExit() @@ -1577,7 +1576,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw StopAsyncIteration"): + with self.subTest("athrow StopAsyncIteration"): g = outer() self.assertIs(await anext(g), yielded_first) thrown = StopAsyncIteration() @@ -1587,7 +1586,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw BaseException"): + with self.subTest("athrow BaseException"): g = outer() self.assertIs(await anext(g), yielded_first) thrown = BaseException() @@ -1597,7 +1596,7 @@ async def outer(): self.assertIsNone(caught.exception.__context__) await self.assert_stop_iteration(g) - with self.subTest("throw Exception"): + with self.subTest("athrow Exception"): g = outer() self.assertIs(await anext(g), yielded_first) thrown = Exception() From 35fea28f814daef41ef91aca020863b0086e9f72 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 3 May 2026 17:31:03 +0200 Subject: [PATCH 06/10] Simplify cross-reference to `test_async_yield_from` The previous wording instructed maintainers to keep the two suites in sync manually; that contract is now enforced by `TestParityWithPEP380` on the async side, so the cross-reference can stay minimal. --- Lib/test/test_yield_from.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_yield_from.py b/Lib/test/test_yield_from.py index 19aa89db1d0843..56d6a1c8d29480 100644 --- a/Lib/test/test_yield_from.py +++ b/Lib/test/test_yield_from.py @@ -6,7 +6,7 @@ adapted from original tests written by Greg Ewing see -See also `test_async_yield_from'. Where applicable, consider keeping the two test suites in sync. +See also `test_async_yield_from'. """ import unittest From e95df7d1c2ee01d8eb2119a2d243df0dbace8cbe Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 3 May 2026 17:32:15 +0200 Subject: [PATCH 07/10] Use the `sentinel` builtin for opaque test markers The `yielded_first` / `yielded_second` / `returned` placeholders were created with `object()`, which renders as a generic repr in failure output. Switching to `sentinel(...)` keeps the same identity semantics while giving each marker a meaningful repr. --- Lib/test/test_async_yield_from.py | 48 +++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Lib/test/test_async_yield_from.py b/Lib/test/test_async_yield_from.py index 8f3fb36537d57c..100188b0d4b199 100644 --- a/Lib/test/test_async_yield_from.py +++ b/Lib/test/test_async_yield_from.py @@ -1090,9 +1090,9 @@ def assert_generator_ignored_generator_exit(self): @_async_test async def test_close_and_throw_work_ayf(self): - yielded_first = object() - yielded_second = object() - returned = object() + yielded_first = sentinel("yielded_first") + yielded_second = sentinel("yielded_second") + returned = sentinel("returned") async def inner(): yield yielded_first @@ -1151,9 +1151,9 @@ async def outer(): @_async_test async def test_close_and_throw_raise_generator_exit_ayf(self): - yielded_first = object() - yielded_second = object() - returned = object() + yielded_first = sentinel("yielded_first") + yielded_second = sentinel("yielded_second") + returned = sentinel("returned") async def inner(): try: @@ -1228,9 +1228,9 @@ async def outer(): @_async_test async def test_close_and_throw_raise_stop_iteration_ayf(self): - yielded_first = object() - yielded_second = object() - returned = object() + yielded_first = sentinel("yielded_first") + yielded_second = sentinel("yielded_second") + returned = sentinel("returned") async def inner(): try: @@ -1312,9 +1312,9 @@ async def outer(): @_async_test async def test_close_and_throw_raise_base_exception_ayf(self): - yielded_first = object() - yielded_second = object() - returned = object() + yielded_first = sentinel("yielded_first") + yielded_second = sentinel("yielded_second") + returned = sentinel("returned") async def inner(): try: @@ -1391,9 +1391,9 @@ async def outer(): @_async_test async def test_close_and_throw_raise_exception_ayf(self): - yielded_first = object() - yielded_second = object() - returned = object() + yielded_first = sentinel("yielded_first") + yielded_second = sentinel("yielded_second") + returned = sentinel("returned") async def inner(): try: @@ -1470,9 +1470,9 @@ async def outer(): @_async_test async def test_close_and_throw_yield_ayf(self): - yielded_first = object() - yielded_second = object() - returned = object() + yielded_first = sentinel("yielded_first") + yielded_second = sentinel("yielded_second") + returned = sentinel("returned") async def inner(): try: @@ -1541,9 +1541,9 @@ async def outer(): @_async_test async def test_close_and_throw_return_ayf(self): - yielded_first = object() - yielded_second = object() - returned = object() + yielded_first = sentinel("yielded_first") + yielded_second = sentinel("yielded_second") + returned = sentinel("returned") async def inner(): try: @@ -1680,9 +1680,9 @@ class TestPEP828Extras(unittest.TestCase): @_async_test async def test_delegate_exception(self): - yielded_first = object() - yielded_second = object() - returned = object() + yielded_first = sentinel("yielded_first") + yielded_second = sentinel("yielded_second") + returned = sentinel("returned") async def inner(): try: From 4da6ce83ea3200933a983b5dcaa898b2933655bd Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 3 May 2026 17:37:27 +0200 Subject: [PATCH 08/10] Make parity-failure output more obvious Print each offender on its own line, labelled with which side is missing the counterpart, instead of dumping two summary sentences with embedded list reprs. Factor the duplicated set-comprehension into a small inner helper so the assertion body is also a touch shorter. Sample failure (deleting one mirrored test, adding two extras): TestPEP828Operation is not a 1:1 mirror of TestPEP380Operation (suffix '_ayf'): missing in TestPEP828Operation: test_yield_from_empty_ayf no counterpart in TestPEP380Operation: test_made_up_extra_ayf no counterpart in TestPEP380Operation: test_other_extra_ayf --- Lib/test/test_async_yield_from.py | 41 ++++++++++++++----------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/Lib/test/test_async_yield_from.py b/Lib/test/test_async_yield_from.py index 100188b0d4b199..0e1d86936f087b 100644 --- a/Lib/test/test_async_yield_from.py +++ b/Lib/test/test_async_yield_from.py @@ -1630,29 +1630,24 @@ def assert_parity(self, base_class, variant_class, *, suffix): ``test_xxx`` on ``variant_class`` and vice versa. Variant-only tests belong in a separate TestCase class. """ - base_tests = { - n for n in dir(base_class) - if n.startswith("test_") and callable(getattr(base_class, n)) - } - variant_tests = { - n for n in dir(variant_class) - if n.startswith("test_") and callable(getattr(variant_class, n)) - } - expected = {n + suffix for n in base_tests} - missing = sorted(expected - variant_tests) - extra = sorted(variant_tests - expected) - problems = [] - if missing: - problems.append( - f"{variant_class.__name__} missing variants of " - f"{base_class.__name__} tests (suffix {suffix!r}): {missing}" - ) - if extra: - problems.append( - f"{variant_class.__name__} has tests with no counterpart in " - f"{base_class.__name__} (suffix {suffix!r}): {extra}" - ) - self.assertEqual(problems, [], "\n".join(problems)) + def test_methods(cls): + return {n for n in dir(cls) + if n.startswith("test_") and callable(getattr(cls, n))} + + expected = {n + suffix for n in test_methods(base_class)} + actual = test_methods(variant_class) + missing = sorted(expected - actual) + extra = sorted(actual - expected) + if missing or extra: + lines = [ + f"{variant_class.__name__} is not a 1:1 mirror of " + f"{base_class.__name__} (suffix {suffix!r}):" + ] + for name in missing: + lines.append(f" missing in {variant_class.__name__}: {name}") + for name in extra: + lines.append(f" no counterpart in {base_class.__name__}: {name}") + self.fail("\n".join(lines)) def test_TestPEP828Operation(self): self.assert_parity( From 43b6f8931b9b729a47584822772042b10aa814b8 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 3 May 2026 17:40:05 +0200 Subject: [PATCH 09/10] Remove callable check --- Lib/test/test_async_yield_from.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_async_yield_from.py b/Lib/test/test_async_yield_from.py index 0e1d86936f087b..1b7ff8ebc874cb 100644 --- a/Lib/test/test_async_yield_from.py +++ b/Lib/test/test_async_yield_from.py @@ -1631,8 +1631,7 @@ def assert_parity(self, base_class, variant_class, *, suffix): tests belong in a separate TestCase class. """ def test_methods(cls): - return {n for n in dir(cls) - if n.startswith("test_") and callable(getattr(cls, n))} + return {n for n in dir(cls) if n.startswith("test_")} expected = {n + suffix for n in test_methods(base_class)} actual = test_methods(variant_class) From b0355f969f918b3044c043dd3b46f8e8e82cec82 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 3 May 2026 17:41:28 +0200 Subject: [PATCH 10/10] Use fully qualified class names in parity-failure output When the mirrored classes share an unqualified name (e.g. both PEP 380 and PEP 828 have a `TestInterestingEdgeCases`), the failure message talked about the class on both sides as if it were the same class. Use `module.qualname` so each side is unambiguous. --- Lib/test/test_async_yield_from.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_async_yield_from.py b/Lib/test/test_async_yield_from.py index 1b7ff8ebc874cb..a9d1d643427880 100644 --- a/Lib/test/test_async_yield_from.py +++ b/Lib/test/test_async_yield_from.py @@ -1633,19 +1633,22 @@ def assert_parity(self, base_class, variant_class, *, suffix): def test_methods(cls): return {n for n in dir(cls) if n.startswith("test_")} + def fqn(cls): + return f"{cls.__module__}.{cls.__qualname__}" + expected = {n + suffix for n in test_methods(base_class)} actual = test_methods(variant_class) missing = sorted(expected - actual) extra = sorted(actual - expected) if missing or extra: lines = [ - f"{variant_class.__name__} is not a 1:1 mirror of " - f"{base_class.__name__} (suffix {suffix!r}):" + f"{fqn(variant_class)} is not a 1:1 mirror of " + f"{fqn(base_class)} (suffix {suffix!r}):" ] for name in missing: - lines.append(f" missing in {variant_class.__name__}: {name}") + lines.append(f" missing in {fqn(variant_class)}: {name}") for name in extra: - lines.append(f" no counterpart in {base_class.__name__}: {name}") + lines.append(f" no counterpart in {fqn(base_class)}: {name}") self.fail("\n".join(lines)) def test_TestPEP828Operation(self):