From d70ede369db7a607f09db5039137b8587be1f6c5 Mon Sep 17 00:00:00 2001 From: Johnny Wilson Dougherty <192861341+JohnnyWilson16@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:48:01 +0530 Subject: [PATCH 1/2] gh-157259: Record awaited-by edge for asyncio subprocess pipe-connect task When BaseSubprocessTransport creates a task for _connect_pipes(waiter), register the awaited-by relationship between task and waiter so that asyncio.print_call_graph() and asyncio.capture_call_graph() connect the background pipe-connect task to the awaiting parent task. Discard the edge in a done callback once _connect_pipes completes or is cancelled. --- Lib/asyncio/base_subprocess.py | 8 +++- Lib/test/test_asyncio/test_graph.py | 46 +++++++++++++++++++ ...-10-16-21-45-00.gh-issue-157259.wK8pLz.rst | 1 + 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-16-21-45-00.gh-issue-157259.wK8pLz.rst diff --git a/Lib/asyncio/base_subprocess.py b/Lib/asyncio/base_subprocess.py index 0423bc100b2d932..c01d6882e0d8421 100644 --- a/Lib/asyncio/base_subprocess.py +++ b/Lib/asyncio/base_subprocess.py @@ -5,6 +5,7 @@ import signal import sys +from . import futures from . import protocols from . import transports from .log import logger @@ -53,7 +54,12 @@ def __init__(self, loop, protocol, args, shell, logger.debug('process %r created: pid %s', program, self._pid) - self._loop.create_task(self._connect_pipes(waiter)) + task = self._loop.create_task(self._connect_pipes(waiter)) + if waiter is not None: + futures.future_add_to_awaited_by(task, waiter) + task.add_done_callback( + lambda t: futures.future_discard_from_awaited_by(t, waiter) + ) def __repr__(self): info = [self.__class__.__name__] diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index 1326ef50c149b0b..fca86805579c0dc 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -450,6 +450,52 @@ async def main(t): ] ]) + async def test_stack_subprocess_connect_pipes(self): + # gh-157259: create_subprocess_exec must record the awaiting waiter / task + # on the internal _connect_pipes task + loop = asyncio.get_running_loop() + in_connect_pipes = asyncio.Event() + can_finish = asyncio.Event() + + pipe_task = None + orig_connect = loop.connect_write_pipe + async def mocked_connect(*args, **kwargs): + nonlocal pipe_task + pipe_task = asyncio.current_task() + in_connect_pipes.set() + await can_finish.wait() + return await orig_connect(*args, **kwargs) + + loop.connect_write_pipe = mocked_connect + spawn = None + try: + spawn = asyncio.create_task( + asyncio.create_subprocess_exec( + sys.executable, '-c', 'pass', + stdin=asyncio.subprocess.PIPE, + ), + name='spawn', + ) + + await in_connect_pipes.wait() + self.assertIsNotNone(pipe_task) + + stack = capture_test_stack(fut=pipe_task) + + self.assertEqual(stack[0][0], 'T') + awaited_by = stack[0][2] + self.assertEqual(len(awaited_by), 1) + self.assertEqual(awaited_by[0][0], 'F') + self.assertEqual(awaited_by[0][2][0][0], 'T') + finally: + can_finish.set() + loop.connect_write_pipe = orig_connect + if spawn is not None: + proc = await spawn + await proc.wait() + + self.assertFalse(pipe_task._asyncio_awaited_by) + async def test_stack_task(self): stack_for_inner = None diff --git a/Misc/NEWS.d/next/Library/2026-10-16-21-45-00.gh-issue-157259.wK8pLz.rst b/Misc/NEWS.d/next/Library/2026-10-16-21-45-00.gh-issue-157259.wK8pLz.rst new file mode 100644 index 000000000000000..a51bbdedec7abc7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-16-21-45-00.gh-issue-157259.wK8pLz.rst @@ -0,0 +1 @@ +Record the awaiting task in the :mod:`asyncio` call graph for internal pipe-connection tasks created during subprocess spawning. From d7f0af9a432ec4feea12fb6869671a79d489ce49 Mon Sep 17 00:00:00 2001 From: Johnny Wilson Dougherty <192861341+JohnnyWilson16@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:19:51 +0530 Subject: [PATCH 2/2] gh-157259: Skip subprocess call graph test on platforms without subprocess support --- Lib/test/test_asyncio/test_graph.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index fca86805579c0dc..94f846b2dcd68f3 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -2,6 +2,7 @@ import io import sys import unittest +from test import support from unittest import mock @@ -450,6 +451,7 @@ async def main(t): ] ]) + @support.requires_subprocess() async def test_stack_subprocess_connect_pipes(self): # gh-157259: create_subprocess_exec must record the awaiting waiter / task # on the internal _connect_pipes task @@ -477,7 +479,7 @@ async def mocked_connect(*args, **kwargs): name='spawn', ) - await in_connect_pipes.wait() + await asyncio.wait_for(in_connect_pipes.wait(), timeout=support.SHORT_TIMEOUT) self.assertIsNotNone(pipe_task) stack = capture_test_stack(fut=pipe_task)