Skip to content

Commit be9e5cb

Browse files
deadlovelllmsullivan
authored andcommitted
gh-156980: Fix asyncio call graph truncation at async generators
1 parent 19270d3 commit be9e5cb

6 files changed

Lines changed: 113 additions & 2 deletions

File tree

‎Doc/builtins/stdtypes.rst‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,14 @@ the underlying generator function:
12381238
that does nothing.
12391239

12401240

1241+
The awaitables returned by :meth:`~agen.asend`, :meth:`~agen.athrow` and
1242+
:meth:`~agen.aclose` expose the generator they drive as a read-only
1243+
``ag_gen`` attribute.
1244+
1245+
.. versionadded:: 3.16
1246+
The ``ag_gen`` attribute.
1247+
1248+
12411249
.. _typesseq:
12421250

12431251
Sequence Types --- :class:`list`, :class:`tuple`, :class:`range`

‎Lib/asyncio/graph.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ def _build_graph_for_future(
7070
if coro.gi_frame is not None:
7171
st.append(FrameCallGraphEntry(coro.gi_frame))
7272
coro = coro.gi_yieldfrom
73+
elif hasattr(coro, 'ag_gen'):
74+
# gh-156980: An asend()/athrow() awaitable: step over it to its generator
75+
coro = coro.ag_gen
7376
else:
7477
break
7578

‎Lib/test/test_asyncgen.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,23 @@ async def gen():
399399
r"cannot reuse already awaited __anext__\(\)/asend\(\)"):
400400
an.send(None)
401401

402+
def test_async_gen_asend_athrow_ag_gen(self):
403+
# gh-156980: asend()/athrow() awaitables expose the generator they drive
404+
async def gen():
405+
yield 1
406+
407+
g = gen()
408+
asend = g.asend(None)
409+
athrow = g.athrow(ValueError)
410+
try:
411+
self.assertIs(asend.ag_gen, g)
412+
self.assertIs(athrow.ag_gen, g)
413+
with self.assertRaises(AttributeError):
414+
asend.ag_gen = None
415+
finally:
416+
asend.close()
417+
athrow.close()
418+
402419
def test_async_gen_asend_throw_concurrent_with_send(self):
403420
import types
404421

‎Lib/test/test_asyncio/test_graph.py‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,77 @@ async def probe(t):
430430
['T<probe>', ['a _cancel_and_wait', 'a wait_for', 'a probe'], []],
431431
])
432432

433+
async def test_stack_async_gen_asend(self):
434+
# gh-156980: an async_generator_asend must not truncate the stack
435+
fut = asyncio.Future()
436+
stack_for_consumer = None
437+
438+
async def deep():
439+
await fut
440+
441+
async def gen():
442+
await deep()
443+
yield 1
444+
445+
async def consumer():
446+
async for _ in gen():
447+
pass
448+
449+
async def main():
450+
nonlocal stack_for_consumer
451+
452+
async with asyncio.TaskGroup() as g:
453+
t = g.create_task(consumer(), name='consumer')
454+
for _ in range(5):
455+
await asyncio.sleep(0)
456+
457+
stack_for_consumer = capture_test_stack(fut=t)
458+
fut.set_result(None)
459+
460+
await main()
461+
462+
self.assertEqual(stack_for_consumer[0][:2], [
463+
'T<consumer>',
464+
['a deep', 'ag gen', 'a consumer'],
465+
])
466+
467+
async def test_stack_async_gen_aclose(self):
468+
# gh-156980: an async_generator_athrow must not truncate the stack
469+
fut = asyncio.Future()
470+
stack_for_consumer = None
471+
472+
async def deep():
473+
await fut
474+
475+
async def gen():
476+
try:
477+
yield 1
478+
finally:
479+
await deep()
480+
481+
async def consumer():
482+
agen = gen()
483+
await anext(agen)
484+
await agen.aclose()
485+
486+
async def main():
487+
nonlocal stack_for_consumer
488+
489+
async with asyncio.TaskGroup() as g:
490+
t = g.create_task(consumer(), name='consumer')
491+
for _ in range(5):
492+
await asyncio.sleep(0)
493+
494+
stack_for_consumer = capture_test_stack(fut=t)
495+
fut.set_result(None)
496+
497+
await main()
498+
499+
self.assertEqual(stack_for_consumer[0][:2], [
500+
'T<consumer>',
501+
['a deep', 'ag gen', 'a consumer'],
502+
])
503+
433504
async def test_stack_gather(self):
434505

435506
stack_for_deep = None
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:func:`asyncio.print_call_graph` no longer truncates the call stack of a
2+
task suspended inside an asynchronous generator.

‎Objects/genobject.c‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2194,6 +2194,11 @@ async_gen_asend_finalize(PyObject *self)
21942194
}
21952195
}
21962196

2197+
static PyMemberDef async_gen_asend_memberlist[] = {
2198+
{"ag_gen", Py_T_OBJECT_EX, offsetof(PyAsyncGenASend, ags_gen), Py_READONLY},
2199+
{NULL} /* Sentinel */
2200+
};
2201+
21972202
static PyMethodDef async_gen_asend_methods[] = {
21982203
{"send", async_gen_asend_send, METH_O, send_doc},
21992204
{"throw", _PyCFunction_CAST(async_gen_asend_throw), METH_FASTCALL, throw_doc},
@@ -2238,7 +2243,7 @@ PyTypeObject _PyAsyncGenASend_Type = {
22382243
PyObject_SelfIter, /* tp_iter */
22392244
async_gen_asend_iternext, /* tp_iternext */
22402245
async_gen_asend_methods, /* tp_methods */
2241-
0, /* tp_members */
2246+
async_gen_asend_memberlist, /* tp_members */
22422247
0, /* tp_getset */
22432248
0, /* tp_base */
22442249
0, /* tp_dict */
@@ -2657,6 +2662,11 @@ async_gen_athrow_finalize(PyObject *op)
26572662
}
26582663
}
26592664

2665+
static PyMemberDef async_gen_athrow_memberlist[] = {
2666+
{"ag_gen", Py_T_OBJECT_EX, offsetof(PyAsyncGenAThrow, agt_gen), Py_READONLY},
2667+
{NULL} /* Sentinel */
2668+
};
2669+
26602670
static PyMethodDef async_gen_athrow_methods[] = {
26612671
{"send", async_gen_athrow_send, METH_O, send_doc},
26622672
{"throw", _PyCFunction_CAST(async_gen_athrow_throw),
@@ -2704,7 +2714,7 @@ PyTypeObject _PyAsyncGenAThrow_Type = {
27042714
PyObject_SelfIter, /* tp_iter */
27052715
async_gen_athrow_iternext, /* tp_iternext */
27062716
async_gen_athrow_methods, /* tp_methods */
2707-
0, /* tp_members */
2717+
async_gen_athrow_memberlist, /* tp_members */
27082718
0, /* tp_getset */
27092719
0, /* tp_base */
27102720
0, /* tp_dict */

0 commit comments

Comments
 (0)