https://github.com/python/cpython/commit/c79146a9eb05dea735a6a22dc004379cdec3ed0e
commit: c79146a9eb05dea735a6a22dc004379cdec3ed0e
branch: main
author: Kumar Aditya <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-10-28T08:14:16Z
summary:

gh-160804: Support generator based awaitables in asyncio introspection (#160805)

files:
A Misc/NEWS.d/next/Library/2026-10-28-07-26-47.gh-issue-160804.R5HeTQ.rst
M Lib/asyncio/graph.py
M Lib/test/test_asyncio/test_graph.py
M Lib/test/test_external_inspection.py
M Modules/_remote_debugging/asyncio.c

diff --git a/Lib/asyncio/graph.py b/Lib/asyncio/graph.py
index d5db59f5d6f5f36..b5c9dafe8ed50a1 100644
--- a/Lib/asyncio/graph.py
+++ b/Lib/asyncio/graph.py
@@ -65,6 +65,11 @@ def _build_graph_for_future(
             # A native async generator or duck-type compatible iterator
             st.append(FrameCallGraphEntry(coro.ag_frame))
             coro = coro.ag_await
+        elif hasattr(coro, 'gi_yieldfrom'):
+            # A generator, e.g. returned by __await__ or @types.coroutine
+            if coro.gi_frame is not None:
+                st.append(FrameCallGraphEntry(coro.gi_frame))
+            coro = coro.gi_yieldfrom
         else:
             break
 
diff --git a/Lib/test/test_asyncio/test_graph.py 
b/Lib/test/test_asyncio/test_graph.py
index 7f6e0cc477774e1..e438e5915b2a852 100644
--- a/Lib/test/test_asyncio/test_graph.py
+++ b/Lib/test/test_asyncio/test_graph.py
@@ -1,6 +1,7 @@
 import asyncio
 import io
 import sys
+import types
 import unittest
 from unittest import mock
 
@@ -11,6 +12,7 @@ def tearDownModule():
 
 
 def capture_test_stack(*, fut=None, depth=1):
+    future_await_code = asyncio.futures._PyFuture.__await__.__code__
 
     def walk(s):
         ret = [
@@ -27,8 +29,12 @@ def walk(s):
                             f"a {entry.frame.f_generator.cr_code.co_name}"
                             if hasattr(entry.frame.f_generator, 'cr_code') else
                             f"ag {entry.frame.f_generator.ag_code.co_name}"
+                            if hasattr(entry.frame.f_generator, 'ag_code') else
+                            f"g {entry.frame.f_generator.gi_code.co_name}"
                         )
                 ) for entry in s.call_stack
+                # Skip the pure Python Future.__await__ generator frame
+                if entry.frame.f_code is not future_await_code
             ]
         )
 
@@ -305,6 +311,73 @@ async def main():
             []
         ])
 
+    async def test_stack_await_generator(self):
+        # The call graph of a suspended task sees through generators
+        # returned by __await__() and the generators they yield from.
+
+        loop = asyncio.get_running_loop()
+        blocker = loop.create_future()
+
+        def wait():
+            yield from blocker
+
+        class Awaitable:
+            def __await__(self):
+                return (yield from wait())
+
+        async def main():
+            await Awaitable()
+
+        task = asyncio.create_task(main(), name='await task')
+        await asyncio.sleep(0)
+        try:
+            stack = capture_test_stack(fut=task)
+        finally:
+            blocker.set_result(None)
+            await task
+
+        self.assertEqual(stack[0], [
+            'T<await task>',
+            [
+                'g wait',
+                'g __await__',
+                'a main',
+            ],
+            []
+        ])
+
+    async def test_stack_types_coroutine(self):
+        loop = asyncio.get_running_loop()
+        blocker = loop.create_future()
+
+        async def inner():
+            await blocker
+
+        @types.coroutine
+        def legacy():
+            yield from inner()
+
+        async def main():
+            await legacy()
+
+        task = asyncio.create_task(main(), name='legacy task')
+        await asyncio.sleep(0)
+        try:
+            stack = capture_test_stack(fut=task)
+        finally:
+            blocker.set_result(None)
+            await task
+
+        self.assertEqual(stack[0], [
+            'T<legacy task>',
+            [
+                'a inner',
+                'g legacy',
+                'a main',
+            ],
+            []
+        ])
+
     def test_ag_frame_used_for_async_generator(self):
         # Regression test for gh-148736: the ag_await branch of
         # _build_graph_for_future must read ag_frame, not cr_frame.
