diff --git a/Lib/asyncio/graph.py b/Lib/asyncio/graph.py index e240c6dd77d1245..94fcd33d7088a68 100644 --- a/Lib/asyncio/graph.py +++ b/Lib/asyncio/graph.py @@ -274,4 +274,5 @@ def print_call_graph( limit: int | None = None, ) -> None: """Print the async call graph for the current task or the provided Future.""" - print(format_call_graph(future, depth=depth, limit=limit), file=file) + # gh-156327: print_call_graph() must not report its own frame + print(format_call_graph(future, depth=depth + 1, limit=limit), file=file) diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index 2c5bb2e8f52e668..50d528fb9fb2c51 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -41,7 +41,7 @@ def walk(s): return ret buf = io.StringIO() - asyncio.print_call_graph(fut, file=buf, depth=depth+1) + asyncio.print_call_graph(fut, file=buf, depth=depth) stack = asyncio.capture_call_graph(fut, depth=depth) return walk(stack), buf.getvalue() @@ -484,6 +484,14 @@ def test_capture_call_graph_non_future(self): with self.assertRaises(TypeError): asyncio.capture_call_graph("not a future") + async def test_print_call_graph_innermost_frame(self): + # gh-156327: print_call_graph() must not report its own frame + buf = io.StringIO() + lineno = sys._getframe().f_lineno + 1 + asyncio.print_call_graph(file=buf) + first_frame = buf.getvalue().splitlines()[2] + self.assertIn(f'File {__file__!r}, line {lineno},', first_frame) + 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(): diff --git a/Misc/NEWS.d/next/Library/2026-08-24-22-20-55.gh-issue-156327.arC8Ph.rst b/Misc/NEWS.d/next/Library/2026-08-24-22-20-55.gh-issue-156327.arC8Ph.rst new file mode 100644 index 000000000000000..979db7a733a3eae --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-24-22-20-55.gh-issue-156327.arC8Ph.rst @@ -0,0 +1,2 @@ +Fix :func:`asyncio.print_call_graph` including its own frame in the printed +call stack.