From 1b70914513792ffc9e57dc5b0bb682ccda46e802 Mon Sep 17 00:00:00 2001 From: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com> Date: Sat, 29 Aug 2026 13:04:57 +0300 Subject: [PATCH] [3.14] gh-156408: Fix asyncio.print_call_graph() on a finished task (GH-156410) (cherry picked from commit 6c425f1d18d3915cb32e3372647db24bad7ab92d) Co-authored-by: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com> --- Lib/asyncio/graph.py | 3 ++- Lib/test/test_asyncio/test_graph.py | 18 ++++++++++++++++++ ...6-08-26-12-48-10.gh-issue-156408.-Yd0k7.rst | 2 ++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-26-12-48-10.gh-issue-156408.-Yd0k7.rst diff --git a/Lib/asyncio/graph.py b/Lib/asyncio/graph.py index 35f7fa62140bd4..54df7afa5553b9 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 2f22fbccba42bc..46a7c3d3192837 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -345,6 +345,24 @@ async def main(): self.assertTrue(stack_for_fut[1].startswith('* Future(id=')) + 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()) + @unittest.skipIf( not hasattr(asyncio.futures, "_c_future_add_to_awaited_by"), 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.