Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add new pathlib base classes for 3.13 #12937

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions stdlib/VERSIONS
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ os: 3.0-
ossaudiodev: 3.0-3.12
parser: 3.0-3.9
pathlib: 3.4-
pathlib._abc: 3.13-
pdb: 3.0-
pickle: 3.0-
pickletools: 3.0-
Expand Down
17 changes: 12 additions & 5 deletions stdlib/pathlib.pyi → stdlib/pathlib/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,23 @@ from types import TracebackType
from typing import IO, Any, BinaryIO, ClassVar, Literal, overload
from typing_extensions import Self, deprecated

if sys.version_info >= (3, 13):
from pathlib._abc import PathBase as _PathBase, PurePathBase as _PurePathBase
else:
_PathBase = object
_PurePathBase = object

if sys.version_info >= (3, 9):
from types import GenericAlias

__all__ = ["PurePath", "PurePosixPath", "PureWindowsPath", "Path", "PosixPath", "WindowsPath"]

if sys.version_info >= (3, 13):
from pathlib._abc import UnsupportedOperation as UnsupportedOperation

__all__ += ["UnsupportedOperation"]

class PurePath(PathLike[str]):
class PurePath(PathLike[str], _PurePathBase):
if sys.version_info >= (3, 13):
parser: ClassVar[types.ModuleType]
def full_match(self, pattern: StrPath, *, case_sensitive: bool | None = None) -> bool: ...
Expand Down Expand Up @@ -100,7 +108,9 @@ class PurePath(PathLike[str]):
class PurePosixPath(PurePath): ...
class PureWindowsPath(PurePath): ...

class Path(PurePath):
# This should be Path(_PathBase, PurePath), but _PathBase has to be at the end for the
# _Pathbase = object trick to work.
class Path(PurePath, _PathBase):
if sys.version_info >= (3, 12):
def __new__(cls, *args: StrPath, **kwargs: Unused) -> Self: ... # pyright: ignore[reportInconsistentConstructor]
else:
Expand Down Expand Up @@ -293,6 +303,3 @@ class Path(PurePath):

class PosixPath(Path, PurePosixPath): ...
class WindowsPath(Path, PureWindowsPath): ...

if sys.version_info >= (3, 13):
class UnsupportedOperation(NotImplementedError): ...
192 changes: 192 additions & 0 deletions stdlib/pathlib/_abc.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import types
from _typeshed import (
AnyOrLiteralStr,
BytesPath,
OpenBinaryMode,
OpenBinaryModeReading,
OpenBinaryModeUpdating,
OpenBinaryModeWriting,
OpenTextMode,
ReadableBuffer,
StrOrBytesPath,
StrPath,
)
from collections.abc import Callable, Generator, Iterator, Sequence
from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper
from os import PathLike, stat_result
from typing import IO, Any, AnyStr, BinaryIO, ClassVar, Literal, overload
from typing_extensions import LiteralString, Self

__all__ = ["UnsupportedOperation"]

class UnsupportedOperation(NotImplementedError): ...

class ParserBase:
@property
def sep(self) -> str: ...
@overload
def join(self, path: LiteralString, *paths: LiteralString) -> LiteralString: ...
@overload
def join(self, path: StrPath, *paths: StrPath) -> str: ...
@overload
def join(self, path: BytesPath, *paths: BytesPath) -> bytes: ...
@overload
def split(self, path: PathLike[AnyStr]) -> tuple[AnyStr, AnyStr]: ...
@overload
def split(self, path: AnyOrLiteralStr) -> tuple[AnyOrLiteralStr, AnyOrLiteralStr]: ...
@overload
def splitdrive(self, path: PathLike[AnyStr]) -> tuple[AnyStr, AnyStr]: ...
@overload
def splitdrive(self, path: AnyOrLiteralStr) -> tuple[AnyOrLiteralStr, AnyOrLiteralStr]: ...
@overload
def normcase(self, path: PathLike[AnyStr]) -> AnyStr: ...
@overload
def normcase(self, path: AnyOrLiteralStr) -> AnyOrLiteralStr: ...
def isabs(self, path: StrOrBytesPath) -> bool: ...

class PurePathBase:
parser: ClassVar[types.ModuleType | ParserBase]
def __init__(self, path: StrPath, *paths: StrPath) -> None: ...
def with_segments(self, *pathsegments: StrPath) -> Self: ...
def as_posix(self) -> 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: ...
def with_name(self, name: str) -> Self: ...
def with_stem(self, stem: str) -> Self: ...
def with_suffix(self, suffix: str) -> Self: ...
def relative_to(self, other: StrPath, *, walk_up: bool = False) -> Self: ...
def is_relative_to(self, other: StrPath) -> bool: ...
@property
def parts(self) -> tuple[str, ...]: ...
def joinpath(self, *pathsegments: StrPath) -> Self: ...
def __truediv__(self, key: StrPath) -> Self: ...
def __rtruediv__(self, key: StrPath) -> Self: ...
@property
def parent(self) -> Self: ...
@property
def parents(self) -> Sequence[Self]: ...
def is_absolute(self) -> bool: ...
def match(self, path_pattern: str, *, case_sensitive: bool | None = None) -> bool: ...
def full_match(self, pattern: StrPath, *, case_sensitive: bool | None = None) -> bool: ...

