From e13fd3f041274ccd6ef9804a6c1abe83c400f038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sat, 15 Jun 2024 12:25:12 +0300 Subject: [PATCH 01/16] Added more subprocess parameters and improved the related annotations --- docs/versionhistory.rst | 5 ++ src/anyio/_backends/_asyncio.py | 25 ++++----- src/anyio/_backends/_trio.py | 39 +++++++------ src/anyio/_core/_subprocesses.py | 96 +++++++++++++++++++++++++++++--- src/anyio/abc/_eventloop.py | 50 ++++------------- tests/test_subprocesses.py | 44 ++++++++++++++- 6 files changed, 179 insertions(+), 80 deletions(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 43a7927a..db4d20aa 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -8,6 +8,11 @@ This library adheres to `Semantic Versioning 2.0 `_. - Added support for the ``from_uri()``, ``full_match()``, ``parser`` methods/properties in ``anyio.Path``, newly added in Python 3.13 (`#737 `_) +- Added support for more keyword arguments for ``run_process()`` and ``open_process()``: + ``preexec_fn``, ``startupinfo``, ``creationflags``, ``user``, ``group``, + ``extra_groups`` and ``umask`` +- Improved the type annotations in ``run_process()`` and ``open_process()`` to allow for + path-like arguments, just like ``subprocess.Popen`` - Changed the ``ResourceWarning`` from an unclosed memory object stream to include its address for easier identification - Fixed ``to_process.run_sync()`` failing to initialize if ``__main__.__file__`` pointed diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 43b7cb0e..4fbd42e2 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -4,6 +4,7 @@ import asyncio import concurrent.futures import math +import pathlib import socket import sys import threading @@ -47,7 +48,6 @@ Collection, ContextManager, Coroutine, - Mapping, Optional, Sequence, Tuple, @@ -80,6 +80,7 @@ UDPPacketType, UNIXDatagramPacketType, ) +from ..abc._eventloop import StrOrBytesPath from ..lowlevel import RunVar from ..streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream @@ -2245,26 +2246,24 @@ def create_blocking_portal(cls) -> abc.BlockingPortal: @classmethod async def open_process( cls, - command: str | bytes | Sequence[str | bytes], + command: StrOrBytesPath | Sequence[StrOrBytesPath], *, - shell: bool, stdin: int | IO[Any] | None, stdout: int | IO[Any] | None, stderr: int | IO[Any] | None, - cwd: str | bytes | PathLike | None = None, - env: Mapping[str, str] | None = None, - start_new_session: bool = False, + **kwargs: Any, ) -> Process: await cls.checkpoint() - if shell: + if isinstance(command, PathLike): + command = str(pathlib.Path(command)) + + if isinstance(command, (str, bytes)): process = await asyncio.create_subprocess_shell( - cast("str | bytes", command), + command, stdin=stdin, stdout=stdout, stderr=stderr, - cwd=cwd, - env=env, - start_new_session=start_new_session, + **kwargs, ) else: process = await asyncio.create_subprocess_exec( @@ -2272,9 +2271,7 @@ async def open_process( stdin=stdin, stdout=stdout, stderr=stderr, - cwd=cwd, - env=env, - start_new_session=start_new_session, + **kwargs, ) stdin_stream = StreamWriterWrapper(process.stdin) if process.stdin else None diff --git a/src/anyio/_backends/_trio.py b/src/anyio/_backends/_trio.py index cf6f3db7..5ebe6dba 100644 --- a/src/anyio/_backends/_trio.py +++ b/src/anyio/_backends/_trio.py @@ -25,7 +25,6 @@ ContextManager, Coroutine, Generic, - Mapping, NoReturn, Sequence, TypeVar, @@ -60,7 +59,7 @@ from .._core._synchronization import ResourceGuard from .._core._tasks import CancelScope as BaseCancelScope from ..abc import IPSockAddrType, UDPPacketType, UNIXDatagramPacketType -from ..abc._eventloop import AsyncBackend +from ..abc._eventloop import AsyncBackend, StrOrBytesPath from ..streams.memory import MemoryObjectSendStream if sys.version_info >= (3, 10): @@ -967,26 +966,32 @@ def create_blocking_portal(cls) -> abc.BlockingPortal: @classmethod async def open_process( cls, - command: str | bytes | Sequence[str | bytes], + command: StrOrBytesPath | Sequence[StrOrBytesPath], *, - shell: bool, stdin: int | IO[Any] | None, stdout: int | IO[Any] | None, stderr: int | IO[Any] | None, - cwd: str | bytes | PathLike | None = None, - env: Mapping[str, str] | None = None, - start_new_session: bool = False, + **kwargs: Any, ) -> Process: - process = await trio.lowlevel.open_process( # type: ignore[misc] - command, # type: ignore[arg-type] - stdin=stdin, - stdout=stdout, - stderr=stderr, - shell=shell, - cwd=cwd, - env=env, - start_new_session=start_new_session, - ) + if isinstance(command, (str, bytes, PathLike)): + process = await trio.lowlevel.open_process( + command, + stdin=stdin, + stdout=stdout, + stderr=stderr, + shell=True, + **kwargs, + ) + else: + process = await trio.lowlevel.open_process( + command, + stdin=stdin, + stdout=stdout, + stderr=stderr, + shell=False, + **kwargs, + ) + stdin_stream = SendStreamWrapper(process.stdin) if process.stdin else None stdout_stream = ReceiveStreamWrapper(process.stdout) if process.stdout else None stderr_stream = ReceiveStreamWrapper(process.stderr) if process.stderr else None diff --git a/src/anyio/_core/_subprocesses.py b/src/anyio/_core/_subprocesses.py index 5d5d7b76..ae1a9e4c 100644 --- a/src/anyio/_core/_subprocesses.py +++ b/src/anyio/_core/_subprocesses.py @@ -1,26 +1,37 @@ from __future__ import annotations -from collections.abc import AsyncIterable, Mapping, Sequence +import sys +from collections.abc import AsyncIterable, Callable, Iterable, Mapping, Sequence from io import BytesIO from os import PathLike from subprocess import DEVNULL, PIPE, CalledProcessError, CompletedProcess from typing import IO, Any, cast from ..abc import Process +from ..abc._eventloop import StrOrBytesPath from ._eventloop import get_async_backend from ._tasks import create_task_group +PY39_ARGS = frozenset(["user", "group", "extra_groups", "umask"]) + async def run_process( - command: str | bytes | Sequence[str | bytes], + command: StrOrBytesPath | Sequence[StrOrBytesPath], *, input: bytes | None = None, stdout: int | IO[Any] | None = PIPE, stderr: int | IO[Any] | None = PIPE, + preexec_fn: Callable[[], Any] | None = None, check: bool = True, - cwd: str | bytes | PathLike[str] | None = None, + cwd: StrOrBytesPath | None = None, env: Mapping[str, str] | None = None, + startupinfo: Any = None, + creationflags: int = 0, start_new_session: bool = False, + user: str | int | None = None, + group: str | int | None = None, + extra_groups: Iterable[str | int] | None = None, + umask: int = -1, ) -> CompletedProcess[bytes]: """ Run an external command in a subprocess and wait until it completes. @@ -34,14 +45,26 @@ async def run_process( a file-like object, or `None` :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, :data:`subprocess.STDOUT`, a file-like object, or `None` + :param preexec_fn: a callable that is called in the child process just before the + actual command is executed :param check: if ``True``, raise :exc:`~subprocess.CalledProcessError` if the process terminates with a return code other than 0 :param cwd: If not ``None``, change the working directory to this before running the command :param env: if not ``None``, this mapping replaces the inherited environment variables from the parent process + :param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used + to specify process startup parameters (Windows only) + :param creationflags: flags that can be used to control the creation of the + subprocess (see :class:`subprocess.Popen` for the specifics) :param start_new_session: if ``true`` the setsid() system call will be made in the child process prior to the execution of the subprocess. (POSIX only) + :param user: effective user to run the process as (Python >= 3.9, POSIX only) + :param group: effective group to run the process as (Python >= 3.9, POSIX only) + :param extra_groups: supplementary groups to set in the subprocess (Python >= 3.9, + POSIX only) + :param umask: if not negative, this umask is applied in the child process before + running the given command (Python >= 3.9, POSIX only) :return: an object representing the completed process :raises ~subprocess.CalledProcessError: if ``check`` is ``True`` and the process exits with a nonzero return code @@ -60,9 +83,16 @@ async def drain_stream(stream: AsyncIterable[bytes], index: int) -> None: stdin=PIPE if input else DEVNULL, stdout=stdout, stderr=stderr, + preexec_fn=preexec_fn, cwd=cwd, env=env, + startupinfo=startupinfo, + creationflags=creationflags, start_new_session=start_new_session, + user=user, + group=group, + extra_groups=extra_groups, + umask=umask, ) as process: stream_contents: list[bytes | None] = [None, None] async with create_task_group() as tg: @@ -86,14 +116,21 @@ async def drain_stream(stream: AsyncIterable[bytes], index: int) -> None: async def open_process( - command: str | bytes | Sequence[str | bytes], + command: StrOrBytesPath | Sequence[StrOrBytesPath], *, stdin: int | IO[Any] | None = PIPE, stdout: int | IO[Any] | None = PIPE, stderr: int | IO[Any] | None = PIPE, - cwd: str | bytes | PathLike[str] | None = None, + preexec_fn: Callable[[], Any] | None = None, + cwd: StrOrBytesPath | None = None, env: Mapping[str, str] | None = None, + startupinfo: Any = None, + creationflags: int = 0, start_new_session: bool = False, + user: str | int | None = None, + group: str | int | None = None, + extra_groups: Iterable[str | int] | None = None, + umask: int = -1, ) -> Process: """ Start an external command in a subprocess. @@ -108,33 +145,76 @@ async def open_process( a file-like object, or ``None`` :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, :data:`subprocess.STDOUT`, a file-like object, or ``None`` + :param preexec_fn: a callable that is called in the child process just before the + actual command is executed :param cwd: If not ``None``, the working directory is changed before executing :param env: If env is not ``None``, it must be a mapping that defines the environment variables for the new process + :param creationflags: flags that can be used to control the creation of the + subprocess (see :class:`subprocess.Popen` for the specifics) + :param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used + to specify process startup parameters (Windows only) :param start_new_session: if ``true`` the setsid() system call will be made in the child process prior to the execution of the subprocess. (POSIX only) + :param user: effective user to run the process as (Python >= 3.9; POSIX only) + :param group: effective group to run the process as (Python >= 3.9; POSIX only) + :param extra_groups: supplementary groups to set in the subprocess (Python >= 3.9; + POSIX only) + :param umask: if not negative, this umask is applied in the child process before + running the given command (Python >= 3.9; POSIX only) :return: an asynchronous process object """ - if isinstance(command, (str, bytes)): + kwargs: dict[str, Any] = {} + if user is not None: + if sys.version_info < (3, 9): + raise TypeError("the 'user' argument requires Python 3.9 or later") + + kwargs["user"] = user + + if group is not None: + if sys.version_info < (3, 9): + raise TypeError("the 'group' argument requires Python 3.9 or later") + + kwargs["group"] = group + + if extra_groups is not None: + if sys.version_info < (3, 9): + raise TypeError("the 'extra_groups' argument requires Python 3.9 or later") + + kwargs["extra_groups"] = group + + if umask >= 0: + if sys.version_info < (3, 9): + raise TypeError("the 'umask' argument requires Python 3.9 or later") + + kwargs["umask"] = umask + + if isinstance(command, (str, bytes, PathLike)): return await get_async_backend().open_process( command, - shell=True, stdin=stdin, stdout=stdout, stderr=stderr, + preexec_fn=preexec_fn, cwd=cwd, env=env, + startupinfo=startupinfo, + creationflags=creationflags, start_new_session=start_new_session, + **kwargs, ) else: return await get_async_backend().open_process( command, - shell=False, stdin=stdin, stdout=stdout, stderr=stderr, + preexec_fn=preexec_fn, cwd=cwd, env=env, + startupinfo=startupinfo, + creationflags=creationflags, start_new_session=start_new_session, + **kwargs, ) diff --git a/src/anyio/abc/_eventloop.py b/src/anyio/abc/_eventloop.py index a50afefa..ee29b8ad 100644 --- a/src/anyio/abc/_eventloop.py +++ b/src/anyio/abc/_eventloop.py @@ -3,7 +3,7 @@ import math import sys from abc import ABCMeta, abstractmethod -from collections.abc import AsyncIterator, Awaitable, Mapping +from collections.abc import AsyncIterator, Awaitable from os import PathLike from signal import Signals from socket import AddressFamily, SocketKind, socket @@ -15,6 +15,7 @@ ContextManager, Sequence, TypeVar, + Union, overload, ) @@ -23,9 +24,12 @@ else: from typing_extensions import TypeVarTuple, Unpack -if TYPE_CHECKING: - from typing import Literal +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + from typing_extensions import TypeAlias +if TYPE_CHECKING: from .._core._synchronization import CapacityLimiter, Event from .._core._tasks import CancelScope from .._core._testing import TaskInfo @@ -46,6 +50,7 @@ T_Retval = TypeVar("T_Retval") PosArgsT = TypeVarTuple("PosArgsT") +StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]"] class AsyncBackend(metaclass=ABCMeta): @@ -213,51 +218,16 @@ def run_sync_from_thread( def create_blocking_portal(cls) -> BlockingPortal: pass - @classmethod - @overload - async def open_process( - cls, - command: str | bytes, - *, - shell: Literal[True], - stdin: int | IO[Any] | None, - stdout: int | IO[Any] | None, - stderr: int | IO[Any] | None, - cwd: str | bytes | PathLike[str] | None = None, - env: Mapping[str, str] | None = None, - start_new_session: bool = False, - ) -> Process: - pass - - @classmethod - @overload - async def open_process( - cls, - command: Sequence[str | bytes], - *, - shell: Literal[False], - stdin: int | IO[Any] | None, - stdout: int | IO[Any] | None, - stderr: int | IO[Any] | None, - cwd: str | bytes | PathLike[str] | None = None, - env: Mapping[str, str] | None = None, - start_new_session: bool = False, - ) -> Process: - pass - @classmethod @abstractmethod async def open_process( cls, - command: str | bytes | Sequence[str | bytes], + command: StrOrBytesPath | Sequence[StrOrBytesPath], *, - shell: bool, stdin: int | IO[Any] | None, stdout: int | IO[Any] | None, stderr: int | IO[Any] | None, - cwd: str | bytes | PathLike[str] | None = None, - env: Mapping[str, str] | None = None, - start_new_session: bool = False, + **kwargs: Any, ) -> Process: pass diff --git a/tests/test_subprocesses.py b/tests/test_subprocesses.py index 22bf882e..29553ee6 100644 --- a/tests/test_subprocesses.py +++ b/tests/test_subprocesses.py @@ -3,12 +3,14 @@ import os import platform import sys +from contextlib import ExitStack from pathlib import Path from subprocess import CalledProcessError from textwrap import dedent +from typing import Any import pytest -from _pytest.fixtures import FixtureRequest +from pytest import FixtureRequest from anyio import CancelScope, ClosedResourceError, open_process, run_process from anyio.streams.buffered import BufferedByteReceiveStream @@ -225,3 +227,43 @@ async def test_process_aexit_cancellation_closes_standard_streams( with pytest.raises(ClosedResourceError): await process.stderr.receive(1) + + +@pytest.mark.parametrize( + "argname, argvalue", + [ + pytest.param("user", os.getuid(), id="user"), + pytest.param("group", os.getuid(), id="group"), + pytest.param("extra_groups", [], id="extra_groups"), + pytest.param("umask", 0, id="umask"), + ], +) +async def test_py39_arguments( + argname: str, + argvalue: Any, + anyio_backend_name: str, + anyio_backend_options: dict[str, Any], +) -> None: + with ExitStack() as stack: + if sys.version_info < (3, 9): + stack.enter_context( + pytest.raises( + TypeError, + match=rf"the {argname!r} argument requires Python 3.9 or later", + ) + ) + + try: + await run_process( + [sys.executable, "-c", "print('hello')"], **{argname: argvalue} + ) + except TypeError as exc: + if ( + "unexpected keyword argument" in str(exc) + and anyio_backend_name == "asyncio" + and anyio_backend_options["loop_factory"] + and anyio_backend_options["loop_factory"].__module__ == "uvloop" + ): + pytest.skip(f"the {argname!r} argument is not supported by uvloop yet") + + raise From aa7c350a54e57812b2ae98ad2d5dade0b9dcfd9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 23 Jun 2024 17:55:36 +0300 Subject: [PATCH 02/16] Fixed copy-paste oopsie --- tests/test_subprocesses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_subprocesses.py b/tests/test_subprocesses.py index 29553ee6..183ec90a 100644 --- a/tests/test_subprocesses.py +++ b/tests/test_subprocesses.py @@ -233,7 +233,7 @@ async def test_process_aexit_cancellation_closes_standard_streams( "argname, argvalue", [ pytest.param("user", os.getuid(), id="user"), - pytest.param("group", os.getuid(), id="group"), + pytest.param("group", os.getgid(), id="group"), pytest.param("extra_groups", [], id="extra_groups"), pytest.param("umask", 0, id="umask"), ], From 15bcacd010b9856fdb016bc5bdac9cff1935d35d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 23 Jun 2024 18:05:03 +0300 Subject: [PATCH 03/16] Worked around collection error on Windows --- tests/test_subprocesses.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/test_subprocesses.py b/tests/test_subprocesses.py index 183ec90a..c67c105b 100644 --- a/tests/test_subprocesses.py +++ b/tests/test_subprocesses.py @@ -3,6 +3,7 @@ import os import platform import sys +from collections.abc import Callable from contextlib import ExitStack from pathlib import Path from subprocess import CalledProcessError @@ -230,17 +231,17 @@ async def test_process_aexit_cancellation_closes_standard_streams( @pytest.mark.parametrize( - "argname, argvalue", + "argname, argvalue_factory", [ - pytest.param("user", os.getuid(), id="user"), - pytest.param("group", os.getgid(), id="group"), - pytest.param("extra_groups", [], id="extra_groups"), - pytest.param("umask", 0, id="umask"), + pytest.param("user", lambda: os.getuid(), id="user"), + pytest.param("group", lambda: os.getgid(), id="group"), + pytest.param("extra_groups", list, id="extra_groups"), + pytest.param("umask", lambda: 0, id="umask"), ], ) async def test_py39_arguments( argname: str, - argvalue: Any, + argvalue_factory: Callable[[], Any], anyio_backend_name: str, anyio_backend_options: dict[str, Any], ) -> None: @@ -255,7 +256,8 @@ async def test_py39_arguments( try: await run_process( - [sys.executable, "-c", "print('hello')"], **{argname: argvalue} + [sys.executable, "-c", "print('hello')"], + **{argname: argvalue_factory()}, ) except TypeError as exc: if ( From 21869756a1a5d4c2b06d321e8032d181111ab429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 23 Jun 2024 18:09:33 +0300 Subject: [PATCH 04/16] Skip user/group tests on Windows --- tests/test_subprocesses.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/test_subprocesses.py b/tests/test_subprocesses.py index c67c105b..84c4b4dc 100644 --- a/tests/test_subprocesses.py +++ b/tests/test_subprocesses.py @@ -233,8 +233,28 @@ async def test_process_aexit_cancellation_closes_standard_streams( @pytest.mark.parametrize( "argname, argvalue_factory", [ - pytest.param("user", lambda: os.getuid(), id="user"), - pytest.param("group", lambda: os.getgid(), id="group"), + pytest.param( + "user", + lambda: os.getuid(), + id="user", + marks=[ + pytest.mark.skipif( + platform.system() == "Windows", + reason="os.getuid() is not available on Windows", + ) + ], + ), + pytest.param( + "group", + lambda: os.getgid(), + id="user", + marks=[ + pytest.mark.skipif( + platform.system() == "Windows", + reason="os.getgid() is not available on Windows", + ) + ], + ), pytest.param("extra_groups", list, id="extra_groups"), pytest.param("umask", lambda: 0, id="umask"), ], From 9c683842b68bd07425d65f827e09f24ecb4bb313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 23 Jun 2024 18:13:47 +0300 Subject: [PATCH 05/16] Added issue reference --- docs/versionhistory.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index db4d20aa..191ad61d 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -11,6 +11,7 @@ This library adheres to `Semantic Versioning 2.0 `_. - Added support for more keyword arguments for ``run_process()`` and ``open_process()``: ``preexec_fn``, ``startupinfo``, ``creationflags``, ``user``, ``group``, ``extra_groups`` and ``umask`` + (`#742 `_) - Improved the type annotations in ``run_process()`` and ``open_process()`` to allow for path-like arguments, just like ``subprocess.Popen`` - Changed the ``ResourceWarning`` from an unclosed memory object stream to include its From e32c5d776110dcd4bd48f7bf3d125e9bc9b3cbba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Wed, 26 Jun 2024 22:21:02 +0300 Subject: [PATCH 06/16] Removed support for preexec_fn It doesn't seem to work well on any backend. --- docs/versionhistory.rst | 3 +- src/anyio/_core/_subprocesses.py | 50 +++++++++----------------------- 2 files changed, 14 insertions(+), 39 deletions(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 191ad61d..76f3cee6 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -9,8 +9,7 @@ This library adheres to `Semantic Versioning 2.0 `_. in ``anyio.Path``, newly added in Python 3.13 (`#737 `_) - Added support for more keyword arguments for ``run_process()`` and ``open_process()``: - ``preexec_fn``, ``startupinfo``, ``creationflags``, ``user``, ``group``, - ``extra_groups`` and ``umask`` + ``startupinfo``, ``creationflags``, ``user``, ``group``, ``extra_groups`` and ``umask`` (`#742 `_) - Improved the type annotations in ``run_process()`` and ``open_process()`` to allow for path-like arguments, just like ``subprocess.Popen`` diff --git a/src/anyio/_core/_subprocesses.py b/src/anyio/_core/_subprocesses.py index ae1a9e4c..540d19a1 100644 --- a/src/anyio/_core/_subprocesses.py +++ b/src/anyio/_core/_subprocesses.py @@ -1,9 +1,8 @@ from __future__ import annotations import sys -from collections.abc import AsyncIterable, Callable, Iterable, Mapping, Sequence +from collections.abc import AsyncIterable, Iterable, Mapping, Sequence from io import BytesIO -from os import PathLike from subprocess import DEVNULL, PIPE, CalledProcessError, CompletedProcess from typing import IO, Any, cast @@ -21,7 +20,6 @@ async def run_process( input: bytes | None = None, stdout: int | IO[Any] | None = PIPE, stderr: int | IO[Any] | None = PIPE, - preexec_fn: Callable[[], Any] | None = None, check: bool = True, cwd: StrOrBytesPath | None = None, env: Mapping[str, str] | None = None, @@ -45,8 +43,6 @@ async def run_process( a file-like object, or `None` :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, :data:`subprocess.STDOUT`, a file-like object, or `None` - :param preexec_fn: a callable that is called in the child process just before the - actual command is executed :param check: if ``True``, raise :exc:`~subprocess.CalledProcessError` if the process terminates with a return code other than 0 :param cwd: If not ``None``, change the working directory to this before running the @@ -83,7 +79,6 @@ async def drain_stream(stream: AsyncIterable[bytes], index: int) -> None: stdin=PIPE if input else DEVNULL, stdout=stdout, stderr=stderr, - preexec_fn=preexec_fn, cwd=cwd, env=env, startupinfo=startupinfo, @@ -121,7 +116,6 @@ async def open_process( stdin: int | IO[Any] | None = PIPE, stdout: int | IO[Any] | None = PIPE, stderr: int | IO[Any] | None = PIPE, - preexec_fn: Callable[[], Any] | None = None, cwd: StrOrBytesPath | None = None, env: Mapping[str, str] | None = None, startupinfo: Any = None, @@ -145,8 +139,6 @@ async def open_process( a file-like object, or ``None`` :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, :data:`subprocess.STDOUT`, a file-like object, or ``None`` - :param preexec_fn: a callable that is called in the child process just before the - actual command is executed :param cwd: If not ``None``, the working directory is changed before executing :param env: If env is not ``None``, it must be a mapping that defines the environment variables for the new process @@ -190,31 +182,15 @@ async def open_process( kwargs["umask"] = umask - if isinstance(command, (str, bytes, PathLike)): - return await get_async_backend().open_process( - command, - stdin=stdin, - stdout=stdout, - stderr=stderr, - preexec_fn=preexec_fn, - cwd=cwd, - env=env, - startupinfo=startupinfo, - creationflags=creationflags, - start_new_session=start_new_session, - **kwargs, - ) - else: - return await get_async_backend().open_process( - command, - stdin=stdin, - stdout=stdout, - stderr=stderr, - preexec_fn=preexec_fn, - cwd=cwd, - env=env, - startupinfo=startupinfo, - creationflags=creationflags, - start_new_session=start_new_session, - **kwargs, - ) + return await get_async_backend().open_process( + command, + stdin=stdin, + stdout=stdout, + stderr=stderr, + cwd=cwd, + env=env, + startupinfo=startupinfo, + creationflags=creationflags, + start_new_session=start_new_session, + **kwargs, + ) From d7875a6f71bdae6ba8ab08e380dd2233c90fd687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Wed, 26 Jun 2024 22:46:30 +0300 Subject: [PATCH 07/16] Fixed unresolved forward reference when building the docs --- src/anyio/_core/_subprocesses.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/anyio/_core/_subprocesses.py b/src/anyio/_core/_subprocesses.py index 540d19a1..ac3f9948 100644 --- a/src/anyio/_core/_subprocesses.py +++ b/src/anyio/_core/_subprocesses.py @@ -3,16 +3,18 @@ import sys from collections.abc import AsyncIterable, Iterable, Mapping, Sequence from io import BytesIO +from os import PathLike from subprocess import DEVNULL, PIPE, CalledProcessError, CompletedProcess -from typing import IO, Any, cast +from typing import IO, Any, TypeAlias, Union, cast from ..abc import Process -from ..abc._eventloop import StrOrBytesPath from ._eventloop import get_async_backend from ._tasks import create_task_group PY39_ARGS = frozenset(["user", "group", "extra_groups", "umask"]) +StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]"] + async def run_process( command: StrOrBytesPath | Sequence[StrOrBytesPath], From dabcd78e3550135750f2a5014cbba922d13b3d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Wed, 26 Jun 2024 23:01:05 +0300 Subject: [PATCH 08/16] Fixed import errors on earlier Pythons --- src/anyio/_core/_subprocesses.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/anyio/_core/_subprocesses.py b/src/anyio/_core/_subprocesses.py index ac3f9948..6ecfd7da 100644 --- a/src/anyio/_core/_subprocesses.py +++ b/src/anyio/_core/_subprocesses.py @@ -5,12 +5,17 @@ from io import BytesIO from os import PathLike from subprocess import DEVNULL, PIPE, CalledProcessError, CompletedProcess -from typing import IO, Any, TypeAlias, Union, cast +from typing import IO, Any, Union, cast from ..abc import Process from ._eventloop import get_async_backend from ._tasks import create_task_group +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + from typing_extensions import TypeAlias + PY39_ARGS = frozenset(["user", "group", "extra_groups", "umask"]) StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]"] From 5515abb5f921a32b705874a5f9fd76ea409814f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 29 Aug 2024 16:18:49 +0300 Subject: [PATCH 09/16] Removed unused frozenset --- src/anyio/_core/_subprocesses.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/anyio/_core/_subprocesses.py b/src/anyio/_core/_subprocesses.py index 6ecfd7da..5a043082 100644 --- a/src/anyio/_core/_subprocesses.py +++ b/src/anyio/_core/_subprocesses.py @@ -16,8 +16,6 @@ else: from typing_extensions import TypeAlias -PY39_ARGS = frozenset(["user", "group", "extra_groups", "umask"]) - StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]"] From 04bee4a6cec59bfb76931c41355bcfa52546ee8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 29 Aug 2024 23:18:57 +0300 Subject: [PATCH 10/16] Added support for PathLike[bytes] and convert all command arguments to str on Trio --- src/anyio/_backends/_asyncio.py | 4 ++-- src/anyio/_backends/_trio.py | 12 ++++++++++-- src/anyio/_core/_subprocesses.py | 2 +- src/anyio/abc/_eventloop.py | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 1b7d15ed..1022beb2 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -4,7 +4,7 @@ import asyncio import concurrent.futures import math -import pathlib +import os import socket import sys import threading @@ -2255,7 +2255,7 @@ async def open_process( ) -> Process: await cls.checkpoint() if isinstance(command, PathLike): - command = str(pathlib.Path(command)) + command = os.fspath(command) if isinstance(command, (str, bytes)): process = await asyncio.create_subprocess_shell( diff --git a/src/anyio/_backends/_trio.py b/src/anyio/_backends/_trio.py index 5ebe6dba..61205009 100644 --- a/src/anyio/_backends/_trio.py +++ b/src/anyio/_backends/_trio.py @@ -2,6 +2,7 @@ import array import math +import os import socket import sys import types @@ -973,9 +974,16 @@ async def open_process( stderr: int | IO[Any] | None, **kwargs: Any, ) -> Process: + def convert_item(item: StrOrBytesPath) -> str: + str_or_bytes = os.fspath(item) + if isinstance(str_or_bytes, str): + return str_or_bytes + else: + return os.fsdecode(str_or_bytes) + if isinstance(command, (str, bytes, PathLike)): process = await trio.lowlevel.open_process( - command, + convert_item(command), stdin=stdin, stdout=stdout, stderr=stderr, @@ -984,7 +992,7 @@ async def open_process( ) else: process = await trio.lowlevel.open_process( - command, + [convert_item(item) for item in command], stdin=stdin, stdout=stdout, stderr=stderr, diff --git a/src/anyio/_core/_subprocesses.py b/src/anyio/_core/_subprocesses.py index 5a043082..e84818a8 100644 --- a/src/anyio/_core/_subprocesses.py +++ b/src/anyio/_core/_subprocesses.py @@ -16,7 +16,7 @@ else: from typing_extensions import TypeAlias -StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]"] +StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]", "PathLike[bytes]"] async def run_process( diff --git a/src/anyio/abc/_eventloop.py b/src/anyio/abc/_eventloop.py index ee29b8ad..258d2e1d 100644 --- a/src/anyio/abc/_eventloop.py +++ b/src/anyio/abc/_eventloop.py @@ -50,7 +50,7 @@ T_Retval = TypeVar("T_Retval") PosArgsT = TypeVarTuple("PosArgsT") -StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]"] +StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]", "PathLike[bytes]"] class AsyncBackend(metaclass=ABCMeta): From 865fd207b845567c92c74ea5d1cc15f97cb526ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 29 Aug 2024 23:28:52 +0300 Subject: [PATCH 11/16] Added support for pass_fds --- docs/versionhistory.rst | 3 ++- src/anyio/_core/_subprocesses.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 4b57d0e5..35a2852c 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -11,7 +11,8 @@ This library adheres to `Semantic Versioning 2.0 `_. in ``anyio.Path``, newly added in Python 3.13 (`#737 `_) - Added support for more keyword arguments for ``run_process()`` and ``open_process()``: - ``startupinfo``, ``creationflags``, ``user``, ``group``, ``extra_groups`` and ``umask`` + ``startupinfo``, ``creationflags``, ``pass_fds``, ``user``, ``group``, + ``extra_groups`` and ``umask`` (`#742 `_) - Improved the type annotations in ``run_process()`` and ``open_process()`` to allow for path-like arguments, just like ``subprocess.Popen`` diff --git a/src/anyio/_core/_subprocesses.py b/src/anyio/_core/_subprocesses.py index e84818a8..1f8bd747 100644 --- a/src/anyio/_core/_subprocesses.py +++ b/src/anyio/_core/_subprocesses.py @@ -126,6 +126,7 @@ async def open_process( startupinfo: Any = None, creationflags: int = 0, start_new_session: bool = False, + pass_fds: Sequence[int] = (), user: str | int | None = None, group: str | int | None = None, extra_groups: Iterable[str | int] | None = None, @@ -153,6 +154,8 @@ async def open_process( to specify process startup parameters (Windows only) :param start_new_session: if ``true`` the setsid() system call will be made in the child process prior to the execution of the subprocess. (POSIX only) + :param pass_fds: sequence of file descriptors to keep open between the parent and + child processes. (POSIX only) :param user: effective user to run the process as (Python >= 3.9; POSIX only) :param group: effective group to run the process as (Python >= 3.9; POSIX only) :param extra_groups: supplementary groups to set in the subprocess (Python >= 3.9; @@ -197,5 +200,6 @@ async def open_process( startupinfo=startupinfo, creationflags=creationflags, start_new_session=start_new_session, + pass_fds=pass_fds, **kwargs, ) From a9ee9e021e1a0b28b8c9848bc29ca4f7c139b5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 1 Sep 2024 13:00:54 +0300 Subject: [PATCH 12/16] Added missing pass_fds parameter to run_process() --- src/anyio/_core/_subprocesses.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/anyio/_core/_subprocesses.py b/src/anyio/_core/_subprocesses.py index 1f8bd747..1ac2d549 100644 --- a/src/anyio/_core/_subprocesses.py +++ b/src/anyio/_core/_subprocesses.py @@ -31,6 +31,7 @@ async def run_process( startupinfo: Any = None, creationflags: int = 0, start_new_session: bool = False, + pass_fds: Sequence[int] = (), user: str | int | None = None, group: str | int | None = None, extra_groups: Iterable[str | int] | None = None, @@ -60,6 +61,8 @@ async def run_process( subprocess (see :class:`subprocess.Popen` for the specifics) :param start_new_session: if ``true`` the setsid() system call will be made in the child process prior to the execution of the subprocess. (POSIX only) + :param pass_fds: sequence of file descriptors to keep open between the parent and + child processes. (POSIX only) :param user: effective user to run the process as (Python >= 3.9, POSIX only) :param group: effective group to run the process as (Python >= 3.9, POSIX only) :param extra_groups: supplementary groups to set in the subprocess (Python >= 3.9, @@ -89,6 +92,7 @@ async def drain_stream(stream: AsyncIterable[bytes], index: int) -> None: startupinfo=startupinfo, creationflags=creationflags, start_new_session=start_new_session, + pass_fds=pass_fds, user=user, group=group, extra_groups=extra_groups, From 089499fc0aa59ca4346910d58fc3fc525ee81a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 1 Sep 2024 13:10:24 +0300 Subject: [PATCH 13/16] Imported StrOrBytesPath from _typeshed --- src/anyio/_backends/_asyncio.py | 6 +++++- src/anyio/_backends/_trio.py | 6 +++++- src/anyio/_core/_subprocesses.py | 11 +++-------- src/anyio/abc/_eventloop.py | 10 ++-------- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 75a39495..9589704c 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -41,6 +41,7 @@ from types import TracebackType from typing import ( IO, + TYPE_CHECKING, Any, AsyncGenerator, Awaitable, @@ -80,7 +81,6 @@ UDPPacketType, UNIXDatagramPacketType, ) -from ..abc._eventloop import StrOrBytesPath from ..lowlevel import RunVar from ..streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream @@ -278,6 +278,10 @@ def _do_shutdown(future: asyncio.futures.Future) -> None: thread.join() +if TYPE_CHECKING: + from _typeshed import StrOrBytesPath + + T_Retval = TypeVar("T_Retval") T_contra = TypeVar("T_contra", contravariant=True) PosArgsT = TypeVarTuple("PosArgsT") diff --git a/src/anyio/_backends/_trio.py b/src/anyio/_backends/_trio.py index 61205009..13569e90 100644 --- a/src/anyio/_backends/_trio.py +++ b/src/anyio/_backends/_trio.py @@ -18,6 +18,7 @@ from types import TracebackType from typing import ( IO, + TYPE_CHECKING, Any, AsyncGenerator, Awaitable, @@ -60,7 +61,7 @@ from .._core._synchronization import ResourceGuard from .._core._tasks import CancelScope as BaseCancelScope from ..abc import IPSockAddrType, UDPPacketType, UNIXDatagramPacketType -from ..abc._eventloop import AsyncBackend, StrOrBytesPath +from ..abc._eventloop import AsyncBackend from ..streams.memory import MemoryObjectSendStream if sys.version_info >= (3, 10): @@ -74,6 +75,9 @@ from exceptiongroup import BaseExceptionGroup from typing_extensions import TypeVarTuple, Unpack +if TYPE_CHECKING: + from _typeshed import StrOrBytesPath + T = TypeVar("T") T_Retval = TypeVar("T_Retval") T_SockAddr = TypeVar("T_SockAddr", str, IPSockAddrType) diff --git a/src/anyio/_core/_subprocesses.py b/src/anyio/_core/_subprocesses.py index 1ac2d549..737e4135 100644 --- a/src/anyio/_core/_subprocesses.py +++ b/src/anyio/_core/_subprocesses.py @@ -3,20 +3,15 @@ import sys from collections.abc import AsyncIterable, Iterable, Mapping, Sequence from io import BytesIO -from os import PathLike from subprocess import DEVNULL, PIPE, CalledProcessError, CompletedProcess -from typing import IO, Any, Union, cast +from typing import IO, TYPE_CHECKING, Any, cast from ..abc import Process from ._eventloop import get_async_backend from ._tasks import create_task_group -if sys.version_info >= (3, 10): - from typing import TypeAlias -else: - from typing_extensions import TypeAlias - -StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]", "PathLike[bytes]"] +if TYPE_CHECKING: + from _typeshed import StrOrBytesPath async def run_process( diff --git a/src/anyio/abc/_eventloop.py b/src/anyio/abc/_eventloop.py index 258d2e1d..127d9401 100644 --- a/src/anyio/abc/_eventloop.py +++ b/src/anyio/abc/_eventloop.py @@ -4,7 +4,6 @@ import sys from abc import ABCMeta, abstractmethod from collections.abc import AsyncIterator, Awaitable -from os import PathLike from signal import Signals from socket import AddressFamily, SocketKind, socket from typing import ( @@ -15,7 +14,6 @@ ContextManager, Sequence, TypeVar, - Union, overload, ) @@ -24,12 +22,9 @@ else: from typing_extensions import TypeVarTuple, Unpack -if sys.version_info >= (3, 10): - from typing import TypeAlias -else: - from typing_extensions import TypeAlias - if TYPE_CHECKING: + from _typeshed import StrOrBytesPath + from .._core._synchronization import CapacityLimiter, Event from .._core._tasks import CancelScope from .._core._testing import TaskInfo @@ -50,7 +45,6 @@ T_Retval = TypeVar("T_Retval") PosArgsT = TypeVarTuple("PosArgsT") -StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]", "PathLike[bytes]"] class AsyncBackend(metaclass=ABCMeta): From 3622381218db8659b1240cfae7e9adaa282507ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 1 Sep 2024 16:56:53 +0300 Subject: [PATCH 14/16] Revert "Imported StrOrBytesPath from _typeshed" This reverts commit 089499fc0aa59ca4346910d58fc3fc525ee81a8d. --- src/anyio/_backends/_asyncio.py | 6 +----- src/anyio/_backends/_trio.py | 6 +----- src/anyio/_core/_subprocesses.py | 6 ++---- src/anyio/abc/_eventloop.py | 10 ++++++++-- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 9589704c..75a39495 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -41,7 +41,6 @@ from types import TracebackType from typing import ( IO, - TYPE_CHECKING, Any, AsyncGenerator, Awaitable, @@ -81,6 +80,7 @@ UDPPacketType, UNIXDatagramPacketType, ) +from ..abc._eventloop import StrOrBytesPath from ..lowlevel import RunVar from ..streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream @@ -278,10 +278,6 @@ def _do_shutdown(future: asyncio.futures.Future) -> None: thread.join() -if TYPE_CHECKING: - from _typeshed import StrOrBytesPath - - T_Retval = TypeVar("T_Retval") T_contra = TypeVar("T_contra", contravariant=True) PosArgsT = TypeVarTuple("PosArgsT") diff --git a/src/anyio/_backends/_trio.py b/src/anyio/_backends/_trio.py index 13569e90..61205009 100644 --- a/src/anyio/_backends/_trio.py +++ b/src/anyio/_backends/_trio.py @@ -18,7 +18,6 @@ from types import TracebackType from typing import ( IO, - TYPE_CHECKING, Any, AsyncGenerator, Awaitable, @@ -61,7 +60,7 @@ from .._core._synchronization import ResourceGuard from .._core._tasks import CancelScope as BaseCancelScope from ..abc import IPSockAddrType, UDPPacketType, UNIXDatagramPacketType -from ..abc._eventloop import AsyncBackend +from ..abc._eventloop import AsyncBackend, StrOrBytesPath from ..streams.memory import MemoryObjectSendStream if sys.version_info >= (3, 10): @@ -75,9 +74,6 @@ from exceptiongroup import BaseExceptionGroup from typing_extensions import TypeVarTuple, Unpack -if TYPE_CHECKING: - from _typeshed import StrOrBytesPath - T = TypeVar("T") T_Retval = TypeVar("T_Retval") T_SockAddr = TypeVar("T_SockAddr", str, IPSockAddrType) diff --git a/src/anyio/_core/_subprocesses.py b/src/anyio/_core/_subprocesses.py index 737e4135..4f94951b 100644 --- a/src/anyio/_core/_subprocesses.py +++ b/src/anyio/_core/_subprocesses.py @@ -4,15 +4,13 @@ from collections.abc import AsyncIterable, Iterable, Mapping, Sequence from io import BytesIO from subprocess import DEVNULL, PIPE, CalledProcessError, CompletedProcess -from typing import IO, TYPE_CHECKING, Any, cast +from typing import IO, Any, cast from ..abc import Process +from ..abc._eventloop import StrOrBytesPath from ._eventloop import get_async_backend from ._tasks import create_task_group -if TYPE_CHECKING: - from _typeshed import StrOrBytesPath - async def run_process( command: StrOrBytesPath | Sequence[StrOrBytesPath], diff --git a/src/anyio/abc/_eventloop.py b/src/anyio/abc/_eventloop.py index 127d9401..258d2e1d 100644 --- a/src/anyio/abc/_eventloop.py +++ b/src/anyio/abc/_eventloop.py @@ -4,6 +4,7 @@ import sys from abc import ABCMeta, abstractmethod from collections.abc import AsyncIterator, Awaitable +from os import PathLike from signal import Signals from socket import AddressFamily, SocketKind, socket from typing import ( @@ -14,6 +15,7 @@ ContextManager, Sequence, TypeVar, + Union, overload, ) @@ -22,9 +24,12 @@ else: from typing_extensions import TypeVarTuple, Unpack -if TYPE_CHECKING: - from _typeshed import StrOrBytesPath +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + from typing_extensions import TypeAlias +if TYPE_CHECKING: from .._core._synchronization import CapacityLimiter, Event from .._core._tasks import CancelScope from .._core._testing import TaskInfo @@ -45,6 +50,7 @@ T_Retval = TypeVar("T_Retval") PosArgsT = TypeVarTuple("PosArgsT") +StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]", "PathLike[bytes]"] class AsyncBackend(metaclass=ABCMeta): From a6f7448735f3d97f209800bc31c1b090708245ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 1 Sep 2024 17:12:47 +0300 Subject: [PATCH 15/16] Reverted completely to original type aliases --- src/anyio/_core/_subprocesses.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/anyio/_core/_subprocesses.py b/src/anyio/_core/_subprocesses.py index 4f94951b..1ac2d549 100644 --- a/src/anyio/_core/_subprocesses.py +++ b/src/anyio/_core/_subprocesses.py @@ -3,14 +3,21 @@ import sys from collections.abc import AsyncIterable, Iterable, Mapping, Sequence from io import BytesIO +from os import PathLike from subprocess import DEVNULL, PIPE, CalledProcessError, CompletedProcess -from typing import IO, Any, cast +from typing import IO, Any, Union, cast from ..abc import Process -from ..abc._eventloop import StrOrBytesPath from ._eventloop import get_async_backend from ._tasks import create_task_group +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + from typing_extensions import TypeAlias + +StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]", "PathLike[bytes]"] + async def run_process( command: StrOrBytesPath | Sequence[StrOrBytesPath], From ecd6c13ec04db486fabf86c4e2a3406f030b981f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Mon, 2 Sep 2024 18:30:27 +0300 Subject: [PATCH 16/16] Tweaked the changelog entry --- docs/versionhistory.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index cf15e37b..2eb72bc7 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -14,8 +14,8 @@ This library adheres to `Semantic Versioning 2.0 `_. ``startupinfo``, ``creationflags``, ``pass_fds``, ``user``, ``group``, ``extra_groups`` and ``umask`` (`#742 `_) -- Improved the type annotations in ``run_process()`` and ``open_process()`` to allow for - path-like arguments, just like ``subprocess.Popen`` +- Improved the type annotations and support for ``PathLike`` in ``run_process()`` and + ``open_process()`` to allow for path-like arguments, just like ``subprocess.Popen`` - Changed the ``ResourceWarning`` from an unclosed memory object stream to include its address for easier identification - Changed ``start_blocking_portal()`` to always use daemonic threads, to accommodate the