diff --git a/Lib/test/test_external_inspection.py 
b/Lib/test/test_external_inspection.py
index d6243c3fc818eab..48e5a2692c8df36 100644
--- a/Lib/test/test_external_inspection.py
+++ b/Lib/test/test_external_inspection.py
@@ -8,6 +8,7 @@
 import socket
 import threading
 import time
+import types
 from contextlib import contextmanager
 from asyncio import staggered, taskgroups, base_events, tasks
 from unittest.mock import ANY
@@ -586,6 +587,54 @@ async def main():
         stacks = self._extract_coroutine_funcnames(asyncio.run(main()))
         self.assertEqual(stacks["anext"], ["agen"])
 
+    @skip_if_not_supported
+    def test_generator_awaitable_stack_is_not_truncated(self):
+        def wait():
+            yield from asyncio.get_running_loop().create_future()
+
+        class Awaitable:
+            def __await__(self):
+                return (yield from wait())
+
+        async def inner():
+            await asyncio.sleep(10_000)
+
+        @types.coroutine
+        def legacy():
+            yield from inner()
+
+        async def consumer(aw):
+            await aw
+
+        async def main():
+            await_task = asyncio.create_task(
+                consumer(Awaitable()), name="__await__")
+            legacy_task = asyncio.create_task(
+                consumer(legacy()), name="types.coroutine")
+            await asyncio.sleep(0)
+            return RemoteUnwinder(os.getpid()).get_all_awaited_by()
+
+        stacks = self._extract_coroutine_funcnames(asyncio.run(main()))
+        self.assertEqual(stacks["__await__"], ["wait", "__await__", 
"consumer"])
+        self.assertEqual(stacks["types.coroutine"],
+                         ["sleep", "inner", "legacy", "consumer"])
+
+    @skip_if_not_supported
+    def test_generator_awaitable_running_task_stack(self):
+        class Awaitable:
+            def __await__(self):
+                return RemoteUnwinder(os.getpid()).get_async_stack_trace()
+                yield
+
+        async def consumer():
+            return await Awaitable()
+
+        async def main():
+            return await asyncio.create_task(consumer(), name="consumer")
+
+        stacks = self._extract_coroutine_funcnames(asyncio.run(main()))
+        self.assertEqual(stacks["consumer"], ["__await__", "consumer"])
+
     @skip_if_not_supported
     def test_class_named_like_async_generator_wrapper_is_not_walked(self):
         # gh-159620
diff --git 
a/Misc/NEWS.d/next/Library/2026-10-28-07-26-47.gh-issue-160804.R5HeTQ.rst 
b/Misc/NEWS.d/next/Library/2026-10-28-07-26-47.gh-issue-160804.R5HeTQ.rst
new file mode 100644
index 000000000000000..36fa06dbb2202b3
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-28-07-26-47.gh-issue-160804.R5HeTQ.rst
@@ -0,0 +1,3 @@
+:mod:`asyncio` call graph introspection now includes frames of generator
+based awaitables, such as generators returned by ``__await__`` and
+:func:`types.coroutine` generators.
diff --git a/Modules/_remote_debugging/asyncio.c 
b/Modules/_remote_debugging/asyncio.c
index 884065b6efdee74..d31f783186a2c30 100644
--- a/Modules/_remote_debugging/asyncio.c
+++ b/Modules/_remote_debugging/asyncio.c
@@ -241,8 +241,8 @@ parse_task_name(
  * 
============================================================================ */
 
 // gh-159620
-/* Return in *coro_addr the coroutine or async generator to walk for the
-   awaitable at addr, or 0 if it is neither (for example, a future). */
+/* Return in *coro_addr the coroutine, generator or async generator to walk
+   for the awaitable at addr, or 0 if it is none (for example, a future). */
 static int
 get_coro_like_address(
     RemoteUnwinderObject *unwinder,
@@ -279,7 +279,7 @@ get_coro_like_address(
         return -1;
     }
 
-    if (strcmp(name, "coroutine") == 0) {
+    if (strcmp(name, "coroutine") == 0 || strcmp(name, "generator") == 0) {
         *coro_addr = addr;
     }
     else if (strcmp(name, "async_generator_asend") == 0

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to