Skip to content

GH-130798: Add type hints to pathlib.types #131639

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

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Changes from 10 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
101 changes: 62 additions & 39 deletions Lib/pathlib/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,25 @@


from abc import ABC, abstractmethod
from collections.abc import Callable, Iterator, Sequence
from glob import _PathGlobber
from pathlib._os import magic_open, ensure_distinct_paths, ensure_different_files, copyfileobj
from pathlib import PurePath, Path
from typing import Optional, Protocol, runtime_checkable
from typing import (
Any, BinaryIO, Literal, Optional, Protocol, Self, TypeVar,
runtime_checkable,
)

_WritablePathT = TypeVar("_WritablePathT", bound="_WritablePath")

def _explode_path(path, split):

def _explode_path(path: str, parser: "_PathParser") -> tuple[str, list[str]]:
"""
Split the path into a 2-tuple (anchor, parts), where *anchor* is the
uppermost parent of the path (equivalent to path.parents[-1]), and
*parts* is a reversed list of parts following the anchor.
"""
split = parser.split
parent, name = split(path)
names = []
while path != parent:
Expand Down Expand Up @@ -70,38 +77,38 @@ class _JoinablePath(ABC):

@property
@abstractmethod
def parser(self):
def parser(self) -> _PathParser:
"""Implementation of pathlib._types.Parser used for low-level path
parsing and manipulation.
"""
raise NotImplementedError

@abstractmethod
def with_segments(self, *pathsegments):
def with_segments(self, *pathsegments: str) -> Self:
"""Construct a new path object from any number of path-like objects.
Subclasses may override this method to customize how new path objects
are created from methods like `iterdir()`.
"""
raise NotImplementedError

@abstractmethod
def __str__(self):
def __str__(self) -> str:
"""Return the string representation of the path, suitable for
passing to system calls."""
raise NotImplementedError

@property
def anchor(self):
def anchor(self) -> str:
"""The concatenation of the drive and root, or ''."""
return _explode_path(str(self), self.parser.split)[0]
return _explode_path(str(self), self.parser)[0]

@property
def name(self):
def name(self) -> str:
"""The final path component, if any."""
return self.parser.split(str(self))[1]

@property
def suffix(self):
def suffix(self) -> str:
"""
The final component's last suffix, if any.

Expand All @@ -110,7 +117,7 @@ def suffix(self):
return self.parser.splitext(self.name)[1]

@property
def suffixes(self):
def suffixes(self) -> Sequence[str]:
"""
A list of the final component's suffixes, if any.

Expand All @@ -125,11 +132,11 @@ def suffixes(self):
return suffixes[::-1]

@property
def stem(self):
def stem(self) -> str:
"""The final path component, minus its last suffix."""
return self.parser.splitext(self.name)[0]

def with_name(self, name):
def with_name(self, name: str) -> Self:
"""Return a new path with the file name changed."""
split = self.parser.split
if split(name)[0]:
Expand All @@ -138,7 +145,7 @@ def with_name(self, name):
path = path.removesuffix(split(path)[1]) + name
return self.with_segments(path)

def with_stem(self, stem):
def with_stem(self, stem: str) -> Self:
"""Return a new path with the stem changed."""
suffix = self.suffix
if not suffix:
Expand All @@ -149,7 +156,7 @@ def with_stem(self, stem):
else:
return self.with_name(stem + suffix)

def with_suffix(self, suffix):
def with_suffix(self, suffix: str) -> Self:
"""Return a new path with the file suffix changed. If the path
has no suffix, add given suffix. If the given suffix is an empty
string, remove the suffix from the path.
Expand All @@ -164,36 +171,36 @@ def with_suffix(self, suffix):
return self.with_name(stem + suffix)

@property
def parts(self):
def parts(self) -> Sequence[str]:
"""An object providing sequence-like access to the
components in the filesystem path."""
anchor, parts = _explode_path(str(self), self.parser.split)
anchor, parts = _explode_path(str(self), self.parser)
if anchor:
parts.append(anchor)
return tuple(reversed(parts))

def joinpath(self, *pathsegments):
def joinpath(self, *pathsegments: str) -> Self:
"""Combine this path with one or several arguments, and return a
new path representing either a subpath (if all arguments are relative
paths) or a totally different path (if one of the arguments is
anchored).
"""
return self.with_segments(str(self), *pathsegments)

def __truediv__(self, key):
def __truediv__(self, key: str) -> Self:
try:
return self.with_segments(str(self), key)
except TypeError:
return NotImplemented

def __rtruediv__(self, key):
def __rtruediv__(self, key: str) -> Self:
try:
return self.with_segments(key, str(self))
except TypeError:
return NotImplemented

@property
def parent(self):
def parent(self) -> Self:
"""The logical parent of the path."""
path = str(self)
parent = self.parser.split(path)[0]
Expand All @@ -202,7 +209,7 @@ def parent(self):
return self

@property
def parents(self):
def parents(self) -> Sequence[Self]:
"""A sequence of this path's logical parents."""
split = self.parser.split
path = str(self)
Expand All @@ -214,7 +221,7 @@ def parents(self):
parent = split(path)[0]
return tuple(parents)

