From f2182a066d4fe8be739c1b7e133737eae62ab3a0 Mon Sep 17 00:00:00 2001 From: Sanne van den Berg <347126544+Tikbaan@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:37:55 +0200 Subject: [PATCH 1/2] fix: avoid importing asyncio on Windows subprocess platform (#3551) trio._subprocess_platform.__init__ previously imported pipe from asyncio.windows_utils. Because asyncio/__init__.py imports the entire asyncio subsystem, importing trio on Windows loaded over 30 asyncio modules into sys.modules, increasing startup latency by ~10%. This reimplements windows_pipe in trio._subprocess_platform.windows directly on top of _winapi.CreateNamedPipe and _winapi.CreateFile, keeping Trio fully decoupled from asyncio during process initialization. --- newsfragments/3551.bugfix.rst | 1 + src/trio/_subprocess_platform/__init__.py | 10 +-- src/trio/_subprocess_platform/windows.py | 82 ++++++++++++++++++++++- src/trio/_tests/test_trio.py | 33 ++++++--- 4 files changed, 109 insertions(+), 17 deletions(-) create mode 100644 newsfragments/3551.bugfix.rst diff --git a/newsfragments/3551.bugfix.rst b/newsfragments/3551.bugfix.rst new file mode 100644 index 0000000000..f114ab559c --- /dev/null +++ b/newsfragments/3551.bugfix.rst @@ -0,0 +1 @@ +Avoid importing ``asyncio`` on Windows by implementing ``windows_pipe`` directly with ``_winapi``, reducing startup latency. diff --git a/src/trio/_subprocess_platform/__init__.py b/src/trio/_subprocess_platform/__init__.py index daa28d8cd2..6df139b081 100644 --- a/src/trio/_subprocess_platform/__init__.py +++ b/src/trio/_subprocess_platform/__init__.py @@ -78,6 +78,7 @@ def create_pipe_from_child_output() -> tuple[ClosableReceiveStream, int]: except ImportError as ex: # pragma: no cover _wait_child_exiting_error = ex + try: if TYPE_CHECKING: # Not worth type checking these definitions @@ -96,15 +97,8 @@ def create_pipe_from_child_output() -> tuple[trio.lowlevel.FdStream, int]: elif os.name == "nt": import msvcrt - # This isn't exported or documented, but it's also not - # underscore-prefixed, and seems kosher to use. The asyncio docs - # for 3.5 included an example that imported socketpair from - # windows_utils (before socket.socketpair existed on Windows), and - # when asyncio.windows_utils.socketpair was removed in 3.7, the - # removal was mentioned in the release notes. - from asyncio.windows_utils import pipe as windows_pipe - from .._windows_pipes import PipeReceiveStream, PipeSendStream + from .windows import windows_pipe def create_pipe_to_child_stdin() -> tuple[PipeSendStream, int]: # for stdin, we want the write end (our end) to use overlapped I/O diff --git a/src/trio/_subprocess_platform/windows.py b/src/trio/_subprocess_platform/windows.py index 81fb960e4b..2e525f7f4d 100644 --- a/src/trio/_subprocess_platform/windows.py +++ b/src/trio/_subprocess_platform/windows.py @@ -1,3 +1,9 @@ +from __future__ import annotations + +import _winapi +import itertools +import os +import tempfile from typing import TYPE_CHECKING from .._wait_for_object import WaitForSingleObject @@ -5,7 +11,81 @@ if TYPE_CHECKING: from .. import _subprocess +_pipe_counter = itertools.count() + + +def windows_pipe( + *, + duplex: bool = False, + overlapped: tuple[bool, bool] = (True, True), + bufsize: int = 8192, +) -> tuple[int, int]: + """Like os.pipe() but with overlapped support and using handles not fds. + + Adapted from CPython's asyncio.windows_utils.pipe(). + Copyright (c) 2001-2026 Python Software Foundation; All Rights Reserved. + Licensed under the Python Software Foundation License Version 2. + + Changes from CPython: + - Renamed pipe prefix from r'\\.\pipe\asyncio-pipe-*' to r'\\.\pipe\trio-pipe-*'. + - Modernized type hints for Trio's static typing and mypy checks. + - Explicit exception guard ensures early handle is closed if connection fails. + """ + address = tempfile.mktemp( + prefix=rf"\\.\pipe\trio-pipe-{os.getpid():d}-{next(_pipe_counter):d}-" + ) + + if duplex: + openmode = _winapi.PIPE_ACCESS_DUPLEX + access = _winapi.GENERIC_READ | _winapi.GENERIC_WRITE + obsize, ibsize = bufsize, bufsize + else: + openmode = _winapi.PIPE_ACCESS_INBOUND + access = _winapi.GENERIC_WRITE + obsize, ibsize = 0, bufsize + + openmode |= _winapi.FILE_FLAG_FIRST_PIPE_INSTANCE + + if overlapped[0]: + openmode |= _winapi.FILE_FLAG_OVERLAPPED + + flags_and_attribs = _winapi.FILE_FLAG_OVERLAPPED if overlapped[1] else 0 + + h1: int | None = None + h2: int | None = None + try: + h1 = _winapi.CreateNamedPipe( + address, + openmode, + _winapi.PIPE_WAIT, + 1, + obsize, + ibsize, + _winapi.NMPWAIT_WAIT_FOREVER, + _winapi.NULL, + ) + + h2 = _winapi.CreateFile( + address, + access, + 0, + _winapi.NULL, + _winapi.OPEN_EXISTING, + flags_and_attribs, + _winapi.NULL, + ) + + ov = _winapi.ConnectNamedPipe(h1, overlapped=True) + ov.GetOverlappedResult(True) + return h1, h2 + except: + if h1 is not None: + _winapi.CloseHandle(h1) + if h2 is not None: + _winapi.CloseHandle(h2) + raise + -async def wait_child_exiting(process: "_subprocess.Process") -> None: +async def wait_child_exiting(process: _subprocess.Process) -> None: # _handle is not in Popen stubs, though it is present on Windows. await WaitForSingleObject(int(process._proc._handle)) # type: ignore[attr-defined] diff --git a/src/trio/_tests/test_trio.py b/src/trio/_tests/test_trio.py index 65d4ce34f2..2f3307810f 100644 --- a/src/trio/_tests/test_trio.py +++ b/src/trio/_tests/test_trio.py @@ -1,8 +1,25 @@ -def test_trio_import() -> None: - import sys - - for module in list(sys.modules.keys()): - if module.startswith("trio"): - del sys.modules[module] - - import trio # noqa: F401 +from __future__ import annotations + +import subprocess +import sys + + +def test_trio_import() -> None: + for module in list(sys.modules.keys()): + if module.startswith("trio"): + del sys.modules[module] + + import trio # noqa: F401 + + +def test_trio_import_does_not_import_asyncio() -> None: + res = subprocess.run( + [ + sys.executable, + "-c", + "import sys, trio; assert 'asyncio' not in sys.modules, f'unexpected asyncio modules: {[m for m in sys.modules if \"asyncio\" in m]}'", + ], + capture_output=True, + text=True, + ) + assert res.returncode == 0, f"import trio loaded asyncio:\n{res.stderr}" From 07152425b6aa877be6606ba47422743fd9e4bc99 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:10:35 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/trio/_subprocess_platform/windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trio/_subprocess_platform/windows.py b/src/trio/_subprocess_platform/windows.py index 2e525f7f4d..411f95b115 100644 --- a/src/trio/_subprocess_platform/windows.py +++ b/src/trio/_subprocess_platform/windows.py @@ -27,7 +27,7 @@ def windows_pipe( Licensed under the Python Software Foundation License Version 2. Changes from CPython: - - Renamed pipe prefix from r'\\.\pipe\asyncio-pipe-*' to r'\\.\pipe\trio-pipe-*'. + - Renamed pipe prefix from r'\\.\\pipe\asyncio-pipe-*' to r'\\.\\pipe\trio-pipe-*'. - Modernized type hints for Trio's static typing and mypy checks. - Explicit exception guard ensures early handle is closed if connection fails. """