From be9e5cb3c7e97e6050ea4a66aca0c0c3c44632ca Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sat, 5 Sep 2026 17:31:57 +0300 Subject: [PATCH 1/4] gh-156980: Fix asyncio call graph truncation at async generators --- Doc/builtins/stdtypes.rst | 8 +++ Lib/asyncio/graph.py | 3 + Lib/test/test_asyncgen.py | 17 +++++ Lib/test/test_asyncio/test_graph.py | 71 +++++++++++++++++++ ...-09-05-17-29-50.gh-issue-156980.W6kHva.rst | 2 + Objects/genobject.c | 14 +++- 6 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-05-17-29-50.gh-issue-156980.W6kHva.rst diff --git a/Doc/builtins/stdtypes.rst b/Doc/builtins/stdtypes.rst index cf05f947c5908df..2c576c50ffd6506 100644 --- a/Doc/builtins/stdtypes.rst +++ b/Doc/builtins/stdtypes.rst @@ -1238,6 +1238,14 @@ the underlying generator function: that does nothing. +The awaitables returned by :meth:`~agen.asend`, :meth:`~agen.athrow` and +:meth:`~agen.aclose` expose the generator they drive as a read-only +``ag_gen`` attribute. + +.. versionadded:: 3.16 + The ``ag_gen`` attribute. + + .. _typesseq: Sequence Types --- :class:`list`, :class:`tuple`, :class:`range` diff --git a/Lib/asyncio/graph.py b/Lib/asyncio/graph.py index b5c9dafe8ed50a1..4abea67ebaa1366 100644 --- a/Lib/asyncio/graph.py +++ b/Lib/asyncio/graph.py @@ -70,6 +70,9 @@ def _build_graph_for_future( if coro.gi_frame is not None: st.append(FrameCallGraphEntry(coro.gi_frame)) coro = coro.gi_yieldfrom + elif hasattr(coro, 'ag_gen'): + # gh-156980: An asend()/athrow() awaitable: step over it to its generator + coro = coro.ag_gen else: break diff --git a/Lib/test/test_asyncgen.py b/Lib/test/test_asyncgen.py index b5e0891feb794e8..5161ec43d892bdd 100644 --- a/Lib/test/test_asyncgen.py +++ b/Lib/test/test_asyncgen.py @@ -399,6 +399,23 @@ async def gen(): r"cannot reuse already awaited __anext__\(\)/asend\(\)"): an.send(None) + def test_async_gen_asend_athrow_ag_gen(self): + # gh-156980: asend()/athrow() awaitables expose the generator they drive + async def gen(): + yield 1 + + g = gen() + asend = g.asend(None) + athrow = g.athrow(ValueError) + try: + self.assertIs(asend.ag_gen, g) + self.assertIs(athrow.ag_gen, g) + with self.assertRaises(AttributeError): + asend.ag_gen = None + finally: + asend.close() + athrow.close() + def test_async_gen_asend_throw_concurrent_with_send(self): import types diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index e438e5915b2a852..06dc7a2efa90175 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -430,6 +430,77 @@ async def probe(t): ['T', ['a _cancel_and_wait', 'a wait_for', 'a probe'], []], ]) + async def test_stack_async_gen_asend(self): + # gh-156980: an async_generator_asend must not truncate the stack + fut = asyncio.Future() + stack_for_consumer = None + + async def deep(): + await fut + + async def gen(): + await deep() + yield 1 + + async def consumer(): + async for _ in gen(): + pass + + async def main(): + nonlocal stack_for_consumer + + async with asyncio.TaskGroup() as g: + t = g.create_task(consumer(), name='consumer') + for _ in range(5): + await asyncio.sleep(0) + + stack_for_consumer = capture_test_stack(fut=t) + fut.set_result(None) + + await main() + + self.assertEqual(stack_for_consumer[0][:2], [ + 'T', + ['a deep', 'ag gen', 'a consumer'], + ]) + + async def test_stack_async_gen_aclose(self): + # gh-156980: an async_generator_athrow must not truncate the stack + fut = asyncio.Future() + stack_for_consumer = None + + async def deep(): + await fut + + async def gen(): + try: + yield 1 + finally: + await deep() + + async def consumer(): + agen = gen() + await anext(agen) + await agen.aclose() + + async def main(): + nonlocal stack_for_consumer + + async with asyncio.TaskGroup() as g: + t = g.create_task(consumer(), name='consumer') + for _ in range(5): + await asyncio.sleep(0) + + stack_for_consumer = capture_test_stack(fut=t) + fut.set_result(None) + + await main() + + self.assertEqual(stack_for_consumer[0][:2], [ + 'T', + ['a deep', 'ag gen', 'a consumer'], + ]) + async def test_stack_gather(self): stack_for_deep = None diff --git a/Misc/NEWS.d/next/Library/2026-09-05-17-29-50.gh-issue-156980.W6kHva.rst b/Misc/NEWS.d/next/Library/2026-09-05-17-29-50.gh-issue-156980.W6kHva.rst new file mode 100644 index 000000000000000..701e42548d5d269 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-05-17-29-50.gh-issue-156980.W6kHva.rst @@ -0,0 +1,2 @@ +:func:`asyncio.print_call_graph` no longer truncates the call stack of a +task suspended inside an asynchronous generator. diff --git a/Objects/genobject.c b/Objects/genobject.c index 1c95a569af8b9b5..0ead7abd23c5d7f 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -2194,6 +2194,11 @@ async_gen_asend_finalize(PyObject *self) } } +static PyMemberDef async_gen_asend_memberlist[] = { + {"ag_gen", Py_T_OBJECT_EX, offsetof(PyAsyncGenASend, ags_gen), Py_READONLY}, + {NULL} /* Sentinel */ +}; + static PyMethodDef async_gen_asend_methods[] = { {"send", async_gen_asend_send, METH_O, send_doc}, {"throw", _PyCFunction_CAST(async_gen_asend_throw), METH_FASTCALL, throw_doc}, @@ -2238,7 +2243,7 @@ PyTypeObject _PyAsyncGenASend_Type = { PyObject_SelfIter, /* tp_iter */ async_gen_asend_iternext, /* tp_iternext */ async_gen_asend_methods, /* tp_methods */ - 0, /* tp_members */ + async_gen_asend_memberlist, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ @@ -2657,6 +2662,11 @@ async_gen_athrow_finalize(PyObject *op) } } +static PyMemberDef async_gen_athrow_memberlist[] = { + {"ag_gen", Py_T_OBJECT_EX, offsetof(PyAsyncGenAThrow, agt_gen), Py_READONLY}, + {NULL} /* Sentinel */ +}; + static PyMethodDef async_gen_athrow_methods[] = { {"send", async_gen_athrow_send, METH_O, send_doc}, {"throw", _PyCFunction_CAST(async_gen_athrow_throw), @@ -2704,7 +2714,7 @@ PyTypeObject _PyAsyncGenAThrow_Type = { PyObject_SelfIter, /* tp_iter */ async_gen_athrow_iternext, /* tp_iternext */ async_gen_athrow_methods, /* tp_methods */ - 0, /* tp_members */ + async_gen_athrow_memberlist, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ From eb30c4ecc1eea842351ab5c2fd70cda0512c8bf6 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 28 Oct 2026 10:48:10 -0700 Subject: [PATCH 2/4] Move docs from stdtypes to inspect --- Doc/builtins/stdtypes.rst | 8 -------- Doc/library/inspect.rst | 11 +++++++++++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Doc/builtins/stdtypes.rst b/Doc/builtins/stdtypes.rst index 2c576c50ffd6506..cf05f947c5908df 100644 --- a/Doc/builtins/stdtypes.rst +++ b/Doc/builtins/stdtypes.rst @@ -1238,14 +1238,6 @@ the underlying generator function: that does nothing. -The awaitables returned by :meth:`~agen.asend`, :meth:`~agen.athrow` and -:meth:`~agen.aclose` expose the generator they drive as a read-only -``ag_gen`` attribute. - -.. versionadded:: 3.16 - The ``ag_gen`` attribute. - - .. _typesseq: Sequence Types --- :class:`list`, :class:`tuple`, :class:`range` diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 29bdae28b1fe268..24a65ddd4a42925 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -284,6 +284,12 @@ attributes (see :ref:`import-mod-attrs` for module attributes): | | | ``AGEN_SUSPENDED``, or | | | | ``AGEN_CLOSED`` | +-----------------+-------------------+---------------------------+ +| async generator | ag_gen | async generator driven by | +| awaitable | | the awaitable returned | +| | | from ``asend()``, | +| | | ``athrow()``, or | +| | | ``aclose()`` | ++-----------------+-------------------+---------------------------+ | coroutine | __name__ | name | +-----------------+-------------------+---------------------------+ | | __qualname__ | qualified name | @@ -360,6 +366,11 @@ attributes (see :ref:`import-mod-attrs` for module attributes): Add ``gi_state`` attribute to generators, ``cr_state`` attribute to coroutines, and ``ag_state`` attribute to async generators. +.. versionchanged:: 3.16 + + Add ``ag_gen`` attribute to the awaitables returned by the ``asend()``, + ``athrow()`` and ``aclose()`` methods of async generators. + .. function:: getmembers(object[, predicate]) Return all the members of an object in a list of ``(name, value)`` From 9a87cdf5fcbfda365b982f6ad74df2f09ec8734e Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 28 Oct 2026 10:49:31 -0700 Subject: [PATCH 3/4] rename ag_gen to aga_gen --- Doc/library/inspect.rst | 4 ++-- Lib/asyncio/graph.py | 4 ++-- Lib/test/test_asyncgen.py | 8 ++++---- Objects/genobject.c | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 24a65ddd4a42925..51c465e8f00b5ee 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -284,7 +284,7 @@ attributes (see :ref:`import-mod-attrs` for module attributes): | | | ``AGEN_SUSPENDED``, or | | | | ``AGEN_CLOSED`` | +-----------------+-------------------+---------------------------+ -| async generator | ag_gen | async generator driven by | +| async generator | aga_gen | async generator driven by | | awaitable | | the awaitable returned | | | | from ``asend()``, | | | | ``athrow()``, or | @@ -368,7 +368,7 @@ attributes (see :ref:`import-mod-attrs` for module attributes): .. versionchanged:: 3.16 - Add ``ag_gen`` attribute to the awaitables returned by the ``asend()``, + Add ``aga_gen`` attribute to the awaitables returned by the ``asend()``, ``athrow()`` and ``aclose()`` methods of async generators. .. function:: getmembers(object[, predicate]) diff --git a/Lib/asyncio/graph.py b/Lib/asyncio/graph.py index 4abea67ebaa1366..9d69cb2e74c310f 100644 --- a/Lib/asyncio/graph.py +++ b/Lib/asyncio/graph.py @@ -70,9 +70,9 @@ def _build_graph_for_future( if coro.gi_frame is not None: st.append(FrameCallGraphEntry(coro.gi_frame)) coro = coro.gi_yieldfrom - elif hasattr(coro, 'ag_gen'): + elif hasattr(coro, 'aga_gen'): # gh-156980: An asend()/athrow() awaitable: step over it to its generator - coro = coro.ag_gen + coro = coro.aga_gen else: break diff --git a/Lib/test/test_asyncgen.py b/Lib/test/test_asyncgen.py index 5161ec43d892bdd..622b290456b88f8 100644 --- a/Lib/test/test_asyncgen.py +++ b/Lib/test/test_asyncgen.py @@ -399,7 +399,7 @@ async def gen(): r"cannot reuse already awaited __anext__\(\)/asend\(\)"): an.send(None) - def test_async_gen_asend_athrow_ag_gen(self): + def test_async_gen_asend_athrow_aga_gen(self): # gh-156980: asend()/athrow() awaitables expose the generator they drive async def gen(): yield 1 @@ -408,10 +408,10 @@ async def gen(): asend = g.asend(None) athrow = g.athrow(ValueError) try: - self.assertIs(asend.ag_gen, g) - self.assertIs(athrow.ag_gen, g) + self.assertIs(asend.aga_gen, g) + self.assertIs(athrow.aga_gen, g) with self.assertRaises(AttributeError): - asend.ag_gen = None + asend.aga_gen = None finally: asend.close() athrow.close() diff --git a/Objects/genobject.c b/Objects/genobject.c index 0ead7abd23c5d7f..82a1bb1ff420682 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -2195,7 +2195,7 @@ async_gen_asend_finalize(PyObject *self) } static PyMemberDef async_gen_asend_memberlist[] = { - {"ag_gen", Py_T_OBJECT_EX, offsetof(PyAsyncGenASend, ags_gen), Py_READONLY}, + {"aga_gen", Py_T_OBJECT_EX, offsetof(PyAsyncGenASend, ags_gen), Py_READONLY}, {NULL} /* Sentinel */ }; @@ -2663,7 +2663,7 @@ async_gen_athrow_finalize(PyObject *op) } static PyMemberDef async_gen_athrow_memberlist[] = { - {"ag_gen", Py_T_OBJECT_EX, offsetof(PyAsyncGenAThrow, agt_gen), Py_READONLY}, + {"aga_gen", Py_T_OBJECT_EX, offsetof(PyAsyncGenAThrow, agt_gen), Py_READONLY}, {NULL} /* Sentinel */ }; From 09be55058236394a9c198c5316dc26a602c7aa8b Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 28 Oct 2026 10:51:07 -0700 Subject: [PATCH 4/4] adjust the gh issue ref --- Lib/asyncio/graph.py | 2 +- Lib/test/test_asyncgen.py | 2 +- Lib/test/test_asyncio/test_graph.py | 4 ++-- ...Hva.rst => 2026-09-05-17-29-50.gh-issue-160837.W6kHva.rst} | 0 4 files changed, 4 insertions(+), 4 deletions(-) rename Misc/NEWS.d/next/Library/{2026-09-05-17-29-50.gh-issue-156980.W6kHva.rst => 2026-09-05-17-29-50.gh-issue-160837.W6kHva.rst} (100%) diff --git a/Lib/asyncio/graph.py b/Lib/asyncio/graph.py index 9d69cb2e74c310f..ce23339782c16e8 100644 --- a/Lib/asyncio/graph.py +++ b/Lib/asyncio/graph.py @@ -71,7 +71,7 @@ def _build_graph_for_future( st.append(FrameCallGraphEntry(coro.gi_frame)) coro = coro.gi_yieldfrom elif hasattr(coro, 'aga_gen'): - # gh-156980: An asend()/athrow() awaitable: step over it to its generator + # An asend()/athrow() awaitable: step over it to its generator coro = coro.aga_gen else: break diff --git a/Lib/test/test_asyncgen.py b/Lib/test/test_asyncgen.py index 622b290456b88f8..47a091cb6e47a7e 100644 --- a/Lib/test/test_asyncgen.py +++ b/Lib/test/test_asyncgen.py @@ -400,7 +400,7 @@ async def gen(): an.send(None) def test_async_gen_asend_athrow_aga_gen(self): - # gh-156980: asend()/athrow() awaitables expose the generator they drive + # gh-160837: asend()/athrow() awaitables expose the generator they drive async def gen(): yield 1 diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index 06dc7a2efa90175..71853ab642f451f 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -431,7 +431,7 @@ async def probe(t): ]) async def test_stack_async_gen_asend(self): - # gh-156980: an async_generator_asend must not truncate the stack + # gh-160837: an async_generator_asend must not truncate the stack fut = asyncio.Future() stack_for_consumer = None @@ -465,7 +465,7 @@ async def main(): ]) async def test_stack_async_gen_aclose(self): - # gh-156980: an async_generator_athrow must not truncate the stack + # gh-160837: an async_generator_athrow must not truncate the stack fut = asyncio.Future() stack_for_consumer = None diff --git a/Misc/NEWS.d/next/Library/2026-09-05-17-29-50.gh-issue-156980.W6kHva.rst b/Misc/NEWS.d/next/Library/2026-09-05-17-29-50.gh-issue-160837.W6kHva.rst similarity index 100% rename from Misc/NEWS.d/next/Library/2026-09-05-17-29-50.gh-issue-156980.W6kHva.rst rename to Misc/NEWS.d/next/Library/2026-09-05-17-29-50.gh-issue-160837.W6kHva.rst