class PathBase(PurePathBase):
def stat(self, *, follow_symlinks: bool = True) -> stat_result: ...
def lstat(self) -> stat_result: ...
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_mount(self) -> bool: ...
def is_symlink(self) -> bool: ...
def is_junction(self) -> bool: ...
def is_block_device(self) -> bool: ...
def is_char_device(self) -> bool: ...
def is_fifo(self) -> bool: ...
def is_socket(self) -> bool: ...
def samefile(self, other_path: StrPath) -> bool: ...

# Adapted from builtins.open
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we should require all implementations of this ABC to duplicate this stack of overloads. Not sure what the alternative would be, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hadn't considered that aspect when copying it over. I think the alternative is to define only the most generic form of open on the base class, and implementations can have more specific overloads as needed. I'll update the MR for that.

Copy link
Contributor Author

@tungol tungol Nov 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stubtest didn't like that... I thought it would work based on a simplified test I did locally, but possibly I misunderstood something. I'll need to look at it later, so I'll put this in draft for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the same concern possibly applies to the overloads on ParserBase. I copied the definitions in the MR right now for that class from the versions that exist in posixpath.pyi.

# Text mode: always returns a TextIOWrapper
# The Traversable .open in stdlib/importlib/abc.pyi should be kept in sync with this.
@overload
def open(
self,
mode: OpenTextMode = "r",
buffering: int = -1,
encoding: str | None = None,
errors: str | None = None,
newline: str | None = None,
) -> TextIOWrapper: ...
# Unbuffered binary mode: returns a FileIO
@overload
def open(
self, mode: OpenBinaryMode, buffering: Literal[0], encoding: None = None, errors: None = None, newline: None = None
) -> FileIO: ...
# Buffering is on: return BufferedRandom, BufferedReader, or BufferedWriter
@overload
def open(
self,
mode: OpenBinaryModeUpdating,
buffering: Literal[-1, 1] = -1,
encoding: None = None,
errors: None = None,
newline: None = None,
) -> BufferedRandom: ...
@overload
def open(
self,
mode: OpenBinaryModeWriting,
buffering: Literal[-1, 1] = -1,
encoding: None = None,
errors: None = None,
newline: None = None,
) -> BufferedWriter: ...
@overload
def open(
self,
mode: OpenBinaryModeReading,
buffering: Literal[-1, 1] = -1,
encoding: None = None,
errors: None = None,
newline: None = None,
) -> BufferedReader: ...
# Buffering cannot be determined: fall back to BinaryIO
@overload
def open(
self, mode: OpenBinaryMode, buffering: int = -1, encoding: None = None, errors: None = None, newline: None = None
) -> BinaryIO: ...
# Fallback if mode is not specified
@overload
def open(
self, mode: str, buffering: int = -1, encoding: str | None = None, errors: str | None = None, newline: str | None = None
) -> IO[Any]: ...
def read_bytes(self) -> bytes: ...
def read_text(self, encoding: str | None = None, errors: str | None = None, newline: str | None = None) -> str: ...
def write_bytes(self, data: ReadableBuffer) -> int: ...
def write_text(
self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None
) -> int: ...
def iterdir(self) -> Generator[Self, None, None]: ...
def glob(
self, pattern: str, *, case_sensitive: bool | None = None, recurse_symlinks: bool = True
) -> Generator[Self, None, None]: ...
def rglob(
self, pattern: str, *, case_sensitive: bool | None = None, recurse_symlinks: bool = True
) -> Generator[Self, None, None]: ...
def walk(
self, top_down: bool = True, on_error: Callable[[OSError], object] | None = None, follow_symlinks: bool = False
) -> Iterator[tuple[Self, list[str], list[str]]]: ...
def absolute(self) -> Self: ...
@classmethod
def cwd(cls) -> Self: ...
def expanduser(self) -> Self: ...
@classmethod
def home(cls) -> Self: ...
def readlink(self) -> Self: ...
def resolve(self, strict: bool = False) -> Self: ...
def symlink_to(self, target: StrOrBytesPath, target_is_directory: bool = False) -> None: ...
def hardlink_to(self, target: StrOrBytesPath) -> None: ...
def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: ...
def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None: ...
tungol marked this conversation as resolved.
Show resolved Hide resolved
def rename(self, target: StrPath) -> Self: ...
def replace(self, target: StrPath) -> Self: ...
def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: ...
def lchmod(self, mode: int) -> None: ...
def unlink(self, missing_ok: bool = False) -> None: ...
def rmdir(self) -> None: ...
def owner(self, *, follow_symlinks: bool = True) -> str: ...
def group(self, *, follow_symlinks: bool = True) -> str: ...
@classmethod
def from_uri(cls, uri: str) -> Self: ...
def as_uri(self) -> str: ...