From 268a2e71bdd93c7f500f265d230c6b3b9bd11b2c Mon Sep 17 00:00:00 2001 From: Max Muoto Date: Sun, 11 May 2025 14:44:37 -0500 Subject: [PATCH] Update pathlib for 3.14 (#14006) --- stdlib/@tests/stubtest_allowlists/py314.txt | 10 ---- stdlib/@tests/test_cases/check_pathlib.py | 22 +++++++-- stdlib/VERSIONS | 1 + stdlib/{pathlib.pyi => pathlib/__init__.pyi} | 51 ++++++++++++-------- stdlib/pathlib/types.pyi | 8 +++ 5 files changed, 59 insertions(+), 33 deletions(-) rename stdlib/{pathlib.pyi => pathlib/__init__.pyi} (87%) create mode 100644 stdlib/pathlib/types.pyi diff --git a/stdlib/@tests/stubtest_allowlists/py314.txt b/stdlib/@tests/stubtest_allowlists/py314.txt index a2fb49e59..67a725257 100644 --- a/stdlib/@tests/stubtest_allowlists/py314.txt +++ b/stdlib/@tests/stubtest_allowlists/py314.txt @@ -147,16 +147,6 @@ multiprocessing.process.BaseProcess.interrupt multiprocessing.synchronize.SemLock.locked os.__all__ os.readinto -pathlib.Path.copy_into -pathlib.Path.copytree -pathlib.Path.delete -pathlib.Path.info -pathlib.Path.move -pathlib.Path.move_into -pathlib.Path.rmtree -pathlib.PurePath.is_relative_to -pathlib.PurePath.relative_to -pathlib.types pdb.__all__ pdb.Pdb.__init__ pdb.Pdb.checkline diff --git a/stdlib/@tests/test_cases/check_pathlib.py b/stdlib/@tests/test_cases/check_pathlib.py index 9b4d681c9..d3e85188b 100644 --- a/stdlib/@tests/test_cases/check_pathlib.py +++ b/stdlib/@tests/test_cases/check_pathlib.py @@ -4,6 +4,10 @@ import sys from pathlib import Path, PureWindowsPath from typing_extensions import assert_type + +class MyCustomPath(Path): ... + + if Path("asdf") == Path("asdf"): ... @@ -23,8 +27,20 @@ if PureWindowsPath("asdf") == Path("asdf"): # type: ignore if sys.version_info >= (3, 13): - - class MyCustomPath(Path): ... - pth = MyCustomPath.from_uri("file:///tmp/abc.txt") assert_type(pth, MyCustomPath) + + +if sys.version_info >= (3, 14): + pth = MyCustomPath("asdf") + # With text path, type should be preserved. + assert_type(pth.move_into("asdf"), MyCustomPath) + assert_type(pth.move("asdf"), MyCustomPath) + assert_type(pth.copy("asdf"), MyCustomPath) + assert_type(pth.copy_into("asdf"), MyCustomPath) + + # With an actual path type, that type should be preserved. + assert_type(pth.move_into(Path("asdf")), Path) + assert_type(pth.move(Path("asdf")), Path) + assert_type(pth.copy(Path("asdf")), Path) + assert_type(pth.copy_into(Path("asdf")), Path) diff --git a/stdlib/VERSIONS b/stdlib/VERSIONS index bea644c67..0b3129254 100644 --- a/stdlib/VERSIONS +++ b/stdlib/VERSIONS @@ -228,6 +228,7 @@ os: 3.0- ossaudiodev: 3.0-3.12 parser: 3.0-3.9 pathlib: 3.4- +pathlib.types: 3.14- pdb: 3.0- pickle: 3.0- pickletools: 3.0- diff --git a/stdlib/pathlib.pyi b/stdlib/pathlib/__init__.pyi similarity index 87% rename from stdlib/pathlib.pyi rename to stdlib/pathlib/__init__.pyi index 1e4d97770..b84fc6931 100644 --- a/stdlib/pathlib.pyi +++ b/stdlib/pathlib/__init__.pyi @@ -15,11 +15,16 @@ from collections.abc import Callable, Generator, Iterator, Sequence from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper from os import PathLike, stat_result from types import GenericAlias, TracebackType -from typing import IO, Any, BinaryIO, ClassVar, Literal, overload +from typing import IO, Any, BinaryIO, ClassVar, Literal, TypeVar, overload from typing_extensions import Never, Self, deprecated +_PathT = TypeVar("_PathT", bound=PurePath) + __all__ = ["PurePath", "PurePosixPath", "PureWindowsPath", "Path", "PosixPath", "WindowsPath"] +if sys.version_info >= (3, 14): + from pathlib.types import PathInfo + if sys.version_info >= (3, 13): __all__ += ["UnsupportedOperation"] @@ -63,7 +68,9 @@ class PurePath(PathLike[str]): def as_uri(self) -> str: ... def is_absolute(self) -> bool: ... def is_reserved(self) -> bool: ... - if sys.version_info >= (3, 12): + if sys.version_info >= (3, 14): + def is_relative_to(self, other: StrPath) -> bool: ... + elif sys.version_info >= (3, 12): def is_relative_to(self, other: StrPath, /, *_deprecated: StrPath) -> bool: ... else: def is_relative_to(self, *other: StrPath) -> bool: ... @@ -73,7 +80,9 @@ class PurePath(PathLike[str]): else: def match(self, path_pattern: str) -> bool: ... - if sys.version_info >= (3, 12): + if sys.version_info >= (3, 14): + def relative_to(self, other: StrPath, *, walk_up: bool = False) -> Self: ... + elif sys.version_info >= (3, 12): def relative_to(self, other: StrPath, /, *_deprecated: StrPath, walk_up: bool = False) -> Self: ... else: def relative_to(self, *other: StrPath) -> Self: ... @@ -154,17 +163,25 @@ class Path(PurePath): def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None: ... if sys.version_info >= (3, 14): - def copy(self, target: StrPath, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> None: ... - def copytree( - self, - target: StrPath, - *, - follow_symlinks: bool = True, - preserve_metadata: bool = False, - dirs_exist_ok: bool = False, - ignore: Callable[[Self], bool] | None = None, - on_error: Callable[[OSError], object] | None = None, - ) -> None: ... + + @property + def info(self) -> PathInfo: ... + @overload + def move_into(self, target_dir: _PathT) -> _PathT: ... # type: ignore[overload-overlap] + @overload + def move_into(self, target_dir: StrPath) -> Self: ... # type: ignore[overload-overlap] + @overload + def move(self, target: _PathT) -> _PathT: ... # type: ignore[overload-overlap] + @overload + def move(self, target: StrPath) -> Self: ... # type: ignore[overload-overlap] + @overload + def copy_into(self, target_dir: _PathT, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> _PathT: ... # type: ignore[overload-overlap] + @overload + def copy_into(self, target_dir: StrPath, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> Self: ... # type: ignore[overload-overlap] + @overload + def copy(self, target: _PathT, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> _PathT: ... # type: ignore[overload-overlap] + @overload + def copy(self, target: StrPath, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> Self: ... # type: ignore[overload-overlap] # Adapted from builtins.open # Text mode: always returns a TextIOWrapper @@ -253,9 +270,6 @@ class Path(PurePath): def resolve(self, strict: bool = False) -> Self: ... def rmdir(self) -> None: ... - if sys.version_info >= (3, 14): - def delete(self, ignore_errors: bool = False, on_error: Callable[[OSError], object] | None = None) -> None: ... - def symlink_to(self, target: StrOrBytesPath, target_is_directory: bool = False) -> None: ... if sys.version_info >= (3, 10): def hardlink_to(self, target: StrOrBytesPath) -> None: ... @@ -286,9 +300,6 @@ class Path(PurePath): self, top_down: bool = ..., on_error: Callable[[OSError], object] | None = ..., follow_symlinks: bool = ... ) -> Iterator[tuple[Self, list[str], list[str]]]: ... - if sys.version_info >= (3, 14): - def rmtree(self, ignore_errors: bool = False, on_error: Callable[[OSError], object] | None = None) -> None: ... - class PosixPath(Path, PurePosixPath): ... class WindowsPath(Path, PureWindowsPath): ... diff --git a/stdlib/pathlib/types.pyi b/stdlib/pathlib/types.pyi new file mode 100644 index 000000000..9f9a65084 --- /dev/null +++ b/stdlib/pathlib/types.pyi @@ -0,0 +1,8 @@ +from typing import Protocol, runtime_checkable + +@runtime_checkable +class PathInfo(Protocol): + def exists(self, *, follow_symlinks: bool = True) -> bool: ... + def is_dir(self, *, follow_symlinks: bool = True) -> bool: ... + def is_file(self, *, follow_symlinks: bool = True) -> bool: ... + def is_symlink(self) -> bool: ...