diff --git a/Lib/asyncio/graph.py b/Lib/asyncio/graph.py index 7e85e08c4291a0..e240c6dd77d124 100644 --- a/Lib/asyncio/graph.py +++ b/Lib/asyncio/graph.py @@ -58,7 +58,8 @@ def _build_graph_for_future( while coro is not None: if hasattr(coro, 'cr_await'): # A native coroutine or duck-type compatible iterator - st.append(FrameCallGraphEntry(coro.cr_frame)) + if coro.cr_frame is not None: + st.append(FrameCallGraphEntry(coro.cr_frame)) coro = coro.cr_await elif hasattr(coro, 'ag_await'): # A native async generator or duck-type compatible iterator diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index 928b618fe5c55b..415615f1373079 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -422,6 +422,24 @@ def test_capture_call_graph_non_future(self): with self.assertRaises(TypeError): asyncio.capture_call_graph("not a future") + async def test_call_graph_finished_task(self): + # gh-156408: the call graph must not record a finished coroutine's None frame + async def boom(): + raise ValueError + + done = asyncio.create_task(asyncio.sleep(0), name='done') + failed = asyncio.create_task(boom(), name='failed') + cancelled = asyncio.create_task(asyncio.Event().wait(), name='cancelled') + cancelled.cancel() + await asyncio.gather(done, failed, cancelled, return_exceptions=True) + + for task in (done, failed, cancelled): + with self.subTest(task=task.get_name()): + buf = io.StringIO() + asyncio.print_call_graph(task, file=buf) + self.assertEqual(asyncio.capture_call_graph(task).call_stack, ()) + self.assertIn(f"name={task.get_name()!r}", buf.getvalue()) + async def test_capture_call_graph_no_current_task(self): results = [] diff --git a/Misc/NEWS.d/next/Library/2026-08-26-12-48-10.gh-issue-156408.-Yd0k7.rst b/Misc/NEWS.d/next/Library/2026-08-26-12-48-10.gh-issue-156408.-Yd0k7.rst new file mode 100644 index 00000000000000..62c92838b5c126 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-26-12-48-10.gh-issue-156408.-Yd0k7.rst @@ -0,0 +1,2 @@ +Fix :func:`asyncio.print_call_graph` raising :exc:`AttributeError` when +called on a task that has already finished.