def full_match(self, pattern):
def full_match(self, pattern: str) -> bool:
"""
Return True if this path matches the given glob-style pattern. The
pattern is matched against the entire path.
Expand All @@ -236,49 +243,54 @@ class _ReadablePath(_JoinablePath):

@property
@abstractmethod
def info(self):
def info(self) -> PathInfo:
"""
A PathInfo object that exposes the file type and other file attributes
of this path.
"""
raise NotImplementedError

@abstractmethod
def __open_rb__(self, buffering=-1):
def __open_rb__(self, buffering: int = -1) -> BinaryIO:
"""
Open the file pointed to by this path for reading in binary mode and
return a file object, like open(mode='rb').
"""
raise NotImplementedError

def read_bytes(self):
def read_bytes(self) -> bytes:
"""
Open the file in bytes mode, read it, and close the file.
"""
with magic_open(self, mode='rb', buffering=0) as f:
return f.read()

def read_text(self, encoding=None, errors=None, newline=None):
def read_text(
self,
encoding: Optional[str] = None,
errors: Optional[str] = None,
newline: Optional[str] = None,
) -> str:
"""
Open the file in text mode, read it, and close the file.
"""
with magic_open(self, mode='r', encoding=encoding, errors=errors, newline=newline) as f:
return f.read()

@abstractmethod
def iterdir(self):
def iterdir(self) -> Iterator[Self]:
"""Yield path objects of the directory contents.

The children are yielded in arbitrary order, and the
special entries '.' and '..' are not included.
"""
raise NotImplementedError

def glob(self, pattern, *, recurse_symlinks=True):
def glob(self, pattern: str, *, recurse_symlinks: Literal[True] = True) -> Iterator[Self]:
"""Iterate over this subtree and yield all existing files (of any
kind, including directories) matching the given relative pattern.
"""
anchor, parts = _explode_path(pattern, self.parser.split)
anchor, parts = _explode_path(pattern, self.parser)
if anchor:
raise NotImplementedError("Non-relative patterns are unsupported")
elif not parts:
Expand All @@ -290,7 +302,12 @@ def glob(self, pattern, *, recurse_symlinks=True):
select = globber.selector(parts)
return select(self.joinpath(''))

def walk(self, top_down=True, on_error=None, follow_symlinks=False):
def walk(
self,
top_down: bool = True,
on_error: Optional[Callable[[Exception], None]] = None,
follow_symlinks: bool = False,
) -> Iterator[tuple[Self, list[str], list[str]]]:
"""Walk the directory tree from this directory, similar to os.walk()."""
paths = [self]
while paths:
Expand Down Expand Up @@ -322,21 +339,21 @@ def walk(self, top_down=True, on_error=None, follow_symlinks=False):
paths += [path.joinpath(d) for d in reversed(dirnames)]

@abstractmethod
def readlink(self):
def readlink(self) -> Self:
"""
Return the path to which the symbolic link points.
"""
raise NotImplementedError

def copy(self, target, **kwargs):
def copy(self, target: _WritablePathT, **kwargs: Any) -> _WritablePathT:
"""
Recursively copy this file or directory tree to the given destination.
"""
ensure_distinct_paths(self, target)
target._copy_from(self, **kwargs)
return target.joinpath() # Empty join to ensure fresh metadata.

def copy_into(self, target_dir, **kwargs):
def copy_into(self, target_dir: _WritablePathT, **kwargs: Any) -> _WritablePathT:
"""
Copy this file or directory tree into the given existing directory.
"""
Expand All @@ -356,29 +373,29 @@ class _WritablePath(_JoinablePath):
__slots__ = ()

@abstractmethod
def symlink_to(self, target, target_is_directory=False):
def symlink_to(self, target: str, target_is_directory: bool = False) -> None:
"""
Make this path a symlink pointing to the target path.
Note the order of arguments (link, target) is the reverse of os.symlink.
"""
raise NotImplementedError

@abstractmethod
def mkdir(self):
def mkdir(self) -> None:
"""
Create a new directory at this given path.
"""
raise NotImplementedError

@abstractmethod
def __open_wb__(self, buffering=-1):
def __open_wb__(self, buffering: int = -1) -> BinaryIO:
"""
Open the file pointed to by this path for writing in binary mode and
return a file object, like open(mode='wb').
"""
raise NotImplementedError

def write_bytes(self, data):
def write_bytes(self, data: bytes) -> int:
"""
Open the file in bytes mode, write to it, and close the file.
"""
Expand All @@ -387,7 +404,13 @@ def write_bytes(self, data):
with magic_open(self, mode='wb') as f:
return f.write(view)

def write_text(self, data, encoding=None, errors=None, newline=None):
def write_text(
self,
data: str,
encoding: Optional[str] = None,
errors: Optional[str] = None,
newline: Optional[str] = None,
) -> int:
"""
Open the file in text mode, write to it, and close the file.
"""
Expand All @@ -397,7 +420,7 @@ def write_text(self, data, encoding=None, errors=None, newline=None):
with magic_open(self, mode='w', encoding=encoding, errors=errors, newline=newline) as f:
return f.write(data)

def _copy_from(self, source, follow_symlinks=True):
def _copy_from(self, source: _ReadablePath, follow_symlinks: bool = True) -> None:
"""
Recursively copy the given path to this path.
"""
Expand Down
Loading