Skip to content

Commit c79146a

Browse files
gh-160804: Support generator based awaitables in asyncio introspection (#160805)
1 parent e354246 commit c79146a

5 files changed

Lines changed: 133 additions & 3 deletions

File tree

‎Lib/asyncio/graph.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ def _build_graph_for_future(
6565
# A native async generator or duck-type compatible iterator
6666
st.append(FrameCallGraphEntry(coro.ag_frame))
6767
coro = coro.ag_await
68+
elif hasattr(coro, 'gi_yieldfrom'):
69+
# A generator, e.g. returned by __await__ or @types.coroutine
70+
if coro.gi_frame is not None:
71+
st.append(FrameCallGraphEntry(coro.gi_frame))
72+
coro = coro.gi_yieldfrom
6873
else:
6974
break
7075

‎Lib/test/test_asyncio/test_graph.py‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import io
33
import sys
4+
import types
45
import unittest
56
from unittest import mock
67

@@ -11,6 +12,7 @@ def tearDownModule():
1112

1213

1314
def capture_test_stack(*, fut=None, depth=1):
15+
future_await_code = asyncio.futures._PyFuture.__await__.__code__
1416

1517
def walk(s):
1618
ret = [
@@ -27,8 +29,12 @@ def walk(s):
2729
f"a {entry.frame.f_generator.cr_code.co_name}"
2830
if hasattr(entry.frame.f_generator, 'cr_code') else
2931
f"ag {entry.frame.f_generator.ag_code.co_name}"
32+
if hasattr(entry.frame.f_generator, 'ag_code') else
33+
f"g {entry.frame.f_generator.gi_code.co_name}"
3034
)
3135
) for entry in s.call_stack
36+
# Skip the pure Python Future.__await__ generator frame
37+
if entry.frame.f_code is not future_await_code
3238
]
3339
)
3440

@@ -305,6 +311,73 @@ async def main():
305311
[]
306312
])
307313

314+
async def test_stack_await_generator(self):
315+
# The call graph of a suspended task sees through generators
316+
# returned by __await__() and the generators they yield from.
317+
318+
loop = asyncio.get_running_loop()
319+
blocker = loop.create_future()
320+
321+
def wait():
322+
yield from blocker
323+
324+
class Awaitable:
325+
def __await__(self):
326+
return (yield from wait())
327+
328+
async def main():
329+
await Awaitable()
330+
331+
task = asyncio.create_task(main(), name='await task')
332+
await asyncio.sleep(0)
333+
try:
334+
stack = capture_test_stack(fut=task)
335+
finally:
336+
blocker.set_result(None)
337+
await task
338+
339+
self.assertEqual(stack[0], [
340+
'T<await task>',
341+
[
342+
'g wait',
343+
'g __await__',
344+
'a main',
345+
],
346+
[]
347+
])
348+
349+
async def test_stack_types_coroutine(self):
350+
loop = asyncio.get_running_loop()
351+
blocker = loop.create_future()
352+
353+
async def inner():
354+
await blocker
355+
356+
@types.coroutine
357+
def legacy():
358+
yield from inner()
359+
360+
async def main():
361+
await legacy()
362+
363+
task = asyncio.create_task(main(), name='legacy task')
364+
await asyncio.sleep(0)
365+
try:
366+
stack = capture_test_stack(fut=task)
367+
finally:
368+
blocker.set_result(None)
369+
await task
370+
371+
self.assertEqual(stack[0], [
372+
'T<legacy task>',
373+
[
374+
'a inner',
375+
'g legacy',
376+
'a main',
377+
],
378+
[]
379+
])
380+
308381
def test_ag_frame_used_for_async_generator(self):
309382
# Regression test for gh-148736: the ag_await branch of
310383
# _build_graph_for_future must read ag_frame, not cr_frame.

‎Lib/test/test_external_inspection.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import socket
99
import threading
1010
import time
11+
import types
1112
from contextlib import contextmanager
1213
from asyncio import staggered, taskgroups, base_events, tasks
1314
from unittest.mock import ANY
@@ -586,6 +587,54 @@ async def main():
586587
stacks = self._extract_coroutine_funcnames(asyncio.run(main()))
587588
self.assertEqual(stacks["anext"], ["agen"])
588589

590+
@skip_if_not_supported
591+
def test_generator_awaitable_stack_is_not_truncated(self):
592+
def wait():
593+
yield from asyncio.get_running_loop().create_future()
594+
595+
class Awaitable:
596+
def __await__(self):
597+
return (yield from wait())
598+
599+
async def inner():
600+
await asyncio.sleep(10_000)
601+
602+
@types.coroutine
603+
def legacy():
604+
yield from inner()
605+
606+
async def consumer(aw):
607+
await aw
608+
609+
async def main():
610+
await_task = asyncio.create_task(
611+
consumer(Awaitable()), name="__await__")
612+
legacy_task = asyncio.create_task(
613+
consumer(legacy()), name="types.coroutine")
614+
await asyncio.sleep(0)
615+
return RemoteUnwinder(os.getpid()).get_all_awaited_by()
616+
617+
stacks = self._extract_coroutine_funcnames(asyncio.run(main()))
618+
self.assertEqual(stacks["__await__"], ["wait", "__await__", "consumer"])
619+
self.assertEqual(stacks["types.coroutine"],
620+
["sleep", "inner", "legacy", "consumer"])
621+
622+
@skip_if_not_supported
623+
def test_generator_awaitable_running_task_stack(self):
624+
class Awaitable:
625+
def __await__(self):
626+
return RemoteUnwinder(os.getpid()).get_async_stack_trace()
627+
yield
628+
629+
async def consumer():
630+
return await Awaitable()
631+
632+
async def main():
633+
return await asyncio.create_task(consumer(), name="consumer")
634+
635+
stacks = self._extract_coroutine_funcnames(asyncio.run(main()))
636+
self.assertEqual(stacks["consumer"], ["__await__", "consumer"])
637+
589638
@skip_if_not_supported
590639
def test_class_named_like_async_generator_wrapper_is_not_walked(self):
591640
# gh-159620
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:mod:`asyncio` call graph introspection now includes frames of generator
2+
based awaitables, such as generators returned by ``__await__`` and
3+
:func:`types.coroutine` generators.

‎Modules/_remote_debugging/asyncio.c‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,8 @@ parse_task_name(
241241
* ============================================================================ */
242242

243243
// gh-159620
244-
/* Return in *coro_addr the coroutine or async generator to walk for the
245-
awaitable at addr, or 0 if it is neither (for example, a future). */
244+
/* Return in *coro_addr the coroutine, generator or async generator to walk
245+
for the awaitable at addr, or 0 if it is none (for example, a future). */
246246
static int
247247
get_coro_like_address(
248248
RemoteUnwinderObject *unwinder,
@@ -279,7 +279,7 @@ get_coro_like_address(
279279
return -1;
280280
}
281281

282-
if (strcmp(name, "coroutine") == 0) {
282+
if (strcmp(name, "coroutine") == 0 || strcmp(name, "generator") == 0) {
283283
*coro_addr = addr;
284284
}
285285
else if (strcmp(name, "async_generator_asend") == 0

0 commit comments

Comments
 (0)