Blame SOURCES/python3.13-PR-2959.patch

7825d3
From 86182cb193a71533a7a12b8d45a6ad4103d41ccc Mon Sep 17 00:00:00 2001
7825d3
From: Dave Hall <dave@etianen.com>
7825d3
Date: Mon, 11 Mar 2024 21:35:12 +0000
7825d3
Subject: [PATCH] `Path` refactor (#2959)
7825d3
7825d3
---
7825d3
 docs/source/reference-io.rst   |   5 +
7825d3
 newsfragments/2959.feature.rst |   5 +
7825d3
 trio/__init__.py               |   2 +-
7825d3
 trio/_path.py                  | 537 ++++++++++-----------------------
7825d3
 trio/_tests/test_exports.py    |  41 +--
7825d3
 trio/_tests/test_path.py       |  48 +--
7825d3
 6 files changed, 193 insertions(+), 445 deletions(-)
7825d3
 create mode 100644 newsfragments/2959.feature.rst
7825d3
7825d3
diff --git a/docs/source/reference-io.rst b/docs/source/reference-io.rst
7825d3
index d0525e3..45752c1 100644
7825d3
--- a/docs/source/reference-io.rst
7825d3
+++ b/docs/source/reference-io.rst
7825d3
@@ -631,6 +631,11 @@ Asynchronous path objects
7825d3
 
7825d3
 .. autoclass:: Path
7825d3
    :members:
7825d3
+   :inherited-members:
7825d3
+
7825d3
+.. autoclass:: PosixPath
7825d3
+
7825d3
+.. autoclass:: WindowsPath
7825d3
 
7825d3
 
7825d3
 .. _async-file-objects:
7825d3
diff --git a/newsfragments/2959.feature.rst b/newsfragments/2959.feature.rst
7825d3
new file mode 100644
7825d3
index 0000000..ca90d56
7825d3
--- /dev/null
7825d3
+++ b/newsfragments/2959.feature.rst
7825d3
@@ -0,0 +1,5 @@
7825d3
+:class:`Path` is now a subclass of :class:`pathlib.PurePath`, allowing it to interoperate with other standard
7825d3
+:mod:`pathlib` types.
7825d3
+
7825d3
+Instantiating :class:`Path` now returns a concrete platform-specific subclass, one of :class:`PosixPath` or
7825d3
+:class:`WindowsPath`, matching the behavior of :class:`pathlib.Path`.
7825d3
diff --git a/trio/__init__.py b/trio/__init__.py
7825d3
index 5adf146..8fdee02 100644
7825d3
--- a/trio/__init__.py
7825d3
+++ b/trio/__init__.py
7825d3
@@ -74,7 +74,7 @@ from ._highlevel_ssl_helpers import (
7825d3
     open_ssl_over_tcp_stream as open_ssl_over_tcp_stream,
7825d3
     serve_ssl_over_tcp as serve_ssl_over_tcp,
7825d3
 )
7825d3
-from ._path import Path as Path
7825d3
+from ._path import Path as Path, PosixPath as PosixPath, WindowsPath as WindowsPath
7825d3
 from ._signals import open_signal_receiver as open_signal_receiver
7825d3
 from ._ssl import (
7825d3
     NeedHandshakeError as NeedHandshakeError,
7825d3
diff --git a/trio/_path.py b/trio/_path.py
7825d3
index 508ad5d..eb06a6b 100644
7825d3
--- a/trio/_path.py
7825d3
+++ b/trio/_path.py
7825d3
@@ -1,28 +1,18 @@
7825d3
 from __future__ import annotations
7825d3
 
7825d3
-import inspect
7825d3
 import os
7825d3
 import pathlib
7825d3
 import sys
7825d3
-import types
7825d3
-from collections.abc import Awaitable, Callable, Iterable, Sequence
7825d3
-from functools import partial
7825d3
+from collections.abc import Awaitable, Callable, Iterable
7825d3
 from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper
7825d3
-from typing import (
7825d3
-    IO,
7825d3
-    TYPE_CHECKING,
7825d3
-    Any,
7825d3
-    BinaryIO,
7825d3
-    ClassVar,
7825d3
-    TypeVar,
7825d3
-    Union,
7825d3
-    cast,
7825d3
-    overload,
7825d3
-)
7825d3
-
7825d3
-import trio
7825d3
-from trio._file_io import AsyncIOWrapper as _AsyncIOWrapper
7825d3
-from trio._util import async_wraps, final, wraps
7825d3
+from functools import partial, update_wrapper
7825d3
+from inspect import cleandoc
7825d3
+from typing import IO, TYPE_CHECKING, Any, BinaryIO, ClassVar, TypeVar, overload
7825d3
+
7825d3
+from trio._file_io import AsyncIOWrapper, wrap_file
7825d3
+from trio._util import final
7825d3
+from trio.to_thread import run_sync
7825d3
+
7825d3
 
7825d3
 if TYPE_CHECKING:
7825d3
     from _typeshed import (
7825d3
@@ -32,217 +22,103 @@ if TYPE_CHECKING:
7825d3
         OpenBinaryModeWriting,
7825d3
         OpenTextMode,
7825d3
     )
7825d3
-    from typing_extensions import Concatenate, Literal, ParamSpec, TypeAlias
7825d3
+    from typing_extensions import Concatenate, Literal, ParamSpec, Self
7825d3
 
7825d3
     P = ParamSpec("P")
7825d3
 
7825d3
-T = TypeVar("T")
7825d3
-StrPath: TypeAlias = Union[str, "os.PathLike[str]"]  # Only subscriptable in 3.9+
7825d3
-
7825d3
-
7825d3
-# re-wrap return value from methods that return new instances of pathlib.Path
7825d3
-def rewrap_path(value: T) -> T | Path:
7825d3
-    if isinstance(value, pathlib.Path):
7825d3
-        return Path(value)
7825d3
-    else:
7825d3
-        return value
7825d3
+    PathT = TypeVar("PathT", bound="Path")
7825d3
+    T = TypeVar("T")
7825d3
 
7825d3
 
7825d3
-def _forward_factory(
7825d3
-    cls: AsyncAutoWrapperType,
7825d3
-    attr_name: str,
7825d3
-    attr: Callable[Concatenate[pathlib.Path, P], T],
7825d3
-) -> Callable[Concatenate[Path, P], T | Path]:
7825d3
-    @wraps(attr)
7825d3
-    def wrapper(self: Path, *args: P.args, **kwargs: P.kwargs) -> T | Path:
7825d3
-        attr = getattr(self._wrapped, attr_name)
7825d3
-        value = attr(*args, **kwargs)
7825d3
-        return rewrap_path(value)
7825d3
+def _wraps_async(
7825d3
+    wrapped: Callable[..., Any]
7825d3
+) -> Callable[[Callable[P, T]], Callable[P, Awaitable[T]]]:
7825d3
+    def decorator(fn: Callable[P, T]) -> Callable[P, Awaitable[T]]:
7825d3
+        async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
7825d3
+            return await run_sync(partial(fn, *args, **kwargs))
7825d3
 
7825d3
-    # Assigning this makes inspect and therefore Sphinx show the original parameters.
7825d3
-    # It's not defined on functions normally though, this is a custom attribute.
7825d3
-    assert isinstance(wrapper, types.FunctionType)
7825d3
-    wrapper.__signature__ = inspect.signature(attr)
7825d3
-
7825d3
-    return wrapper
7825d3
+        update_wrapper(wrapper, wrapped)
7825d3
+        assert wrapped.__doc__ is not None
7825d3
+        wrapper.__doc__ = (
7825d3
+            f"Like :meth:`~{wrapped.__module__}.{wrapped.__qualname__}`, but async.\n"
7825d3
+            f"\n"
7825d3
+            f"{cleandoc(wrapped.__doc__)}\n"
7825d3
+        )
7825d3
+        return wrapper
7825d3
 
7825d3
+    return decorator
7825d3
 
7825d3
-def _forward_magic(
7825d3
-    cls: AsyncAutoWrapperType, attr: Callable[..., T]
7825d3
-) -> Callable[..., Path | T]:
7825d3
-    sentinel = object()
7825d3
 
7825d3
-    @wraps(attr)
7825d3
-    def wrapper(self: Path, other: object = sentinel) -> Path | T:
7825d3
-        if other is sentinel:
7825d3
-            return attr(self._wrapped)
7825d3
-        if isinstance(other, cls):
7825d3
-            other = cast(Path, other)._wrapped
7825d3
-        value = attr(self._wrapped, other)
7825d3
-        return rewrap_path(value)
7825d3
+def _wrap_method(
7825d3
+    fn: Callable[Concatenate[pathlib.Path, P], T],
7825d3
+) -> Callable[Concatenate[Path, P], Awaitable[T]]:
7825d3
+    @_wraps_async(fn)
7825d3
+    def wrapper(self: Path, /, *args: P.args, **kwargs: P.kwargs) -> T:
7825d3
+        return fn(self._wrapped_cls(self), *args, **kwargs)
7825d3
 
7825d3
-    assert isinstance(wrapper, types.FunctionType)
7825d3
-    wrapper.__signature__ = inspect.signature(attr)
7825d3
     return wrapper
7825d3
 
7825d3
 
7825d3
-def iter_wrapper_factory(
7825d3
-    cls: AsyncAutoWrapperType, meth_name: str
7825d3
-) -> Callable[Concatenate[Path, P], Awaitable[Iterable[Path]]]:
7825d3
-    @async_wraps(cls, cls._wraps, meth_name)
7825d3
-    async def wrapper(self: Path, *args: P.args, **kwargs: P.kwargs) -> Iterable[Path]:
7825d3
-        meth = getattr(self._wrapped, meth_name)
7825d3
-        func = partial(meth, *args, **kwargs)
7825d3
-        # Make sure that the full iteration is performed in the thread
7825d3
-        # by converting the generator produced by pathlib into a list
7825d3
-        items = await trio.to_thread.run_sync(lambda: list(func()))
7825d3
-        return (rewrap_path(item) for item in items)
7825d3
+def _wrap_method_path(
7825d3
+    fn: Callable[Concatenate[pathlib.Path, P], pathlib.Path],
7825d3
+) -> Callable[Concatenate[PathT, P], Awaitable[PathT]]:
7825d3
+    @_wraps_async(fn)
7825d3
+    def wrapper(self: PathT, /, *args: P.args, **kwargs: P.kwargs) -> PathT:
7825d3
+        return self.__class__(fn(self._wrapped_cls(self), *args, **kwargs))
7825d3
 
7825d3
     return wrapper
7825d3
 
7825d3
 
7825d3
-def thread_wrapper_factory(
7825d3
-    cls: AsyncAutoWrapperType, meth_name: str
7825d3
-) -> Callable[Concatenate[Path, P], Awaitable[Path]]:
7825d3
-    @async_wraps(cls, cls._wraps, meth_name)
7825d3
-    async def wrapper(self: Path, *args: P.args, **kwargs: P.kwargs) -> Path:
7825d3
-        meth = getattr(self._wrapped, meth_name)
7825d3
-        func = partial(meth, *args, **kwargs)
7825d3
-        value = await trio.to_thread.run_sync(func)
7825d3
-        return rewrap_path(value)
7825d3
-
7825d3
+def _wrap_method_path_iterable(
7825d3
+    fn: Callable[Concatenate[pathlib.Path, P], Iterable[pathlib.Path]],
7825d3
+) -> Callable[Concatenate[PathT, P], Awaitable[Iterable[PathT]]]:
7825d3
+    @_wraps_async(fn)
7825d3
+    def wrapper(self: PathT, /, *args: P.args, **kwargs: P.kwargs) -> Iterable[PathT]:
7825d3
+        return map(self.__class__, [*fn(self._wrapped_cls(self), *args, **kwargs)])
7825d3
+
7825d3
+    assert wrapper.__doc__ is not None
7825d3
+    wrapper.__doc__ += (
7825d3
+        f"\n"
7825d3
+        f"This is an async method that returns a synchronous iterator, so you\n"
7825d3
+        f"use it like:\n"
7825d3
+        f"\n"
7825d3
+        f".. code:: python\n"
7825d3
+        f"\n"
7825d3
+        f"    for subpath in await mypath.{fn.__name__}():\n"
7825d3
+        f"        ...\n"
7825d3
+        f"\n"
7825d3
+        f".. note::\n"
7825d3
+        f"\n"
7825d3
+        f"    The iterator is loaded into memory immediately during the initial\n"
7825d3
+        f"    call (see `issue #501\n"
7825d3
+        f"    <https://github.com/python-trio/trio/issues/501>`__ for discussion).\n"
7825d3
+    )
7825d3
     return wrapper
7825d3
 
7825d3
+class Path(pathlib.PurePath):
7825d3
+    """An async :class:`pathlib.Path` that executes blocking methods in :meth:`trio.to_thread.run_sync`.
7825d3
+    Instantiating :class:`Path` returns a concrete platform-specific subclass, one of :class:`PosixPath` or
7825d3
+    :class:`WindowsPath`.
7825d3
+    """
7825d3
 
7825d3
-def classmethod_wrapper_factory(
7825d3
-    cls: AsyncAutoWrapperType, meth_name: str
7825d3
-) -> classmethod:  # type: ignore[type-arg]
7825d3
-    @async_wraps(cls, cls._wraps, meth_name)
7825d3
-    async def wrapper(cls: type[Path], *args: Any, **kwargs: Any) -> Path:  # type: ignore[misc] # contains Any
7825d3
-        meth = getattr(cls._wraps, meth_name)
7825d3
-        func = partial(meth, *args, **kwargs)
7825d3
-        value = await trio.to_thread.run_sync(func)
7825d3
-        return rewrap_path(value)
7825d3
-
7825d3
-    assert isinstance(wrapper, types.FunctionType)
7825d3
-    wrapper.__signature__ = inspect.signature(getattr(cls._wraps, meth_name))
7825d3
-    return classmethod(wrapper)
7825d3
-
7825d3
-
7825d3
-class AsyncAutoWrapperType(type):
7825d3
-    _forwards: type
7825d3
-    _wraps: type
7825d3
-    _forward_magic: list[str]
7825d3
-    _wrap_iter: list[str]
7825d3
-    _forward: list[str]
7825d3
-
7825d3
-    def __init__(
7825d3
-        cls, name: str, bases: tuple[type, ...], attrs: dict[str, object]
7825d3
-    ) -> None:
7825d3
-        super().__init__(name, bases, attrs)
7825d3
-
7825d3
-        cls._forward = []
7825d3
-        type(cls).generate_forwards(cls, attrs)
7825d3
-        type(cls).generate_wraps(cls, attrs)
7825d3
-        type(cls).generate_magic(cls, attrs)
7825d3
-        type(cls).generate_iter(cls, attrs)
7825d3
-
7825d3
-    def generate_forwards(cls, attrs: dict[str, object]) -> None:
7825d3
-        # forward functions of _forwards
7825d3
-        for attr_name, attr in cls._forwards.__dict__.items():
7825d3
-            if attr_name.startswith("_") or attr_name in attrs:
7825d3
-                continue
7825d3
-
7825d3
-            if isinstance(attr, property):
7825d3
-                cls._forward.append(attr_name)
7825d3
-            elif isinstance(attr, types.FunctionType):
7825d3
-                wrapper = _forward_factory(cls, attr_name, attr)
7825d3
-                setattr(cls, attr_name, wrapper)
7825d3
-            else:
7825d3
-                raise TypeError(attr_name, type(attr))
7825d3
-
7825d3
-    def generate_wraps(cls, attrs: dict[str, object]) -> None:
7825d3
-        # generate wrappers for functions of _wraps
7825d3
-        wrapper: classmethod | Callable[..., object]  # type: ignore[type-arg]
7825d3
-        for attr_name, attr in cls._wraps.__dict__.items():
7825d3
-            # .z. exclude cls._wrap_iter
7825d3
-            if attr_name.startswith("_") or attr_name in attrs:
7825d3
-                continue
7825d3
-            if isinstance(attr, classmethod):
7825d3
-                wrapper = classmethod_wrapper_factory(cls, attr_name)
7825d3
-                setattr(cls, attr_name, wrapper)
7825d3
-            elif isinstance(attr, types.FunctionType):
7825d3
-                wrapper = thread_wrapper_factory(cls, attr_name)
7825d3
-                assert isinstance(wrapper, types.FunctionType)
7825d3
-                wrapper.__signature__ = inspect.signature(attr)
7825d3
-                setattr(cls, attr_name, wrapper)
7825d3
-            else:
7825d3
-                raise TypeError(attr_name, type(attr))
7825d3
-
7825d3
-    def generate_magic(cls, attrs: dict[str, object]) -> None:
7825d3
-        # generate wrappers for magic
7825d3
-        for attr_name in cls._forward_magic:
7825d3
-            attr = getattr(cls._forwards, attr_name)
7825d3
-            wrapper = _forward_magic(cls, attr)
7825d3
-            setattr(cls, attr_name, wrapper)
7825d3
-
7825d3
-    def generate_iter(cls, attrs: dict[str, object]) -> None:
7825d3
-        # generate wrappers for methods that return iterators
7825d3
-        wrapper: Callable[..., object]
7825d3
-        for attr_name, attr in cls._wraps.__dict__.items():
7825d3
-            if attr_name in cls._wrap_iter:
7825d3
-                wrapper = iter_wrapper_factory(cls, attr_name)
7825d3
-                assert isinstance(wrapper, types.FunctionType)
7825d3
-                wrapper.__signature__ = inspect.signature(attr)
7825d3
-                setattr(cls, attr_name, wrapper)
7825d3
-
7825d3
+    __slots__ = ()
7825d3
 
7825d3
-@final
7825d3
-class Path(metaclass=AsyncAutoWrapperType):
7825d3
-    """A :class:`pathlib.Path` wrapper that executes blocking methods in
7825d3
-    :meth:`trio.to_thread.run_sync`.
7825d3
+    _wrapped_cls: ClassVar[type[pathlib.Path]]
7825d3
 
7825d3
-    """
7825d3
+    def __new__(cls, *args: str | os.PathLike[str]) -> Self:
7825d3
+        if cls is Path:
7825d3
+            cls = WindowsPath if os.name == "nt" else PosixPath  # type: ignore[assignment]
7825d3
+        return super().__new__(cls, *args)
7825d3
 
7825d3
-    _forward: ClassVar[list[str]]
7825d3
-    _wraps: ClassVar[type] = pathlib.Path
7825d3
-    _forwards: ClassVar[type] = pathlib.PurePath
7825d3
-    _forward_magic: ClassVar[list[str]] = [
7825d3
-        "__str__",
7825d3
-        "__bytes__",
7825d3
-        "__truediv__",
7825d3
-        "__rtruediv__",
7825d3
-        "__eq__",
7825d3
-        "__lt__",
7825d3
-        "__le__",
7825d3
-        "__gt__",
7825d3
-        "__ge__",
7825d3
-        "__hash__",
7825d3
-    ]
7825d3
-    _wrap_iter: ClassVar[list[str]] = ["glob", "rglob", "iterdir"]
7825d3
-
7825d3
-    def __init__(self, *args: StrPath) -> None:
7825d3
-        self._wrapped = pathlib.Path(*args)
7825d3
-
7825d3
-    # type checkers allow accessing any attributes on class instances with `__getattr__`
7825d3
-    # so we hide it behind a type guard forcing it to rely on the hardcoded attribute
7825d3
-    # list below.
7825d3
-    if not TYPE_CHECKING:
7825d3
-
7825d3
-        def __getattr__(self, name):
7825d3
-            if name in self._forward:
7825d3
-                value = getattr(self._wrapped, name)
7825d3
-                return rewrap_path(value)
7825d3
-            raise AttributeError(name)
7825d3
-
7825d3
-    def __dir__(self) -> list[str]:
7825d3
-        return [*super().__dir__(), *self._forward]
7825d3
-
7825d3
-    def __repr__(self) -> str:
7825d3
-        return f"trio.Path({str(self)!r})"
7825d3
+    @classmethod
7825d3
+    @_wraps_async(pathlib.Path.cwd)
7825d3
+    def cwd(cls) -> Self:
7825d3
+        return cls(pathlib.Path.cwd())
7825d3
 
7825d3
-    def __fspath__(self) -> str:
7825d3
-        return os.fspath(self._wrapped)
7825d3
+    @classmethod
7825d3
+    @_wraps_async(pathlib.Path.home)
7825d3
+    def home(cls) -> Self:
7825d3
+        return cls(pathlib.Path.home())
7825d3
 
7825d3
     @overload
7825d3
     async def open(
7825d3
@@ -252,7 +128,7 @@ class Path(metaclass=AsyncAutoWrapperType):
7825d3
         encoding: str | None = None,
7825d3
         errors: str | None = None,
7825d3
         newline: str | None = None,
7825d3
-    ) -> _AsyncIOWrapper[TextIOWrapper]:
7825d3
+    ) -> AsyncIOWrapper[TextIOWrapper]:
7825d3
         ...
7825d3
 
7825d3
     @overload
7825d3
@@ -263,7 +139,7 @@ class Path(metaclass=AsyncAutoWrapperType):
7825d3
         encoding: None = None,
7825d3
         errors: None = None,
7825d3
         newline: None = None,
7825d3
-    ) -> _AsyncIOWrapper[FileIO]:
7825d3
+    ) -> AsyncIOWrapper[FileIO]:
7825d3
         ...
7825d3
 
7825d3
     @overload
7825d3
@@ -274,7 +150,7 @@ class Path(metaclass=AsyncAutoWrapperType):
7825d3
         encoding: None = None,
7825d3
         errors: None = None,
7825d3
         newline: None = None,
7825d3
-    ) -> _AsyncIOWrapper[BufferedRandom]:
7825d3
+    ) -> AsyncIOWrapper[BufferedRandom]:
7825d3
         ...
7825d3
 
7825d3
     @overload
7825d3
@@ -285,7 +161,7 @@ class Path(metaclass=AsyncAutoWrapperType):
7825d3
         encoding: None = None,
7825d3
         errors: None = None,
7825d3
         newline: None = None,
7825d3
-    ) -> _AsyncIOWrapper[BufferedWriter]:
7825d3
+    ) -> AsyncIOWrapper[BufferedWriter]:
7825d3
         ...
7825d3
 
7825d3
     @overload
7825d3
@@ -296,7 +172,7 @@ class Path(metaclass=AsyncAutoWrapperType):
7825d3
         encoding: None = None,
7825d3
         errors: None = None,
7825d3
         newline: None = None,
7825d3
-    ) -> _AsyncIOWrapper[BufferedReader]:
7825d3
+    ) -> AsyncIOWrapper[BufferedReader]:
7825d3
         ...
7825d3
 
7825d3
     @overload
7825d3
@@ -307,7 +183,7 @@ class Path(metaclass=AsyncAutoWrapperType):
7825d3
         encoding: None = None,
7825d3
         errors: None = None,
7825d3
         newline: None = None,
7825d3
-    ) -> _AsyncIOWrapper[BinaryIO]:
7825d3
+    ) -> AsyncIOWrapper[BinaryIO]:
7825d3
         ...
7825d3
 
7825d3
     @overload
7825d3
@@ -318,173 +194,74 @@ class Path(metaclass=AsyncAutoWrapperType):
7825d3
         encoding: str | None = None,
7825d3
         errors: str | None = None,
7825d3
         newline: str | None = None,
7825d3
-    ) -> _AsyncIOWrapper[IO[Any]]:
7825d3
+    ) -> AsyncIOWrapper[IO[Any]]:
7825d3
         ...
7825d3
 
7825d3
-    @wraps(pathlib.Path.open)  # type: ignore[misc]  # Overload return mismatch.
7825d3
-    async def open(self, *args: Any, **kwargs: Any) -> _AsyncIOWrapper[IO[Any]]:
7825d3
-        """Open the file pointed to by the path, like the :func:`trio.open_file`
7825d3
-        function does.
7825d3
-
7825d3
-        """
7825d3
-
7825d3
-        func = partial(self._wrapped.open, *args, **kwargs)
7825d3
-        value = await trio.to_thread.run_sync(func)
7825d3
-        return trio.wrap_file(value)
7825d3
-
7825d3
-    if TYPE_CHECKING:
7825d3
-        # the dunders listed in _forward_magic that aren't seen otherwise
7825d3
-        # fmt: off
7825d3
-        def __bytes__(self) -> bytes: ...
7825d3
-        def __truediv__(self, other: StrPath) -> Path: ...
7825d3
-        def __rtruediv__(self, other: StrPath) -> Path: ...
7825d3
-        def __lt__(self, other: Path | pathlib.PurePath) -> bool: ...
7825d3
-        def __le__(self, other: Path | pathlib.PurePath) -> bool: ...
7825d3
-        def __gt__(self, other: Path | pathlib.PurePath) -> bool: ...
7825d3
-        def __ge__(self, other: Path | pathlib.PurePath) -> bool: ...
7825d3
-
7825d3
-        # The following are ordered the same as in typeshed.
7825d3
-
7825d3
-        # Properties produced by __getattr__() - all synchronous.
7825d3
-        @property
7825d3
-        def parts(self) -> tuple[str, ...]: ...
7825d3
-        @property
7825d3
-        def drive(self) -> str: ...
7825d3
-        @property
7825d3
-        def root(self) -> str: ...
7825d3
-        @property
7825d3
-        def anchor(self) -> str: ...
7825d3
-        @property
7825d3
-        def name(self) -> str: ...
7825d3
-        @property
7825d3
-        def suffix(self) -> str: ...
7825d3
-        @property
7825d3
-        def suffixes(self) -> list[str]: ...
7825d3
-        @property
7825d3
-        def stem(self) -> str: ...
7825d3
-        @property
7825d3
-        def parents(self) -> Sequence[pathlib.Path]: ...   # TODO: Convert these to trio Paths?
7825d3
-        @property
7825d3
-        def parent(self) -> Path: ...
7825d3
-
7825d3
-        # PurePath methods - synchronous.
7825d3
-        def as_posix(self) -> str: ...
7825d3
-        def as_uri(self) -> str: ...
7825d3
-        def is_absolute(self) -> bool: ...
7825d3
-        def is_reserved(self) -> bool: ...
7825d3
-        def match(self, path_pattern: str) -> bool: ...
7825d3
-        def relative_to(self, *other: StrPath) -> Path: ...
7825d3
-        def with_name(self, name: str) -> Path: ...
7825d3
-        def with_suffix(self, suffix: str) -> Path: ...
7825d3
-        def joinpath(self, *other: StrPath) -> Path: ...
7825d3
-
7825d3
-        if sys.version_info >= (3, 9):
7825d3
-            def is_relative_to(self, *other: StrPath) -> bool: ...
7825d3
-            def with_stem(self, stem: str) -> Path: ...
7825d3
-
7825d3
-        # pathlib.Path methods and properties - async.
7825d3
-        @classmethod
7825d3
-        async def cwd(self) -> Path: ...
7825d3
-
7825d3
-        if sys.version_info >= (3, 10):
7825d3
-            async def stat(self, *, follow_symlinks: bool = True) -> os.stat_result: ...
7825d3
-            async def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: ...
7825d3
-        else:
7825d3
-            async def stat(self) -> os.stat_result: ...
7825d3
-            async def chmod(self, mode: int) -> None: ...
7825d3
-
7825d3
-        async def exists(self) -> bool: ...
7825d3
-        async def glob(self, pattern: str) -> Iterable[Path]: ...
7825d3
-        async def is_dir(self) -> bool: ...
7825d3
-        async def is_file(self) -> bool: ...
7825d3
-        async def is_symlink(self) -> bool: ...
7825d3
-        async def is_socket(self) -> bool: ...
7825d3
-        async def is_fifo(self) -> bool: ...
7825d3
-        async def is_block_device(self) -> bool: ...
7825d3
-        async def is_char_device(self) -> bool: ...
7825d3
-        async def iterdir(self) -> Iterable[Path]: ...
7825d3
-        async def lchmod(self, mode: int) -> None: ...
7825d3
-        async def lstat(self) -> os.stat_result: ...
7825d3
-        async def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None: ...
7825d3
-
7825d3
-        if sys.platform != "win32":
7825d3
-            async def owner(self) -> str: ...
7825d3
-            async def group(self) -> str: ...
7825d3
-            async def is_mount(self) -> bool: ...
7825d3
-        if sys.version_info >= (3, 9):
7825d3
-            async def readlink(self) -> Path: ...
7825d3
-        async def rename(self, target: StrPath) -> Path: ...
7825d3
-        async def replace(self, target: StrPath) -> Path: ...
7825d3
-        async def resolve(self, strict: bool = False) -> Path: ...
7825d3
-        async def rglob(self, pattern: str) -> Iterable[Path]: ...
7825d3
-        async def rmdir(self) -> None: ...
7825d3
-        async def symlink_to(self, target: StrPath, target_is_directory: bool = False) -> None: ...
7825d3
-        if sys.version_info >= (3, 10):
7825d3
-            async def hardlink_to(self, target: str | pathlib.Path) -> None: ...
7825d3
-        async def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: ...
7825d3
-        async def unlink(self, missing_ok: bool = False) -> None: ...
7825d3
-        @classmethod
7825d3
-        async def home(self) -> Path: ...
7825d3
-        async def absolute(self) -> Path: ...
7825d3
-        async def expanduser(self) -> Path: ...
7825d3
-        async def read_bytes(self) -> bytes: ...
7825d3
-        async def read_text(self, encoding: str | None = None, errors: str | None = None) -> str: ...
7825d3
-        async def samefile(self, other_path: bytes | int | StrPath) -> bool: ...
7825d3
-        async def write_bytes(self, data: bytes) -> int: ...
7825d3
-
7825d3
-        if sys.version_info >= (3, 10):
7825d3
-            async def write_text(
7825d3
-                self, data: str,
7825d3
-                encoding: str | None = None,
7825d3
-                errors: str | None = None,
7825d3
-                newline: str | None = None,
7825d3
-            ) -> int: ...
7825d3
-        else:
7825d3
-            async def write_text(
7825d3
-                self, data: str,
7825d3
-                encoding: str | None = None,
7825d3
-                errors: str | None = None,
7825d3
-            ) -> int: ...
7825d3
-
7825d3
-        if sys.version_info < (3, 12):
7825d3
-            async def link_to(self, target: StrPath | bytes) -> None: ...
7825d3
-        if sys.version_info >= (3, 12):
7825d3
-            async def is_junction(self) -> bool: ...
7825d3
-            walk: Any  # TODO
7825d3
-            async def with_segments(self, *pathsegments: StrPath) -> Path: ...
7825d3
-
7825d3
-
7825d3
-Path.iterdir.__doc__ = """
7825d3
-    Like :meth:`~pathlib.Path.iterdir`, but async.
7825d3
-
7825d3
-    This is an async method that returns a synchronous iterator, so you
7825d3
-    use it like::
7825d3
-
7825d3
-       for subpath in await mypath.iterdir():
7825d3
-           ...
7825d3
-
7825d3
-    Note that it actually loads the whole directory list into memory
7825d3
-    immediately, during the initial call. (See `issue #501
7825d3
-    <https://github.com/python-trio/trio/issues/501>`__ for discussion.)
7825d3
-
7825d3
-"""
7825d3
-
7825d3
-if sys.version_info < (3, 12):
7825d3
-    # Since we synthesise methods from the stdlib, this automatically will
7825d3
-    # have deprecation warnings, and disappear entirely in 3.12+.
7825d3
-    Path.link_to.__doc__ = """
7825d3
-    Like Python 3.8-3.11's :meth:`~pathlib.Path.link_to`, but async.
7825d3
-
7825d3
-    :deprecated: This method was deprecated in Python 3.10 and entirely \
7825d3
-    removed in 3.12. Use :meth:`hardlink_to` instead which has \
7825d3
-    a more meaningful parameter order.
7825d3
-"""
7825d3
-
7825d3
-# The value of Path.absolute.__doc__ makes a reference to
7825d3
-# :meth:~pathlib.Path.absolute, which does not exist. Removing this makes more
7825d3
-# sense than inventing our own special docstring for this.
7825d3
-del Path.absolute.__doc__
7825d3
-
7825d3
-# TODO: This is likely not supported by all the static tools out there, see discussion in
7825d3
-# https://github.com/python-trio/trio/pull/2631#discussion_r1185612528
7825d3
-os.PathLike.register(Path)
7825d3
+    @_wraps_async(pathlib.Path.open)  # type: ignore[misc]  # Overload return mismatch.
7825d3
+    def open(self, *args: Any, **kwargs: Any) -> AsyncIOWrapper[IO[Any]]:
7825d3
+        return wrap_file(self._wrapped_cls(self).open(*args, **kwargs))
7825d3
+
7825d3
+    def __repr__(self) -> str:
7825d3
+        return f"trio.Path({str(self)!r})"
7825d3
+
7825d3
+    stat = _wrap_method(pathlib.Path.stat)
7825d3
+    chmod = _wrap_method(pathlib.Path.chmod)
7825d3
+    exists = _wrap_method(pathlib.Path.exists)
7825d3
+    glob = _wrap_method_path_iterable(pathlib.Path.glob)
7825d3
+    rglob = _wrap_method_path_iterable(pathlib.Path.rglob)
7825d3
+    is_dir = _wrap_method(pathlib.Path.is_dir)
7825d3
+    is_file = _wrap_method(pathlib.Path.is_file)
7825d3
+    is_symlink = _wrap_method(pathlib.Path.is_symlink)
7825d3
+    is_socket = _wrap_method(pathlib.Path.is_socket)
7825d3
+    is_fifo = _wrap_method(pathlib.Path.is_fifo)
7825d3
+    is_block_device = _wrap_method(pathlib.Path.is_block_device)
7825d3
+    is_char_device = _wrap_method(pathlib.Path.is_char_device)
7825d3
+    if sys.version_info >= (3, 12):
7825d3
+        is_junction = _wrap_method(pathlib.Path.is_junction)
7825d3
+    iterdir = _wrap_method_path_iterable(pathlib.Path.iterdir)
7825d3
+    lchmod = _wrap_method(pathlib.Path.lchmod)
7825d3
+    lstat = _wrap_method(pathlib.Path.lstat)
7825d3
+    mkdir = _wrap_method(pathlib.Path.mkdir)
7825d3
+    if sys.platform != "win32":
7825d3
+        owner = _wrap_method(pathlib.Path.owner)
7825d3
+        group = _wrap_method(pathlib.Path.group)
7825d3
+    if sys.platform != "win32" or sys.version_info >= (3, 12):
7825d3
+        is_mount = _wrap_method(pathlib.Path.is_mount)
7825d3
+    if sys.version_info >= (3, 9):
7825d3
+        readlink = _wrap_method_path(pathlib.Path.readlink)
7825d3
+    rename = _wrap_method_path(pathlib.Path.rename)
7825d3
+    replace = _wrap_method_path(pathlib.Path.replace)
7825d3
+    resolve = _wrap_method_path(pathlib.Path.resolve)
7825d3
+    rmdir = _wrap_method(pathlib.Path.rmdir)
7825d3
+    symlink_to = _wrap_method(pathlib.Path.symlink_to)
7825d3
+    if sys.version_info >= (3, 10):
7825d3
+        hardlink_to = _wrap_method(pathlib.Path.hardlink_to)
7825d3
+    touch = _wrap_method(pathlib.Path.touch)
7825d3
+    unlink = _wrap_method(pathlib.Path.unlink)
7825d3
+    absolute = _wrap_method_path(pathlib.Path.absolute)
7825d3
+    expanduser = _wrap_method_path(pathlib.Path.expanduser)
7825d3
+    read_bytes = _wrap_method(pathlib.Path.read_bytes)
7825d3
+    read_text = _wrap_method(pathlib.Path.read_text)
7825d3
+    samefile = _wrap_method(pathlib.Path.samefile)
7825d3
+    write_bytes = _wrap_method(pathlib.Path.write_bytes)
7825d3
+    write_text = _wrap_method(pathlib.Path.write_text)
7825d3
+    if sys.version_info < (3, 12):
7825d3
+        link_to = _wrap_method(pathlib.Path.link_to)
7825d3
+
7825d3
+
7825d3
+@final
7825d3
+class PosixPath(Path, pathlib.PurePosixPath):
7825d3
+    """An async :class:`pathlib.PosixPath` that executes blocking methods in :meth:`trio.to_thread.run_sync`."""
7825d3
+
7825d3
+    __slots__ = ()
7825d3
+
7825d3
+    _wrapped_cls: ClassVar[type[pathlib.Path]] = pathlib.PosixPath
7825d3
+
7825d3
+
7825d3
+@final
7825d3
+class WindowsPath(Path, pathlib.PureWindowsPath):
7825d3
+    """An async :class:`pathlib.WindowsPath` that executes blocking methods in :meth:`trio.to_thread.run_sync`."""
7825d3
+
7825d3
+    __slots__ = ()
7825d3
+
7825d3
+    _wrapped_cls: ClassVar[type[pathlib.Path]] = pathlib.WindowsPath
7825d3
diff --git a/trio/_tests/test_exports.py b/trio/_tests/test_exports.py
7825d3
index 7b38137..e1ced60 100644
7825d3
--- a/trio/_tests/test_exports.py
7825d3
+++ b/trio/_tests/test_exports.py
7825d3
@@ -11,7 +11,7 @@ import socket as stdlib_socket
7825d3
 import sys
7825d3
 import types
7825d3
 from collections.abc import Iterator
7825d3
-from pathlib import Path
7825d3
+from pathlib import Path, PurePath
7825d3
 from types import ModuleType
7825d3
 from typing import Iterable, Protocol
7825d3
 
7825d3
@@ -333,7 +333,8 @@ def test_static_tool_sees_class_members(
7825d3
                     mod_cache = next_cache / "__init__.data.json"
7825d3
                 else:
7825d3
                     mod_cache = mod_cache / (modname[-1] + ".data.json")
7825d3
-
7825d3
+            elif mod_cache.is_dir():
7825d3
+                mod_cache /= "__init__.data.json"
7825d3
             with mod_cache.open() as f:
7825d3
                 return json.loads(f.read())["names"][name]  # type: ignore[no-any-return]
7825d3
 
7825d3
@@ -480,12 +481,6 @@ def test_static_tool_sees_class_members(
7825d3
             extra -= EXTRAS[class_]
7825d3
             assert len(extra) == before - len(EXTRAS[class_])
7825d3
 
7825d3
-        # probably an issue with mypy....
7825d3
-        if tool == "mypy" and class_ == trio.Path and sys.platform == "win32":
7825d3
-            before = len(missing)
7825d3
-            missing -= {"owner", "group", "is_mount"}
7825d3
-            assert len(missing) == before - 3
7825d3
-
7825d3
         # TODO: why is this? Is it a problem?
7825d3
         # see https://github.com/python-trio/trio/pull/2631#discussion_r1185615916
7825d3
         if class_ == trio.StapledStream:
7825d3
@@ -508,25 +503,14 @@ def test_static_tool_sees_class_members(
7825d3
                 missing.remove("__aiter__")
7825d3
                 missing.remove("__anext__")
7825d3
 
7825d3
-        # __getattr__ is intentionally hidden behind type guard. That hook then
7825d3
-        # forwards property accesses to PurePath, meaning these names aren't directly on
7825d3
-        # the class.
7825d3
-        if class_ == trio.Path:
7825d3
-            missing.remove("__getattr__")
7825d3
-            before = len(extra)
7825d3
-            extra -= {
7825d3
-                "anchor",
7825d3
-                "drive",
7825d3
-                "name",
7825d3
-                "parent",
7825d3
-                "parents",
7825d3
-                "parts",
7825d3
-                "root",
7825d3
-                "stem",
7825d3
-                "suffix",
7825d3
-                "suffixes",
7825d3
-            }
7825d3
-            assert len(extra) == before - 10
7825d3
+        if class_ in (trio.Path, trio.WindowsPath, trio.PosixPath):
7825d3
+            # These are from inherited subclasses.
7825d3
+            missing -= PurePath.__dict__.keys()
7825d3
+            # These are unix-only.
7825d3
+            if tool == "mypy" and sys.platform == "win32":
7825d3
+                missing -= {"owner", "is_mount", "group"}
7825d3
+            if tool == "jedi" and sys.platform == "win32":
7825d3
+                extra -= {"owner", "is_mount", "group"}
7825d3
 
7825d3
         if missing or extra:  # pragma: no cover
7825d3
             errors[f"{module_name}.{class_name}"] = {
7825d3
@@ -588,6 +572,9 @@ def test_classes_are_final() -> None:
7825d3
                 continue
7825d3
             # ... insert other special cases here ...
7825d3
 
7825d3
+            # The `Path` class needs to support inheritance to allow `WindowsPath` and `PosixPath`.
7825d3
+            if class_ is trio.Path:
7825d3
+                continue
7825d3
             # don't care about the *Statistics classes
7825d3
             if name.endswith("Statistics"):
7825d3
                 continue
7825d3
diff --git a/trio/_tests/test_path.py b/trio/_tests/test_path.py
7825d3
index 30158f8..3528053 100644
7825d3
--- a/trio/_tests/test_path.py
7825d3
+++ b/trio/_tests/test_path.py
7825d3
@@ -3,13 +3,12 @@ from __future__ import annotations
7825d3
 import os
7825d3
 import pathlib
7825d3
 from collections.abc import Awaitable, Callable
7825d3
-from typing import Any, Type, Union
7825d3
+from typing import Type, Union
7825d3
 
7825d3
 import pytest
7825d3
 
7825d3
 import trio
7825d3
 from trio._file_io import AsyncIOWrapper
7825d3
-from trio._path import AsyncAutoWrapperType as WrapperType
7825d3
 
7825d3
 
7825d3
 @pytest.fixture
7825d3
@@ -26,6 +25,16 @@ def method_pair(
7825d3
     return getattr(sync_path, method_name), getattr(async_path, method_name)
7825d3
 
7825d3
 
7825d3
+@pytest.mark.skipif(os.name == "nt", reason="OS is not posix")
7825d3
+async def test_instantiate_posix() -> None:
7825d3
+    assert isinstance(trio.Path(), trio.PosixPath)
7825d3
+
7825d3
+
7825d3
+@pytest.mark.skipif(os.name != "nt", reason="OS is not Windows")
7825d3
+async def test_instantiate_windows() -> None:
7825d3
+    assert isinstance(trio.Path(), trio.WindowsPath)
7825d3
+
7825d3
+
7825d3
 async def test_open_is_async_context_manager(path: trio.Path) -> None:
7825d3
     async with await path.open("w") as f:
7825d3
         assert isinstance(f, AsyncIOWrapper)
7825d3
@@ -166,41 +175,6 @@ async def test_repr() -> None:
7825d3
     assert repr(path) == "trio.Path('.')"
7825d3
 
7825d3
 
7825d3
-class MockWrapped:
7825d3
-    unsupported = "unsupported"
7825d3
-    _private = "private"
7825d3
-
7825d3
-
7825d3
-class _MockWrapper:
7825d3
-    _forwards = MockWrapped
7825d3
-    _wraps = MockWrapped
7825d3
-
7825d3
-
7825d3
-MockWrapper: Any = _MockWrapper  # Disable type checking, it's a mock.
7825d3
-
7825d3
-
7825d3
-async def test_type_forwards_unsupported() -> None:
7825d3
-    with pytest.raises(TypeError):
7825d3
-        WrapperType.generate_forwards(MockWrapper, {})
7825d3
-
7825d3
-
7825d3
-async def test_type_wraps_unsupported() -> None:
7825d3
-    with pytest.raises(TypeError):
7825d3
-        WrapperType.generate_wraps(MockWrapper, {})
7825d3
-
7825d3
-
7825d3
-async def test_type_forwards_private() -> None:
7825d3
-    WrapperType.generate_forwards(MockWrapper, {"unsupported": None})
7825d3
-
7825d3
-    assert not hasattr(MockWrapper, "_private")
7825d3
-
7825d3
-
7825d3
-async def test_type_wraps_private() -> None:
7825d3
-    WrapperType.generate_wraps(MockWrapper, {"unsupported": None})
7825d3
-
7825d3
-    assert not hasattr(MockWrapper, "_private")
7825d3
-
7825d3
-
7825d3
 @pytest.mark.parametrize("meth", [trio.Path.__init__, trio.Path.joinpath])
7825d3
 async def test_path_wraps_path(
7825d3
     path: trio.Path,
7825d3
-- 
7825d3
2.45.0
7825d3