From b6e1698933ce4a236a046ccd1fdf28d8c7f500f8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 12:34:31 +0200 Subject: [PATCH 1/4] [pre-commit.ci] pre-commit autoupdate (#2816) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f7eca86de..70c7ebc5d0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 4b2fd9877cd47e290b58a355e758176809ea0fc0 Mon Sep 17 00:00:00 2001 From: Spencer Brown Date: Tue, 17 Oct 2023 16:12:19 +1000 Subject: [PATCH 2/4] Fix some type issues with `trio.Path` (#2815) * Reorder trio.Path stubs to match typeshed, fix incorrect sync/async definitions * Fully type test_path * Add type tests for trio.Path * Fix docs build failure - Path.link_to was removed in 3.12 * Replace deprecated tmpdir fixture with tmp_path --- docs/source/conf.py | 14 +++ pyproject.toml | 1 - trio/_file_io.py | 6 +- trio/_path.py | 143 +++++++++++++++++--------- trio/_tests/test_exports.py | 22 +++- trio/_tests/test_file_io.py | 5 +- trio/_tests/test_path.py | 104 +++++++++++-------- trio/_tests/type_tests/path.py | 141 +++++++++++++++++++++++++ trio/_tests/verify_types_darwin.json | 2 +- trio/_tests/verify_types_linux.json | 2 +- trio/_tests/verify_types_windows.json | 2 +- 11 files changed, 336 insertions(+), 106 deletions(-) create mode 100644 trio/_tests/type_tests/path.py diff --git a/docs/source/conf.py b/docs/source/conf.py index ee23ce587f..b85974a901 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -102,6 +102,8 @@ def autodoc_process_signature( def setup(app): app.add_css_file("hackrtd.css") app.connect("autodoc-process-signature", autodoc_process_signature) + # After Intersphinx runs, add additional mappings. + app.connect("builder-inited", add_intersphinx, priority=1000) # -- General configuration ------------------------------------------------ @@ -131,6 +133,18 @@ def setup(app): "sniffio": ("https://sniffio.readthedocs.io/en/latest/", None), } + +def add_intersphinx(app) -> None: + """Add some specific intersphinx mappings.""" + # This has been removed in Py3.12, so add a link to the 3.11 version with deprecation warnings. + app.builder.env.intersphinx_inventory["py:method"]["pathlib.Path.link_to"] = ( + "Python", + "3.11", + "https://docs.python.org/3.11/library/pathlib.html#pathlib.Path.link_to", + "-", + ) + + autodoc_member_order = "bysource" # Add any paths that contain templates here, relative to this directory. diff --git a/pyproject.toml b/pyproject.toml index 73b110ebac..7b88df44a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,6 @@ module = [ "trio/_tests/test_highlevel_open_unix_stream", "trio/_tests/test_highlevel_serve_listeners", "trio/_tests/test_highlevel_ssl_helpers", -"trio/_tests/test_path", "trio/_tests/test_scheduler_determinism", "trio/_tests/test_ssl", "trio/_tests/test_subprocess", diff --git a/trio/_file_io.py b/trio/_file_io.py index 6b79ae25b5..4a5650b453 100644 --- a/trio/_file_io.py +++ b/trio/_file_io.py @@ -195,8 +195,8 @@ def tell(self) -> int: ... class _CanTruncate(Protocol): def truncate(self, size: int | None = ..., /) -> int: ... - class _CanWrite(Protocol[AnyStr_contra]): - def write(self, data: AnyStr_contra, /) -> int: ... + class _CanWrite(Protocol[T_contra]): + def write(self, data: T_contra, /) -> int: ... class _CanWriteLines(Protocol[T_contra]): # The lines parameter varies for bytes/str, so use a typevar to make the async match. @@ -334,7 +334,7 @@ async def readlines(self: AsyncIOWrapper[_CanReadLines[AnyStr]]) -> list[AnyStr] async def seek(self: AsyncIOWrapper[_CanSeek], target: int, whence: int = 0, /) -> int: ... async def tell(self: AsyncIOWrapper[_CanTell]) -> int: ... async def truncate(self: AsyncIOWrapper[_CanTruncate], size: int | None = None, /) -> int: ... - async def write(self: AsyncIOWrapper[_CanWrite[AnyStr]], data: AnyStr, /) -> int: ... + async def write(self: AsyncIOWrapper[_CanWrite[T]], data: T, /) -> int: ... async def writelines(self: AsyncIOWrapper[_CanWriteLines[T]], lines: Iterable[T], /) -> None: ... async def readinto1(self: AsyncIOWrapper[_CanReadInto1], buffer: Buffer, /) -> int: ... async def peek(self: AsyncIOWrapper[_CanPeek[AnyStr]], size: int = 0, /) -> AnyStr: ... diff --git a/trio/_path.py b/trio/_path.py index b1cb623190..fb01420a75 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -5,7 +5,7 @@ import pathlib import sys import types -from collections.abc import Awaitable, Callable, Iterable +from collections.abc import Awaitable, Callable, Iterable, Sequence from functools import partial from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper from typing import ( @@ -245,7 +245,7 @@ def __fspath__(self) -> str: return os.fspath(self._wrapped) @overload - def open( + async def open( self, mode: OpenTextMode = "r", buffering: int = -1, @@ -256,7 +256,7 @@ def open( ... @overload - def open( + async def open( self, mode: OpenBinaryMode, buffering: Literal[0], @@ -267,7 +267,7 @@ def open( ... @overload - def open( + async def open( self, mode: OpenBinaryModeUpdating, buffering: Literal[-1, 1] = -1, @@ -278,7 +278,7 @@ def open( ... @overload - def open( + async def open( self, mode: OpenBinaryModeWriting, buffering: Literal[-1, 1] = -1, @@ -289,7 +289,7 @@ def open( ... @overload - def open( + async def open( self, mode: OpenBinaryModeReading, buffering: Literal[-1, 1] = -1, @@ -300,7 +300,7 @@ def open( ... @overload - def open( + async def open( self, mode: OpenBinaryMode, buffering: int = -1, @@ -311,7 +311,7 @@ def open( ... @overload - def open( + async def open( self, mode: str, buffering: int = -1, @@ -338,11 +338,53 @@ async def open(self, *args: Any, **kwargs: Any) -> _AsyncIOWrapper[IO[Any]]: 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: ... - # wrapped methods handled by __getattr__ - async def absolute(self) -> Path: ... - async def as_posix(self) -> str: ... - async def as_uri(self) -> str: ... + 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: ... @@ -351,51 +393,50 @@ async def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: ... async def stat(self) -> os.stat_result: ... async def chmod(self, mode: int) -> None: ... - @classmethod - async def cwd(self) -> Path: ... - async def exists(self) -> bool: ... - async def expanduser(self) -> Path: ... async def glob(self, pattern: str) -> Iterable[Path]: ... - async def home(self) -> Path: ... - async def is_absolute(self) -> bool: ... - async def is_block_device(self) -> bool: ... - async def is_char_device(self) -> bool: ... async def is_dir(self) -> bool: ... - async def is_fifo(self) -> bool: ... async def is_file(self) -> bool: ... - async def is_reserved(self) -> bool: ... - async def is_socket(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 joinpath(self, *other: StrPath) -> Path: ... async def lchmod(self, mode: int) -> None: ... async def lstat(self) -> os.stat_result: ... - async def match(self, path_pattern: str) -> bool: ... async def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None: ... - async def read_bytes(self) -> bytes: ... - async def read_text(self, encoding: str | None = None, errors: str | None = None) -> str: ... - async def relative_to(self, *other: StrPath) -> Path: ... + 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: ... if sys.version_info >= (3, 8): - def rename(self, target: str | pathlib.PurePath) -> Path: ... - def replace(self, target: str | pathlib.PurePath) -> Path: ... + async def rename(self, target: StrPath) -> Path: ... + async def replace(self, target: StrPath) -> Path: ... else: - def rename(self, target: str | pathlib.PurePath) -> None: ... - def replace(self, target: str | pathlib.PurePath) -> None: ... - + async def rename(self, target: StrPath) -> None: ... + async def replace(self, target: StrPath) -> None: ... async def resolve(self, strict: bool = False) -> Path: ... async def rglob(self, pattern: str) -> Iterable[Path]: ... async def rmdir(self) -> None: ... - async def samefile(self, other_path: str | bytes | int | Path) -> bool: ... - async def symlink_to(self, target: str | Path, target_is_directory: bool = False) -> 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: ... if sys.version_info >= (3, 8): - def unlink(self, missing_ok: bool = False) -> None: ... + async def unlink(self, missing_ok: bool = False) -> None: ... else: - def unlink(self) -> None: ... - async def with_name(self, name: str) -> Path: ... - async def with_suffix(self, suffix: str) -> Path: ... + async def unlink(self) -> 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): @@ -412,17 +453,6 @@ async def write_text( errors: str | None = None, ) -> int: ... - 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 is_relative_to(self, *other: StrPath) -> bool: ... - async def with_stem(self, stem: str) -> Path: ... - async def readlink(self) -> Path: ... - if sys.version_info >= (3, 10): - async def hardlink_to(self, target: str | pathlib.Path) -> None: ... if sys.version_info < (3, 12): async def link_to(self, target: StrPath | bytes) -> None: ... if sys.version_info >= (3, 12): @@ -432,7 +462,7 @@ async def with_segments(self, *pathsegments: StrPath) -> Path: ... Path.iterdir.__doc__ = """ - Like :meth:`pathlib.Path.iterdir`, but async. + Like :meth:`~pathlib.Path.iterdir`, but async. This is an async method that returns a synchronous iterator, so you use it like:: @@ -446,6 +476,17 @@ async def with_segments(self, *pathsegments: StrPath) -> Path: ... """ +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. + + :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. +""" + # 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. diff --git a/trio/_tests/test_exports.py b/trio/_tests/test_exports.py index c3d8a03b63..e756e8ad69 100644 --- a/trio/_tests/test_exports.py +++ b/trio/_tests/test_exports.py @@ -126,7 +126,7 @@ def iter_modules( # https://github.com/pypa/setuptools/issues/3274 "ignore:module 'sre_constants' is deprecated:DeprecationWarning", ) -def test_static_tool_sees_all_symbols(tool, modname, tmpdir): +def test_static_tool_sees_all_symbols(tool, modname, tmp_path): module = importlib.import_module(modname) def no_underscores(symbols): @@ -273,7 +273,7 @@ def no_underscores(symbols): @pytest.mark.parametrize("module_name", PUBLIC_MODULE_NAMES) @pytest.mark.parametrize("tool", ["jedi", "mypy"]) def test_static_tool_sees_class_members( - tool: str, module_name: str, tmpdir: Path + tool: str, module_name: str, tmp_path: Path ) -> None: module = PUBLIC_MODULES[PUBLIC_MODULE_NAMES.index(module_name)] @@ -494,9 +494,25 @@ def lookup_symbol(symbol): missing.remove("__aiter__") missing.remove("__anext__") - # intentionally hidden behind type guard + # __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 missing or extra: # pragma: no cover errors[f"{module_name}.{class_name}"] = { diff --git a/trio/_tests/test_file_io.py b/trio/_tests/test_file_io.py index bae426cf48..3913d25298 100644 --- a/trio/_tests/test_file_io.py +++ b/trio/_tests/test_file_io.py @@ -1,6 +1,7 @@ import importlib import io import os +import pathlib import re from typing import List, Tuple from unittest import mock @@ -14,8 +15,8 @@ @pytest.fixture -def path(tmpdir): - return os.fspath(tmpdir.join("test")) +def path(tmp_path: pathlib.Path) -> str: + return os.fspath(tmp_path / "test") @pytest.fixture diff --git a/trio/_tests/test_path.py b/trio/_tests/test_path.py index bfef1aaf2c..b0b16f9e4f 100644 --- a/trio/_tests/test_path.py +++ b/trio/_tests/test_path.py @@ -1,40 +1,48 @@ +from __future__ import annotations + import os import pathlib +from collections.abc import Awaitable, Callable +from typing import Any, Type, Union import pytest import trio from trio._file_io import AsyncIOWrapper -from trio._path import AsyncAutoWrapperType as Type +from trio._path import AsyncAutoWrapperType as WrapperType @pytest.fixture -def path(tmpdir): - p = str(tmpdir.join("test")) - return trio.Path(p) +def path(tmp_path: pathlib.Path) -> trio.Path: + return trio.Path(tmp_path / "test") -def method_pair(path, method_name): - path = pathlib.Path(path) +def method_pair( + path: str, + method_name: str, +) -> tuple[Callable[[], object], Callable[[], Awaitable[object]]]: + sync_path = pathlib.Path(path) async_path = trio.Path(path) - return getattr(path, method_name), getattr(async_path, method_name) + return getattr(sync_path, method_name), getattr(async_path, method_name) -async def test_open_is_async_context_manager(path): +async def test_open_is_async_context_manager(path: trio.Path) -> None: async with await path.open("w") as f: assert isinstance(f, AsyncIOWrapper) assert f.closed -async def test_magic(): +async def test_magic() -> None: path = trio.Path("test") assert str(path) == "test" assert bytes(path) == b"test" -cls_pairs = [ +EitherPathType = Union[Type[trio.Path], Type[pathlib.Path]] +PathOrStrType = Union[EitherPathType, Type[str]] +cls_pairs: list[tuple[EitherPathType, EitherPathType]] = [ (trio.Path, pathlib.Path), (pathlib.Path, trio.Path), (trio.Path, trio.Path), @@ -42,7 +50,7 @@ async def test_magic(): @pytest.mark.parametrize("cls_a,cls_b", cls_pairs) -async def test_cmp_magic(cls_a, cls_b): +async def test_cmp_magic(cls_a: EitherPathType, cls_b: EitherPathType) -> None: a, b = cls_a(""), cls_b("") assert a == b assert not a != b @@ -60,7 +68,7 @@ async def test_cmp_magic(cls_a, cls_b): # upstream python3.8 bug: we should also test (pathlib.Path, trio.Path), but # __*div__ does not properly raise NotImplementedError like the other comparison # magic, so trio.Path's implementation does not get dispatched -cls_pairs = [ +cls_pairs_str: list[tuple[PathOrStrType, PathOrStrType]] = [ (trio.Path, pathlib.Path), (trio.Path, trio.Path), (trio.Path, str), @@ -68,11 +76,12 @@ async def test_cmp_magic(cls_a, cls_b): ] -@pytest.mark.parametrize("cls_a,cls_b", cls_pairs) -async def test_div_magic(cls_a, cls_b): +@pytest.mark.parametrize("cls_a,cls_b", cls_pairs_str) +async def test_div_magic(cls_a: PathOrStrType, cls_b: PathOrStrType) -> None: a, b = cls_a("a"), cls_b("b") - result = a / b + result = a / b # type: ignore[operator] + # Type checkers think str / str could happen. Check each combo manually in type_tests/. assert isinstance(result, trio.Path) assert str(result) == os.path.join("a", "b") @@ -81,29 +90,32 @@ async def test_div_magic(cls_a, cls_b): "cls_a,cls_b", [(trio.Path, pathlib.Path), (trio.Path, trio.Path)] ) @pytest.mark.parametrize("path", ["foo", "foo/bar/baz", "./foo"]) -async def test_hash_magic(cls_a, cls_b, path): +async def test_hash_magic( + cls_a: EitherPathType, cls_b: EitherPathType, path: str +) -> None: a, b = cls_a(path), cls_b(path) assert hash(a) == hash(b) -async def test_forwarded_properties(path): +async def test_forwarded_properties(path: trio.Path) -> None: # use `name` as a representative of forwarded properties assert "name" in dir(path) assert path.name == "test" -async def test_async_method_signature(path): +async def test_async_method_signature(path: trio.Path) -> None: # use `resolve` as a representative of wrapped methods assert path.resolve.__name__ == "resolve" assert path.resolve.__qualname__ == "Path.resolve" + assert path.resolve.__doc__ is not None assert "pathlib.Path.resolve" in path.resolve.__doc__ @pytest.mark.parametrize("method_name", ["is_dir", "is_file"]) -async def test_compare_async_stat_methods(method_name): +async def test_compare_async_stat_methods(method_name: str) -> None: method, async_method = method_pair(".", method_name) result = method() @@ -112,13 +124,13 @@ async def test_compare_async_stat_methods(method_name): assert result == async_result -async def test_invalid_name_not_wrapped(path): +async def test_invalid_name_not_wrapped(path: trio.Path) -> None: with pytest.raises(AttributeError): getattr(path, "invalid_fake_attr") @pytest.mark.parametrize("method_name", ["absolute", "resolve"]) -async def test_async_methods_rewrap(method_name): +async def test_async_methods_rewrap(method_name: str) -> None: method, async_method = method_pair(".", method_name) result = method() @@ -128,27 +140,27 @@ async def test_async_methods_rewrap(method_name): assert str(result) == str(async_result) -async def test_forward_methods_rewrap(path, tmpdir): +async def test_forward_methods_rewrap(path: trio.Path, tmp_path: pathlib.Path) -> None: with_name = path.with_name("foo") with_suffix = path.with_suffix(".py") assert isinstance(with_name, trio.Path) - assert with_name == tmpdir.join("foo") + assert with_name == tmp_path / "foo" assert isinstance(with_suffix, trio.Path) - assert with_suffix == tmpdir.join("test.py") + assert with_suffix == tmp_path / "test.py" -async def test_forward_properties_rewrap(path): +async def test_forward_properties_rewrap(path: trio.Path) -> None: assert isinstance(path.parent, trio.Path) -async def test_forward_methods_without_rewrap(path, tmpdir): +async def test_forward_methods_without_rewrap(path: trio.Path) -> None: path = await path.parent.resolve() assert path.as_uri().startswith("file:///") -async def test_repr(): +async def test_repr() -> None: path = trio.Path(".") assert repr(path) == "trio.Path('.')" @@ -159,35 +171,41 @@ class MockWrapped: _private = "private" -class MockWrapper: +class _MockWrapper: _forwards = MockWrapped _wraps = MockWrapped -async def test_type_forwards_unsupported(): +MockWrapper: Any = _MockWrapper # Disable type checking, it's a mock. + + +async def test_type_forwards_unsupported() -> None: with pytest.raises(TypeError): - Type.generate_forwards(MockWrapper, {}) + WrapperType.generate_forwards(MockWrapper, {}) -async def test_type_wraps_unsupported(): +async def test_type_wraps_unsupported() -> None: with pytest.raises(TypeError): - Type.generate_wraps(MockWrapper, {}) + WrapperType.generate_wraps(MockWrapper, {}) -async def test_type_forwards_private(): - Type.generate_forwards(MockWrapper, {"unsupported": None}) +async def test_type_forwards_private() -> None: + WrapperType.generate_forwards(MockWrapper, {"unsupported": None}) assert not hasattr(MockWrapper, "_private") -async def test_type_wraps_private(): - Type.generate_wraps(MockWrapper, {"unsupported": None}) +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, meth): +async def test_path_wraps_path( + path: trio.Path, + meth: Callable[[trio.Path, trio.Path], object], +) -> None: wrapped = await path.absolute() result = meth(path, wrapped) if result is None: @@ -196,17 +214,17 @@ async def test_path_wraps_path(path, meth): assert wrapped == result -async def test_path_nonpath(): +async def test_path_nonpath() -> None: with pytest.raises(TypeError): - trio.Path(1) + trio.Path(1) # type: ignore -async def test_open_file_can_open_path(path): +async def test_open_file_can_open_path(path: trio.Path) -> None: async with await trio.open_file(path, "w") as f: assert f.name == os.fspath(path) -async def test_globmethods(path): +async def test_globmethods(path: trio.Path) -> None: # Populate a directory tree await path.mkdir() await (path / "foo").mkdir() @@ -235,7 +253,7 @@ async def test_globmethods(path): assert entries == {"_bar.txt", "bar.txt"} -async def test_iterdir(path): +async def test_iterdir(path: trio.Path) -> None: # Populate a directory await path.mkdir() await (path / "foo").mkdir() @@ -249,7 +267,7 @@ async def test_iterdir(path): assert entries == {"bar.txt", "foo"} -async def test_classmethods(): +async def test_classmethods() -> None: assert isinstance(await trio.Path.home(), trio.Path) # pathlib.Path has only two classmethods diff --git a/trio/_tests/type_tests/path.py b/trio/_tests/type_tests/path.py new file mode 100644 index 0000000000..321fd1043b --- /dev/null +++ b/trio/_tests/type_tests/path.py @@ -0,0 +1,141 @@ +"""Path wrapping is quite complex, ensure all methods are understood as wrapped correctly.""" +import io +import os +import pathlib +import sys +from typing import IO, Any, BinaryIO, List, Tuple + +import trio +from trio._path import _AsyncIOWrapper +from typing_extensions import assert_type + + +def operator_checks(text: str, tpath: trio.Path, ppath: pathlib.Path) -> None: + """Verify operators produce the right results.""" + assert_type(tpath / ppath, trio.Path) + assert_type(tpath / tpath, trio.Path) + assert_type(tpath / text, trio.Path) + assert_type(text / tpath, trio.Path) + + assert_type(tpath > tpath, bool) + assert_type(tpath >= tpath, bool) + assert_type(tpath < tpath, bool) + assert_type(tpath <= tpath, bool) + + assert_type(tpath > ppath, bool) + assert_type(tpath >= ppath, bool) + assert_type(tpath < ppath, bool) + assert_type(tpath <= ppath, bool) + + assert_type(ppath > tpath, bool) + assert_type(ppath >= tpath, bool) + assert_type(ppath < tpath, bool) + assert_type(ppath <= tpath, bool) + + +def sync_attrs(path: trio.Path) -> None: + assert_type(path.parts, Tuple[str, ...]) + 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.parent, trio.Path) + assert_type(path.name, str) + assert_type(path.suffix, str) + assert_type(path.suffixes, List[str]) + assert_type(path.stem, str) + assert_type(path.as_posix(), str) + assert_type(path.as_uri(), str) + assert_type(path.is_absolute(), bool) + if sys.version_info > (3, 9): + assert_type(path.is_relative_to(path), bool) + assert_type(path.is_reserved(), bool) + assert_type(path.joinpath(path, "folder"), trio.Path) + assert_type(path.match("*.py"), bool) + assert_type(path.relative_to("/usr"), trio.Path) + if sys.version_info > (3, 12): + assert_type(path.relative_to("/", walk_up=True), bool) + assert_type(path.with_name("filename.txt"), trio.Path) + if sys.version_info > (3, 9): + assert_type(path.with_stem("readme"), trio.Path) + assert_type(path.with_suffix(".log"), trio.Path) + + +async def async_attrs(path: trio.Path) -> None: + assert_type(await trio.Path.cwd(), trio.Path) + assert_type(await trio.Path.home(), trio.Path) + assert_type(await path.stat(), os.stat_result) + assert_type(await path.chmod(0o777), None) + assert_type(await path.exists(), bool) + assert_type(await path.expanduser(), trio.Path) + for result in await path.glob("*.py"): + assert_type(result, trio.Path) + if sys.platform != "win32": + assert_type(await path.group(), str) + assert_type(await path.is_dir(), bool) + assert_type(await path.is_file(), bool) + if sys.version_info > (3, 12): + assert_type(await path.is_junction(), bool) + if sys.platform != "win32": + assert_type(await path.is_mount(), bool) + assert_type(await path.is_symlink(), bool) + assert_type(await path.is_socket(), bool) + assert_type(await path.is_fifo(), bool) + assert_type(await path.is_block_device(), bool) + assert_type(await path.is_char_device(), bool) + for child_iter in await path.iterdir(): + assert_type(child_iter, trio.Path) + # TODO: Path.walk() in 3.12 + assert_type(await path.lchmod(0o111), None) + assert_type(await path.lstat(), os.stat_result) + assert_type(await path.mkdir(mode=0o777, parents=True, exist_ok=False), None) + # Open done separately. + if sys.platform != "win32": + assert_type(await path.owner(), str) + assert_type(await path.read_bytes(), bytes) + assert_type(await path.read_text(encoding="utf16", errors="replace"), str) + if sys.version_info > (3, 9): + assert_type(await path.readlink(), trio.Path) + assert_type(await path.rename("another"), trio.Path) + assert_type(await path.replace(path), trio.Path) + assert_type(await path.resolve(), trio.Path) + for child_glob in await path.glob("*.py"): + assert_type(child_glob, trio.Path) + for child_rglob in await path.rglob("*.py"): + assert_type(child_rglob, trio.Path) + assert_type(await path.rmdir(), None) + assert_type(await path.samefile("something_else"), bool) + assert_type(await path.symlink_to("somewhere"), None) + if sys.version_info > (3, 10): + assert_type(await path.hardlink_to("elsewhere"), None) + assert_type(await path.touch(), None) + assert_type(await path.unlink(missing_ok=True), None) + assert_type(await path.write_bytes(b"123"), int) + assert_type( + await path.write_text("hello", encoding="utf32le", errors="ignore"), int + ) + + +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]]) + + # Check they produce the right types. + file_bin = await path.open("rb+") + assert_type(await file_bin.read(), bytes) + assert_type(await file_bin.write(b"test"), int) + assert_type(await file_bin.seek(32), int) + + file_text = await path.open("r+t") + assert_type(await file_text.read(), str) + assert_type(await file_text.write("test"), int) + assert_type(await file_text.readlines(), List[str]) diff --git a/trio/_tests/verify_types_darwin.json b/trio/_tests/verify_types_darwin.json index e83a324714..40238f367e 100644 --- a/trio/_tests/verify_types_darwin.json +++ b/trio/_tests/verify_types_darwin.json @@ -76,7 +76,7 @@ ], "otherSymbolCounts": { "withAmbiguousType": 0, - "withKnownType": 685, + "withKnownType": 699, "withUnknownType": 0 }, "packageName": "trio" diff --git a/trio/_tests/verify_types_linux.json b/trio/_tests/verify_types_linux.json index 7c9d745dba..6a8a3933d9 100644 --- a/trio/_tests/verify_types_linux.json +++ b/trio/_tests/verify_types_linux.json @@ -64,7 +64,7 @@ ], "otherSymbolCounts": { "withAmbiguousType": 0, - "withKnownType": 685, + "withKnownType": 699, "withUnknownType": 0 }, "packageName": "trio" diff --git a/trio/_tests/verify_types_windows.json b/trio/_tests/verify_types_windows.json index a58416fe76..a983537cd4 100644 --- a/trio/_tests/verify_types_windows.json +++ b/trio/_tests/verify_types_windows.json @@ -100,7 +100,7 @@ ], "otherSymbolCounts": { "withAmbiguousType": 0, - "withKnownType": 677, + "withKnownType": 691, "withUnknownType": 0 }, "packageName": "trio" From f2cb7b1e759e9220721c923eb12e37c5fab3414a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 06:47:24 +0000 Subject: [PATCH 3/4] [pre-commit.ci] pre-commit autoupdate (#2818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.0.292 → v0.1.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.292...v0.1.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: EXPLOSION --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 70c7ebc5d0..20a83484a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.292 + rev: v0.1.0 hooks: - id: ruff types: [file] From b161fecb3820ad1129a8eb1b458bbfb88df60d6e Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Tue, 17 Oct 2023 06:24:56 -0500 Subject: [PATCH 4/4] Add type annotations for windows_pipes and its test (#2812) * Add type annotations for windows_pipes and its test --- pyproject.toml | 4 ---- trio/_tests/test_windows_pipes.py | 40 +++++++++++++++++-------------- trio/_windows_pipes.py | 20 +++++++++------- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7b88df44a8..7ecbbe5f4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,9 +83,6 @@ disallow_untyped_calls = false # files not yet fully typed [[tool.mypy.overrides]] module = [ -# internal -"trio/_windows_pipes", - # tests "trio/testing/_fake_net", "trio/_core/_tests/test_guest_mode", @@ -115,7 +112,6 @@ module = [ "trio/_tests/test_tracing", "trio/_tests/test_util", "trio/_tests/test_wait_for_object", -"trio/_tests/test_windows_pipes", "trio/_tests/tools/test_gen_exports", ] check_untyped_defs = false diff --git a/trio/_tests/test_windows_pipes.py b/trio/_tests/test_windows_pipes.py index 5c4bae7d25..f0783b7b06 100644 --- a/trio/_tests/test_windows_pipes.py +++ b/trio/_tests/test_windows_pipes.py @@ -1,37 +1,41 @@ +from __future__ import annotations + import sys -from typing import Any, Tuple +from typing import TYPE_CHECKING import pytest from .. import _core from ..testing import check_one_way_stream, wait_all_tasks_blocked +# Mark all the tests in this file as being windows-only +pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="windows only") + +assert ( # Skip type checking when not on Windows + sys.platform == "win32" or not TYPE_CHECKING +) + if sys.platform == "win32": from asyncio.windows_utils import pipe from .._core._windows_cffi import _handle, kernel32 from .._windows_pipes import PipeReceiveStream, PipeSendStream -else: - pytestmark = pytest.mark.skip(reason="windows only") - pipe: Any = None - PipeSendStream: Any = None - PipeReceiveStream: Any = None -async def make_pipe() -> Tuple[PipeSendStream, PipeReceiveStream]: +async def make_pipe() -> tuple[PipeSendStream, PipeReceiveStream]: """Makes a new pair of pipes.""" (r, w) = pipe() return PipeSendStream(w), PipeReceiveStream(r) -async def test_pipe_typecheck(): +async def test_pipe_typecheck() -> None: with pytest.raises(TypeError): - PipeSendStream(1.0) + PipeSendStream(1.0) # type: ignore[arg-type] with pytest.raises(TypeError): - PipeReceiveStream(None) + PipeReceiveStream(None) # type: ignore[arg-type] -async def test_pipe_error_on_close(): +async def test_pipe_error_on_close() -> None: # Make sure we correctly handle a failure from kernel32.CloseHandle r, w = pipe() @@ -47,18 +51,18 @@ async def test_pipe_error_on_close(): await receive_stream.aclose() -async def test_pipes_combined(): +async def test_pipes_combined() -> None: write, read = await make_pipe() count = 2**20 replicas = 3 - async def sender(): + async def sender() -> None: async with write: big = bytearray(count) for _ in range(replicas): await write.send_all(big) - async def reader(): + async def reader() -> None: async with read: await wait_all_tasks_blocked() total_received = 0 @@ -76,7 +80,7 @@ async def reader(): n.start_soon(reader) -async def test_async_with(): +async def test_async_with() -> None: w, r = await make_pipe() async with w, r: pass @@ -87,11 +91,11 @@ async def test_async_with(): await r.receive_some(10) -async def test_close_during_write(): +async def test_close_during_write() -> None: w, r = await make_pipe() async with _core.open_nursery() as nursery: - async def write_forever(): + async def write_forever() -> None: with pytest.raises(_core.ClosedResourceError) as excinfo: while True: await w.send_all(b"x" * 4096) @@ -102,7 +106,7 @@ async def write_forever(): await w.aclose() -async def test_pipe_fully(): +async def test_pipe_fully() -> None: # passing make_clogged_pipe tests wait_send_all_might_not_block, and we # can't implement that on Windows await check_one_way_stream(make_pipe, None) diff --git a/trio/_windows_pipes.py b/trio/_windows_pipes.py index a4f59b2c70..43592807b8 100644 --- a/trio/_windows_pipes.py +++ b/trio/_windows_pipes.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys from typing import TYPE_CHECKING @@ -23,10 +25,10 @@ def __init__(self, handle: int) -> None: _core.register_with_iocp(self.handle) @property - def closed(self): + def closed(self) -> bool: return self.handle == -1 - def close(self): + def close(self) -> None: if self.closed: return handle = self.handle @@ -34,7 +36,7 @@ def close(self): if not kernel32.CloseHandle(_handle(handle)): raise_winerror() - def __del__(self): + def __del__(self) -> None: self.close() @@ -50,7 +52,7 @@ def __init__(self, handle: int) -> None: "another task is currently using this pipe" ) - async def send_all(self, data: bytes): + async def send_all(self, data: bytes) -> None: with self._conflict_detector: if self._handle_holder.closed: raise _core.ClosedResourceError("this pipe is already closed") @@ -76,10 +78,10 @@ async def wait_send_all_might_not_block(self) -> None: # not implemented yet, and probably not needed await _core.checkpoint() - def close(self): + def close(self) -> None: self._handle_holder.close() - async def aclose(self): + async def aclose(self) -> None: self.close() await _core.checkpoint() @@ -94,7 +96,7 @@ def __init__(self, handle: int) -> None: "another task is currently using this pipe" ) - async def receive_some(self, max_bytes=None) -> bytes: + async def receive_some(self, max_bytes: int | None = None) -> bytes: with self._conflict_detector: if self._handle_holder.closed: raise _core.ClosedResourceError("this pipe is already closed") @@ -133,9 +135,9 @@ async def receive_some(self, max_bytes=None) -> bytes: del buffer[size:] return buffer - def close(self): + def close(self) -> None: self._handle_holder.close() - async def aclose(self): + async def aclose(self) -> None: self.close() await _core.checkpoint()