From d076f696e1bae55ac3487811689e1642ffaaad23 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sat, 5 Sep 2026 18:28:48 +0300 Subject: [PATCH 1/2] gh-156988: Fix asyncio call graph loss at sync generators --- Lib/asyncio/graph.py | 3 ++- Lib/test/test_asyncio/test_graph.py | 20 +++++++++++++++++++ ...-09-05-18-23-30.gh-issue-156988.nEUQE6.rst | 2 ++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-05-18-23-30.gh-issue-156988.nEUQE6.rst diff --git a/Lib/asyncio/graph.py b/Lib/asyncio/graph.py index 94fcd33d7088a68..b7cfb308bf7af2b 100644 --- a/Lib/asyncio/graph.py +++ b/Lib/asyncio/graph.py @@ -155,7 +155,8 @@ def capture_call_graph( f = sys._getframe(depth) if limit != 0 else None try: while f is not None: - is_async = f.f_generator is not None + # gh-156988: sync gen should not clear the call chain + is_async = isinstance(f.f_generator, types.CoroutineType) call_stack.append(FrameCallGraphEntry(f)) if is_async: diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index 50d528fb9fb2c51..36841672e1f0f65 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -593,6 +593,26 @@ async def main(): await main() self.assertRegex(output[0], r'in generator [\w.<>]+\.gen\(\)') + async def test_capture_call_graph_generator_keeps_caller_frames(self): + # gh-156988: sync gen should not clear the call chain + stack = None + + def gen(): + nonlocal stack + graph = asyncio.capture_call_graph() + stack = [entry.frame.f_code.co_name for entry in graph.call_stack] + yield + + def middle(): + for _ in gen(): + pass + + async def main(): + middle() + + await main() + self.assertEqual(stack[:3], ['gen', 'middle', 'main']) + @unittest.skipIf( not hasattr(asyncio.futures, "_c_future_add_to_awaited_by"), diff --git a/Misc/NEWS.d/next/Library/2026-09-05-18-23-30.gh-issue-156988.nEUQE6.rst b/Misc/NEWS.d/next/Library/2026-09-05-18-23-30.gh-issue-156988.nEUQE6.rst new file mode 100644 index 000000000000000..6cb2f1f4cf14ae0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-05-18-23-30.gh-issue-156988.nEUQE6.rst @@ -0,0 +1,2 @@ +:func:`asyncio.print_call_graph` no longer truncates the call stack at a +synchronous generator. From 2cc18aa65bfab8aa75b98685c97cbad60459e47d Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sat, 5 Sep 2026 19:18:32 +0300 Subject: [PATCH 2/2] add AsyncGeneratorType check --- Lib/asyncio/graph.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/asyncio/graph.py b/Lib/asyncio/graph.py index b7cfb308bf7af2b..d5db59f5d6f5f36 100644 --- a/Lib/asyncio/graph.py +++ b/Lib/asyncio/graph.py @@ -156,7 +156,8 @@ def capture_call_graph( try: while f is not None: # gh-156988: sync gen should not clear the call chain - is_async = isinstance(f.f_generator, types.CoroutineType) + is_async = isinstance( + f.f_generator, (types.CoroutineType, types.AsyncGeneratorType)) call_stack.append(FrameCallGraphEntry(f)) if is_async: