diff --git a/docs/source/reference-io.rst b/docs/source/reference-io.rst index 52c3690c00..aa857e9f8f 100644 --- a/docs/source/reference-io.rst +++ b/docs/source/reference-io.rst @@ -631,6 +631,11 @@ Asynchronous path objects .. autoclass:: Path :members: + :inherited-members: + +.. autoclass:: PosixPath + +.. autoclass:: WindowsPath .. _async-file-objects: diff --git a/src/trio/__init__.py b/src/trio/__init__.py index 1a54499b62..d2151677b1 100644 --- a/src/trio/__init__.py +++ b/src/trio/__init__.py @@ -73,7 +73,7 @@ open_ssl_over_tcp_stream as open_ssl_over_tcp_stream, serve_ssl_over_tcp as serve_ssl_over_tcp, ) -from ._path import Path as Path +from ._path import Path as Path, PosixPath as PosixPath, WindowsPath as WindowsPath from ._signals import open_signal_receiver as open_signal_receiver from ._ssl import ( NeedHandshakeError as NeedHandshakeError, diff --git a/src/trio/_path.py b/src/trio/_path.py index e1ced5d4c8..4d418b7e73 100644 --- a/src/trio/_path.py +++ b/src/trio/_path.py @@ -1,28 +1,17 @@ from __future__ import annotations -import inspect import os import pathlib import sys -import types -from functools import partial -from typing import ( - IO, - TYPE_CHECKING, - Any, - BinaryIO, - ClassVar, - TypeVar, - Union, - cast, - overload, -) - -import trio -from trio._util import async_wraps, final, wraps +from functools import partial, update_wrapper +from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypeVar, overload + +from trio._file_io import AsyncIOWrapper, wrap_file +from trio._util import final +from trio.to_thread import run_sync if TYPE_CHECKING: - from collections.abc import Awaitable, Callable, Iterable, Sequence + from collections.abc import Awaitable, Callable, Iterable from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper from _typeshed import ( @@ -32,219 +21,97 @@ OpenBinaryModeWriting, OpenTextMode, ) - from typing_extensions import Concatenate, Literal, ParamSpec, TypeAlias - - from trio._file_io import AsyncIOWrapper as _AsyncIOWrapper + from typing_extensions import Concatenate, Literal, ParamSpec P = ParamSpec("P") -T = TypeVar("T") -StrPath: TypeAlias = Union[str, "os.PathLike[str]"] # Only subscriptable in 3.9+ + Self = TypeVar("Self", bound="Path") + T = TypeVar("T") -# re-wrap return value from methods that return new instances of pathlib.Path -def rewrap_path(value: T) -> T | Path: - if isinstance(value, pathlib.Path): - return Path(value) - else: - return value +def _wraps_async( + wrapped: Callable[..., Any] +) -> Callable[[Callable[P, T]], Callable[P, Awaitable[T]]]: + def decorator(fn: Callable[P, T]) -> Callable[P, Awaitable[T]]: + async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + return await run_sync(partial(fn, *args, **kwargs)) + update_wrapper(wrapper, wrapped) + wrapper.__doc__ = ( + f"Like :meth:`~{wrapped.__module__}.{wrapped.__qualname__}`, but async." + ) + return wrapper -def _forward_factory( - cls: AsyncAutoWrapperType, - attr_name: str, - attr: Callable[Concatenate[pathlib.Path, P], T], -) -> Callable[Concatenate[Path, P], T | Path]: - @wraps(attr) - def wrapper(self: Path, *args: P.args, **kwargs: P.kwargs) -> T | Path: - attr = getattr(self._wrapped, attr_name) - value = attr(*args, **kwargs) - return rewrap_path(value) + return decorator - # Assigning this makes inspect and therefore Sphinx show the original parameters. - # It's not defined on functions normally though, this is a custom attribute. - assert isinstance(wrapper, types.FunctionType) - wrapper.__signature__ = inspect.signature(attr) - return wrapper +def _wrap_method( + fn: Callable[Concatenate[pathlib.Path, P], T], +) -> Callable[Concatenate[Path, P], Awaitable[T]]: + @_wraps_async(fn) + def wrapper(self: Path, /, *args: P.args, **kwargs: P.kwargs) -> T: + return fn(pathlib.Path(self), *args, **kwargs) + return wrapper -def _forward_magic( - cls: AsyncAutoWrapperType, attr: Callable[..., T] -) -> Callable[..., Path | T]: - sentinel = object() - @wraps(attr) - def wrapper(self: Path, other: object = sentinel) -> Path | T: - if other is sentinel: - return attr(self._wrapped) - if isinstance(other, cls): - other = cast(Path, other)._wrapped - value = attr(self._wrapped, other) - return rewrap_path(value) +def _wrap_method_path( + fn: Callable[Concatenate[pathlib.Path, P], pathlib.Path], +) -> Callable[Concatenate[Self, P], Awaitable[Self]]: + @_wraps_async(fn) + def wrapper(self: Self, /, *args: P.args, **kwargs: P.kwargs) -> Self: + return self.__class__(fn(pathlib.Path(self), *args, **kwargs)) - assert isinstance(wrapper, types.FunctionType) - wrapper.__signature__ = inspect.signature(attr) return wrapper -def iter_wrapper_factory( - cls: AsyncAutoWrapperType, meth_name: str -) -> Callable[Concatenate[Path, P], Awaitable[Iterable[Path]]]: - @async_wraps(cls, cls._wraps, meth_name) - async def wrapper(self: Path, *args: P.args, **kwargs: P.kwargs) -> Iterable[Path]: - meth = getattr(self._wrapped, meth_name) - func = partial(meth, *args, **kwargs) - # Make sure that the full iteration is performed in the thread - # by converting the generator produced by pathlib into a list - items = await trio.to_thread.run_sync(lambda: list(func())) - return (rewrap_path(item) for item in items) - - return wrapper +def _wrap_method_path_iterable( + fn: Callable[Concatenate[pathlib.Path, P], Iterable[pathlib.Path]], +) -> Callable[Concatenate[Self, P], Awaitable[Iterable[Self]]]: + @_wraps_async(fn) + def wrapper(self: Self, /, *args: P.args, **kwargs: P.kwargs) -> Iterable[Self]: + return map(self.__class__, [*fn(pathlib.Path(self), *args, **kwargs)]) + assert wrapper.__doc__ is not None + wrapper.__doc__ += f""" -def thread_wrapper_factory( - cls: AsyncAutoWrapperType, meth_name: str -) -> Callable[Concatenate[Path, P], Awaitable[Path]]: - @async_wraps(cls, cls._wraps, meth_name) - async def wrapper(self: Path, *args: P.args, **kwargs: P.kwargs) -> Path: - meth = getattr(self._wrapped, meth_name) - func = partial(meth, *args, **kwargs) - value = await trio.to_thread.run_sync(func) - return rewrap_path(value) + This is an async method that returns a synchronous iterator, so you + use it like:: - return wrapper + for subpath in await mypath.{fn.__name__}(): + ... + .. note:: -def classmethod_wrapper_factory( - cls: AsyncAutoWrapperType, meth_name: str -) -> classmethod: # type: ignore[type-arg] - @async_wraps(cls, cls._wraps, meth_name) - async def wrapper(cls: type[Path], *args: Any, **kwargs: Any) -> Path: # type: ignore[misc] # contains Any - meth = getattr(cls._wraps, meth_name) - func = partial(meth, *args, **kwargs) - value = await trio.to_thread.run_sync(func) - return rewrap_path(value) - - assert isinstance(wrapper, types.FunctionType) - wrapper.__signature__ = inspect.signature(getattr(cls._wraps, meth_name)) - return classmethod(wrapper) - - -class AsyncAutoWrapperType(type): - _forwards: type - _wraps: type - _forward_magic: list[str] - _wrap_iter: list[str] - _forward: list[str] - - def __init__( - cls, name: str, bases: tuple[type, ...], attrs: dict[str, object] - ) -> None: - super().__init__(name, bases, attrs) - - cls._forward = [] - type(cls).generate_forwards(cls, attrs) - type(cls).generate_wraps(cls, attrs) - type(cls).generate_magic(cls, attrs) - type(cls).generate_iter(cls, attrs) - - def generate_forwards(cls, attrs: dict[str, object]) -> None: - # forward functions of _forwards - for attr_name, attr in cls._forwards.__dict__.items(): - if attr_name.startswith("_") or attr_name in attrs: - continue - - if isinstance(attr, (property, types.ModuleType)): - cls._forward.append(attr_name) - elif isinstance(attr, (types.FunctionType, types.BuiltinMethodType)): - wrapper = _forward_factory(cls, attr_name, attr) - setattr(cls, attr_name, wrapper) - else: - raise TypeError(attr_name, type(attr)) - - def generate_wraps(cls, attrs: dict[str, object]) -> None: - # generate wrappers for functions of _wraps - wrapper: classmethod | Callable[..., object] # type: ignore[type-arg] - for attr_name, attr in cls._wraps.__dict__.items(): - # .z. exclude cls._wrap_iter - if attr_name.startswith("_") or attr_name in attrs: - continue - if isinstance(attr, classmethod): - wrapper = classmethod_wrapper_factory(cls, attr_name) - setattr(cls, attr_name, wrapper) - elif isinstance(attr, types.FunctionType): - wrapper = thread_wrapper_factory(cls, attr_name) - assert isinstance(wrapper, types.FunctionType) - wrapper.__signature__ = inspect.signature(attr) - setattr(cls, attr_name, wrapper) - else: - raise TypeError(attr_name, type(attr)) - - def generate_magic(cls, attrs: dict[str, object]) -> None: - # generate wrappers for magic - for attr_name in cls._forward_magic: - attr = getattr(cls._forwards, attr_name) - wrapper = _forward_magic(cls, attr) - setattr(cls, attr_name, wrapper) - - def generate_iter(cls, attrs: dict[str, object]) -> None: - # generate wrappers for methods that return iterators - wrapper: Callable[..., object] - for attr_name, attr in cls._wraps.__dict__.items(): - if attr_name in cls._wrap_iter: - wrapper = iter_wrapper_factory(cls, attr_name) - assert isinstance(wrapper, types.FunctionType) - wrapper.__signature__ = inspect.signature(attr) - setattr(cls, attr_name, wrapper) + The iterator is loaded into memory immediately during the initial + call, (see `issue #501 + `__ for discussion). + """ + return wrapper -@final -class Path(metaclass=AsyncAutoWrapperType): +class Path(pathlib.PurePath): """A :class:`pathlib.Path` wrapper that executes blocking methods in :meth:`trio.to_thread.run_sync`. """ - _forward: ClassVar[list[str]] - _wraps: ClassVar[type] = pathlib.Path - _forwards: ClassVar[type] = pathlib.PurePath - _forward_magic: ClassVar[list[str]] = [ - "__str__", - "__bytes__", - "__truediv__", - "__rtruediv__", - "__eq__", - "__lt__", - "__le__", - "__gt__", - "__ge__", - "__hash__", - ] - _wrap_iter: ClassVar[list[str]] = ["glob", "rglob", "iterdir"] - - def __init__(self, *args: StrPath) -> None: - self._wrapped = pathlib.Path(*args) - - # type checkers allow accessing any attributes on class instances with `__getattr__` - # so we hide it behind a type guard forcing it to rely on the hardcoded attribute - # list below. - if not TYPE_CHECKING: - - def __getattr__(self, name): - if name in self._forward: - value = getattr(self._wrapped, name) - return rewrap_path(value) - raise AttributeError(name) - - def __dir__(self) -> list[str]: - return [*super().__dir__(), *self._forward] + __slots__ = () - def __repr__(self) -> str: - return f"trio.Path({str(self)!r})" + def __new__(cls: type[Self], *args: str | os.PathLike[str]) -> Self: + if cls is Path: + cls = WindowsPath if os.name == "nt" else PosixPath # type: ignore[assignment] + return super().__new__(cls, *args) + + @classmethod + @_wraps_async(pathlib.Path.cwd) + def cwd(cls: type[Self]) -> Self: + return cls(pathlib.Path.cwd()) - def __fspath__(self) -> str: - return os.fspath(self._wrapped) + @classmethod + @_wraps_async(pathlib.Path.home) + def home(cls: type[Self]) -> Self: + return cls(pathlib.Path.home()) @overload async def open( @@ -254,7 +121,7 @@ async def open( encoding: str | None = None, errors: str | None = None, newline: str | None = None, - ) -> _AsyncIOWrapper[TextIOWrapper]: ... + ) -> AsyncIOWrapper[TextIOWrapper]: ... @overload async def open( @@ -264,7 +131,7 @@ async def open( encoding: None = None, errors: None = None, newline: None = None, - ) -> _AsyncIOWrapper[FileIO]: ... + ) -> AsyncIOWrapper[FileIO]: ... @overload async def open( @@ -274,7 +141,7 @@ async def open( encoding: None = None, errors: None = None, newline: None = None, - ) -> _AsyncIOWrapper[BufferedRandom]: ... + ) -> AsyncIOWrapper[BufferedRandom]: ... @overload async def open( @@ -284,7 +151,7 @@ async def open( encoding: None = None, errors: None = None, newline: None = None, - ) -> _AsyncIOWrapper[BufferedWriter]: ... + ) -> AsyncIOWrapper[BufferedWriter]: ... @overload async def open( @@ -294,7 +161,7 @@ async def open( encoding: None = None, errors: None = None, newline: None = None, - ) -> _AsyncIOWrapper[BufferedReader]: ... + ) -> AsyncIOWrapper[BufferedReader]: ... @overload async def open( @@ -304,7 +171,7 @@ async def open( encoding: None = None, errors: None = None, newline: None = None, - ) -> _AsyncIOWrapper[BinaryIO]: ... + ) -> AsyncIOWrapper[BinaryIO]: ... @overload async def open( # type: ignore[misc] # Any usage matches builtins.open(). @@ -314,172 +181,78 @@ async def open( # type: ignore[misc] # Any usage matches builtins.open(). encoding: str | None = None, errors: str | None = None, newline: str | None = None, - ) -> _AsyncIOWrapper[IO[Any]]: ... - - @wraps(pathlib.Path.open) # type: ignore[misc] # Overload return mismatch. - async def open(self, *args: Any, **kwargs: Any) -> _AsyncIOWrapper[IO[Any]]: - """Open the file pointed to by the path, like the :func:`trio.open_file` - function does. - - """ - - func = partial(self._wrapped.open, *args, **kwargs) - value = await trio.to_thread.run_sync(func) - return trio.wrap_file(value) - - if TYPE_CHECKING: - # the dunders listed in _forward_magic that aren't seen otherwise - # fmt: off - def __bytes__(self) -> bytes: ... - def __truediv__(self, other: StrPath) -> Path: ... - def __rtruediv__(self, other: StrPath) -> Path: ... - def __lt__(self, other: Path | pathlib.PurePath) -> bool: ... - def __le__(self, other: Path | pathlib.PurePath) -> bool: ... - def __gt__(self, other: Path | pathlib.PurePath) -> bool: ... - def __ge__(self, other: Path | pathlib.PurePath) -> bool: ... - - # The following are ordered the same as in typeshed. - - # Properties produced by __getattr__() - all synchronous. - @property - def parts(self) -> tuple[str, ...]: ... - @property - def drive(self) -> str: ... - @property - def root(self) -> str: ... - @property - def anchor(self) -> str: ... - @property - def name(self) -> str: ... - @property - def suffix(self) -> str: ... - @property - def suffixes(self) -> list[str]: ... - @property - def stem(self) -> str: ... - @property - def parents(self) -> Sequence[pathlib.Path]: ... # TODO: Convert these to trio Paths? - @property - def parent(self) -> Path: ... - - # PurePath methods - synchronous. - def as_posix(self) -> str: ... - def as_uri(self) -> str: ... - def is_absolute(self) -> bool: ... - def is_reserved(self) -> bool: ... - def match(self, path_pattern: str) -> bool: ... - def relative_to(self, *other: StrPath) -> Path: ... - def with_name(self, name: str) -> Path: ... - def with_suffix(self, suffix: str) -> Path: ... - def joinpath(self, *other: StrPath) -> Path: ... - - if sys.version_info >= (3, 9): - def is_relative_to(self, *other: StrPath) -> bool: ... - def with_stem(self, stem: str) -> Path: ... - - # pathlib.Path methods and properties - async. - @classmethod - async def cwd(self) -> Path: ... - - if sys.version_info >= (3, 10): - async def stat(self, *, follow_symlinks: bool = True) -> os.stat_result: ... - async def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: ... - else: - async def stat(self) -> os.stat_result: ... - async def chmod(self, mode: int) -> None: ... - - async def exists(self) -> bool: ... - async def glob(self, pattern: str) -> Iterable[Path]: ... - async def is_dir(self) -> bool: ... - async def is_file(self) -> bool: ... - async def is_symlink(self) -> bool: ... - async def is_socket(self) -> bool: ... - async def is_fifo(self) -> bool: ... - async def is_block_device(self) -> bool: ... - async def is_char_device(self) -> bool: ... - async def iterdir(self) -> Iterable[Path]: ... - async def lchmod(self, mode: int) -> None: ... - async def lstat(self) -> os.stat_result: ... - async def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None: ... - - if sys.platform != "win32": - async def owner(self) -> str: ... - async def group(self) -> str: ... - async def is_mount(self) -> bool: ... - if sys.version_info >= (3, 9): - async def readlink(self) -> Path: ... - async def rename(self, target: StrPath) -> Path: ... - async def replace(self, target: StrPath) -> Path: ... - async def resolve(self, strict: bool = False) -> Path: ... - async def rglob(self, pattern: str) -> Iterable[Path]: ... - async def rmdir(self) -> None: ... - async def symlink_to(self, target: StrPath, target_is_directory: bool = False) -> None: ... - if sys.version_info >= (3, 10): - async def hardlink_to(self, target: str | pathlib.Path) -> None: ... - async def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: ... - async def unlink(self, missing_ok: bool = False) -> None: ... - @classmethod - async def home(self) -> Path: ... - async def absolute(self) -> Path: ... - async def expanduser(self) -> Path: ... - async def read_bytes(self) -> bytes: ... - async def read_text(self, encoding: str | None = None, errors: str | None = None) -> str: ... - async def samefile(self, other_path: bytes | int | StrPath) -> bool: ... - async def write_bytes(self, data: bytes) -> int: ... - - if sys.version_info >= (3, 10): - async def write_text( - self, data: str, - encoding: str | None = None, - errors: str | None = None, - newline: str | None = None, - ) -> int: ... - else: - async def write_text( - self, data: str, - encoding: str | None = None, - errors: str | None = None, - ) -> int: ... - - if sys.version_info < (3, 12): - async def link_to(self, target: StrPath | bytes) -> None: ... - if sys.version_info >= (3, 12): - async def is_junction(self) -> bool: ... - walk: Any # TODO - async def with_segments(self, *pathsegments: StrPath) -> Path: ... - - -Path.iterdir.__doc__ = """ - Like :meth:`~pathlib.Path.iterdir`, but async. + ) -> AsyncIOWrapper[IO[Any]]: ... - This is an async method that returns a synchronous iterator, so you - use it like:: + @_wraps_async(pathlib.Path.open) # type: ignore[misc] # Overload return mismatch. + def open(self, *args: Any, **kwargs: Any) -> AsyncIOWrapper[IO[Any]]: + return wrap_file(pathlib.Path(self).open(*args, **kwargs)) # noqa: SIM115 - for subpath in await mypath.iterdir(): - ... + def __repr__(self) -> str: + return f"trio.Path({str(self)!r})" + + stat = _wrap_method(pathlib.Path.stat) + chmod = _wrap_method(pathlib.Path.chmod) + exists = _wrap_method(pathlib.Path.exists) + glob = _wrap_method_path_iterable(pathlib.Path.glob) + rglob = _wrap_method_path_iterable(pathlib.Path.rglob) + is_dir = _wrap_method(pathlib.Path.is_dir) + is_file = _wrap_method(pathlib.Path.is_file) + is_symlink = _wrap_method(pathlib.Path.is_symlink) + is_socket = _wrap_method(pathlib.Path.is_socket) + is_fifo = _wrap_method(pathlib.Path.is_fifo) + is_block_device = _wrap_method(pathlib.Path.is_block_device) + is_char_device = _wrap_method(pathlib.Path.is_char_device) + if sys.version_info >= (3, 12): + is_junction = _wrap_method(pathlib.Path.is_junction) + iterdir = _wrap_method_path_iterable(pathlib.Path.iterdir) + lchmod = _wrap_method(pathlib.Path.lchmod) + lstat = _wrap_method(pathlib.Path.lstat) + mkdir = _wrap_method(pathlib.Path.mkdir) + if sys.platform != "win32": + owner = _wrap_method(pathlib.Path.owner) + group = _wrap_method(pathlib.Path.group) + if sys.platform != "win32" or sys.version_info >= (3, 12): + is_mount = _wrap_method(pathlib.Path.is_mount) + if sys.version_info >= (3, 9): + readlink = _wrap_method_path(pathlib.Path.readlink) + rename = _wrap_method_path(pathlib.Path.rename) + replace = _wrap_method_path(pathlib.Path.replace) + resolve = _wrap_method_path(pathlib.Path.resolve) + rmdir = _wrap_method(pathlib.Path.rmdir) + symlink_to = _wrap_method(pathlib.Path.symlink_to) + if sys.version_info >= (3, 10): + hardlink_to = _wrap_method(pathlib.Path.hardlink_to) + touch = _wrap_method(pathlib.Path.touch) + unlink = _wrap_method(pathlib.Path.unlink) + absolute = _wrap_method_path(pathlib.Path.absolute) + expanduser = _wrap_method_path(pathlib.Path.expanduser) + read_bytes = _wrap_method(pathlib.Path.read_bytes) + read_text = _wrap_method(pathlib.Path.read_text) + samefile = _wrap_method(pathlib.Path.samefile) + write_bytes = _wrap_method(pathlib.Path.write_bytes) + write_text = _wrap_method(pathlib.Path.write_text) + if sys.version_info < (3, 12): + link_to = _wrap_method(pathlib.Path.link_to) + + +@final +class PosixPath(Path, pathlib.PurePosixPath): + """A :class:`pathlib.PosixPath` wrapper that executes blocking methods in + :meth:`trio.to_thread.run_sync`. + + """ - Note that it actually loads the whole directory list into memory - immediately, during the initial call. (See `issue #501 - `__ for discussion.) + __slots__ = () -""" -if sys.version_info < (3, 12): - # Since we synthesise methods from the stdlib, this automatically will - # have deprecation warnings, and disappear entirely in 3.12+. - Path.link_to.__doc__ = """ - Like Python 3.8-3.11's :meth:`~pathlib.Path.link_to`, but async. +@final +class WindowsPath(Path, pathlib.PureWindowsPath): + """A :class:`pathlib.WindowsPath` wrapper that executes blocking methods in + :meth:`trio.to_thread.run_sync`. + + """ - :deprecated: This method was deprecated in Python 3.10 and entirely \ - removed in 3.12. Use :meth:`hardlink_to` instead which has \ - a more meaningful parameter order. -""" + __slots__ = () -# The value of Path.absolute.__doc__ makes a reference to -# :meth:~pathlib.Path.absolute, which does not exist. Removing this makes more -# sense than inventing our own special docstring for this. -del Path.absolute.__doc__ -# TODO: This is likely not supported by all the static tools out there, see discussion in -# https://github.com/python-trio/trio/pull/2631#discussion_r1185612528 -os.PathLike.register(Path) +final(Path) diff --git a/src/trio/_tests/test_exports.py b/src/trio/_tests/test_exports.py index 7418f11da8..a290a74e8c 100644 --- a/src/trio/_tests/test_exports.py +++ b/src/trio/_tests/test_exports.py @@ -10,7 +10,7 @@ import socket as stdlib_socket import sys import types -from pathlib import Path +from pathlib import Path, PurePath from types import ModuleType from typing import TYPE_CHECKING, Protocol @@ -306,7 +306,8 @@ def lookup_symbol(symbol: str) -> dict[str, str]: mod_cache = next_cache / "__init__.data.json" else: mod_cache = mod_cache / (modname[-1] + ".data.json") - + elif mod_cache.is_dir(): + mod_cache /= "__init__.data.json" with mod_cache.open() as f: return json.loads(f.read())["names"][name] # type: ignore[no-any-return] @@ -461,12 +462,6 @@ def lookup_symbol(symbol: str) -> dict[str, str]: extra -= EXTRAS[class_] assert len(extra) == before - len(EXTRAS[class_]) - # probably an issue with mypy.... - if tool == "mypy" and class_ == trio.Path and sys.platform == "win32": - before = len(missing) - missing -= {"owner", "group", "is_mount"} - assert len(missing) == before - 3 - # TODO: why is this? Is it a problem? # see https://github.com/python-trio/trio/pull/2631#discussion_r1185615916 if class_ == trio.StapledStream: @@ -489,25 +484,12 @@ def lookup_symbol(symbol: str) -> dict[str, str]: missing.remove("__aiter__") missing.remove("__anext__") - # __getattr__ is intentionally hidden behind type guard. That hook then - # forwards property accesses to PurePath, meaning these names aren't directly on - # the class. - if class_ == trio.Path: - missing.remove("__getattr__") - before = len(extra) - extra -= { - "anchor", - "drive", - "name", - "parent", - "parents", - "parts", - "root", - "stem", - "suffix", - "suffixes", - } - assert len(extra) == before - 10 + if class_ in (trio.Path, trio.WindowsPath, trio.PosixPath): + # These are from inherited subclasses. + missing -= PurePath.__dict__.keys() + # These are unix-only. + if tool == "mypy" and sys.platform == "win32": + missing -= {"owner", "is_mount", "group"} if missing or extra: # pragma: no cover errors[f"{module_name}.{class_name}"] = { diff --git a/src/trio/_tests/test_path.py b/src/trio/_tests/test_path.py index 2d994d397e..ed7ace2af7 100644 --- a/src/trio/_tests/test_path.py +++ b/src/trio/_tests/test_path.py @@ -2,13 +2,12 @@ import os import pathlib -from typing import TYPE_CHECKING, Any, Type, Union +from typing import TYPE_CHECKING, Type, Union import pytest import trio from trio._file_io import AsyncIOWrapper -from trio._path import AsyncAutoWrapperType as WrapperType if TYPE_CHECKING: from collections.abc import Awaitable, Callable @@ -168,41 +167,6 @@ async def test_repr() -> None: assert repr(path) == "trio.Path('.')" -class MockWrapped: - unsupported = "unsupported" - _private = "private" - - -class _MockWrapper: - _forwards = MockWrapped - _wraps = MockWrapped - - -MockWrapper: Any = _MockWrapper # Disable type checking, it's a mock. - - -async def test_type_forwards_unsupported() -> None: - with pytest.raises(TypeError): - WrapperType.generate_forwards(MockWrapper, {}) - - -async def test_type_wraps_unsupported() -> None: - with pytest.raises(TypeError): - WrapperType.generate_wraps(MockWrapper, {}) - - -async def test_type_forwards_private() -> None: - WrapperType.generate_forwards(MockWrapper, {"unsupported": None}) - - assert not hasattr(MockWrapper, "_private") - - -async def test_type_wraps_private() -> None: - WrapperType.generate_wraps(MockWrapper, {"unsupported": None}) - - assert not hasattr(MockWrapper, "_private") - - @pytest.mark.parametrize("meth", [trio.Path.__init__, trio.Path.joinpath]) async def test_path_wraps_path( path: trio.Path, diff --git a/src/trio/_tests/type_tests/path.py b/src/trio/_tests/type_tests/path.py index b935a74e46..3d64a241f1 100644 --- a/src/trio/_tests/type_tests/path.py +++ b/src/trio/_tests/type_tests/path.py @@ -7,7 +7,7 @@ from typing import IO, Any, BinaryIO, List, Tuple import trio -from trio._path import _AsyncIOWrapper # pyright: ignore[reportPrivateUsage] +from trio._path import AsyncIOWrapper # pyright: ignore[reportPrivateUsage] from typing_extensions import assert_type @@ -39,7 +39,7 @@ def sync_attrs(path: trio.Path) -> None: assert_type(path.drive, str) assert_type(path.root, str) assert_type(path.anchor, str) - assert_type(path.parents[3], pathlib.Path) + assert_type(path.parents[3], trio.Path) assert_type(path.parent, trio.Path) assert_type(path.name, str) assert_type(path.suffix, str) @@ -119,16 +119,16 @@ async def async_attrs(path: trio.Path) -> None: async def open_results(path: trio.Path, some_int: int, some_str: str) -> None: # Check the overloads. - assert_type(await path.open(), _AsyncIOWrapper[io.TextIOWrapper]) - assert_type(await path.open("r"), _AsyncIOWrapper[io.TextIOWrapper]) - assert_type(await path.open("r+"), _AsyncIOWrapper[io.TextIOWrapper]) - assert_type(await path.open("w"), _AsyncIOWrapper[io.TextIOWrapper]) - assert_type(await path.open("rb", buffering=0), _AsyncIOWrapper[io.FileIO]) - assert_type(await path.open("rb+"), _AsyncIOWrapper[io.BufferedRandom]) - assert_type(await path.open("wb"), _AsyncIOWrapper[io.BufferedWriter]) - assert_type(await path.open("rb"), _AsyncIOWrapper[io.BufferedReader]) - assert_type(await path.open("rb", buffering=some_int), _AsyncIOWrapper[BinaryIO]) - assert_type(await path.open(some_str), _AsyncIOWrapper[IO[Any]]) + assert_type(await path.open(), AsyncIOWrapper[io.TextIOWrapper]) + assert_type(await path.open("r"), AsyncIOWrapper[io.TextIOWrapper]) + assert_type(await path.open("r+"), AsyncIOWrapper[io.TextIOWrapper]) + assert_type(await path.open("w"), AsyncIOWrapper[io.TextIOWrapper]) + assert_type(await path.open("rb", buffering=0), AsyncIOWrapper[io.FileIO]) + assert_type(await path.open("rb+"), AsyncIOWrapper[io.BufferedRandom]) + assert_type(await path.open("wb"), AsyncIOWrapper[io.BufferedWriter]) + assert_type(await path.open("rb"), AsyncIOWrapper[io.BufferedReader]) + assert_type(await path.open("rb", buffering=some_int), AsyncIOWrapper[BinaryIO]) + assert_type(await path.open(some_str), AsyncIOWrapper[IO[Any]]) # Check they produce the right types. file_bin = await path.open("rb+")