mirror of
https://github.com/python/cpython.git
synced 2025-11-01 14:11:41 +00:00
GH-128520: Subclass abc.ABC in pathlib._abc (#128745)
Convert `JoinablePath`, `ReadablePath` and `WritablePath` to real ABCs derived from `abc.ABC`. Make `JoinablePath.parser` abstract, rather than defaulting to `posixpath`. Register `PurePath` and `Path` as virtual subclasses of the ABCs rather than deriving. This avoids a hit to path object instantiation performance. No change of behaviour in the public (non-abstract) classes.
This commit is contained in:
parent
359c7dde3b
commit
a7d41a8947
4 changed files with 125 additions and 49 deletions
|
|
@ -12,8 +12,9 @@
|
|||
"""
|
||||
|
||||
import functools
|
||||
import posixpath
|
||||
from abc import ABC, abstractmethod
|
||||
from glob import _PathGlobber, _no_recurse_symlinks
|
||||
from pathlib import PurePath, Path
|
||||
from pathlib._os import magic_open, CopyReader, CopyWriter
|
||||
|
||||
|
||||
|
|
@ -39,17 +40,24 @@ def _explode_path(path):
|
|||
return path, names
|
||||
|
||||
|
||||
class JoinablePath:
|
||||
"""Base class for pure path objects.
|
||||
class JoinablePath(ABC):
|
||||
"""Abstract base class for pure path objects.
|
||||
|
||||
This class *does not* provide several magic methods that are defined in
|
||||
its subclass PurePath. They are: __init__, __fspath__, __bytes__,
|
||||
its implementation PurePath. They are: __init__, __fspath__, __bytes__,
|
||||
__reduce__, __hash__, __eq__, __lt__, __le__, __gt__, __ge__.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
parser = posixpath
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def parser(self):
|
||||
"""Implementation of pathlib._types.Parser used for low-level path
|
||||
parsing and manipulation.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def with_segments(self, *pathsegments):
|
||||
"""Construct a new path object from any number of path-like objects.
|
||||
Subclasses may override this method to customize how new path objects
|
||||
|
|
@ -57,6 +65,7 @@ def with_segments(self, *pathsegments):
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
"""Return the string representation of the path, suitable for
|
||||
passing to system calls."""
|
||||
|
|
@ -198,23 +207,17 @@ def full_match(self, pattern, *, case_sensitive=None):
|
|||
return match(str(self)) is not None
|
||||
|
||||
|
||||
|
||||
class ReadablePath(JoinablePath):
|
||||
"""Base class for concrete path objects.
|
||||
"""Abstract base class for readable path objects.
|
||||
|
||||
This class provides dummy implementations for many methods that derived
|
||||
classes can override selectively; the default implementations raise
|
||||
NotImplementedError. The most basic methods, such as stat() and open(),
|
||||
directly raise NotImplementedError; these basic methods are called by
|
||||
other methods such as is_dir() and read_text().
|
||||
|
||||
The Path class derives this class to implement local filesystem paths.
|
||||
Users may derive their own classes to implement virtual filesystem paths,
|
||||
such as paths in archive files or on remote storage systems.
|
||||
The Path class implements this ABC for local filesystem paths. Users may
|
||||
create subclasses to implement readable virtual filesystem paths, such as
|
||||
paths in archive files or on remote storage systems.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def info(self):
|
||||
"""
|
||||
A PathInfo object that exposes the file type and other file attributes
|
||||
|
|
@ -254,6 +257,7 @@ def is_symlink(self):
|
|||
info = self.joinpath().info
|
||||
return info.is_symlink()
|
||||
|
||||
@abstractmethod
|
||||
def __open_rb__(self, buffering=-1):
|
||||
"""
|
||||
Open the file pointed to by this path for reading in binary mode and
|
||||
|
|
@ -275,6 +279,7 @@ def read_text(self, encoding=None, errors=None, newline=None):
|
|||
with magic_open(self, mode='r', encoding=encoding, errors=errors, newline=newline) as f:
|
||||
return f.read()
|
||||
|
||||
@abstractmethod
|
||||
def iterdir(self):
|
||||
"""Yield path objects of the directory contents.
|
||||
|
||||
|
|
@ -348,6 +353,7 @@ def walk(self, top_down=True, on_error=None, follow_symlinks=False):
|
|||
yield path, dirnames, filenames
|
||||
paths += [path.joinpath(d) for d in reversed(dirnames)]
|
||||
|
||||
@abstractmethod
|
||||
def readlink(self):
|
||||
"""
|
||||
Return the path to which the symbolic link points.
|
||||
|
|
@ -389,8 +395,15 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
|
|||
|
||||
|
||||
class WritablePath(JoinablePath):
|
||||
"""Abstract base class for writable path objects.
|
||||
|
||||
The Path class implements this ABC for local filesystem paths. Users may
|
||||
create subclasses to implement writable virtual filesystem paths, such as
|
||||
paths in archive files or on remote storage systems.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
def symlink_to(self, target, target_is_directory=False):
|
||||
"""
|
||||
Make this path a symlink pointing to the target path.
|
||||
|
|
@ -398,12 +411,14 @@ def symlink_to(self, target, target_is_directory=False):
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
|
||||
"""
|
||||
Create a new directory at this given path.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def __open_wb__(self, buffering=-1):
|
||||
"""
|
||||
Open the file pointed to by this path for writing in binary mode and
|
||||
|
|
@ -431,3 +446,8 @@ def write_text(self, data, encoding=None, errors=None, newline=None):
|
|||
return f.write(data)
|
||||
|
||||
_copy_writer = property(CopyWriter)
|
||||
|
||||
|
||||
JoinablePath.register(PurePath)
|
||||
ReadablePath.register(Path)
|
||||
WritablePath.register(Path)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
grp = None
|
||||
|
||||
from pathlib._os import LocalCopyReader, LocalCopyWriter, PathInfo, DirEntryInfo
|
||||
from pathlib._abc import JoinablePath, ReadablePath, WritablePath
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -65,7 +64,7 @@ def __repr__(self):
|
|||
return "<{}.parents>".format(type(self._path).__name__)
|
||||
|
||||
|
||||
class PurePath(JoinablePath):
|
||||
class PurePath:
|
||||
"""Base class for manipulating paths without I/O.
|
||||
|
||||
PurePath represents a filesystem path and offers operations which
|
||||
|
|
@ -409,6 +408,31 @@ def with_name(self, name):
|
|||
tail[-1] = name
|
||||
return self._from_parsed_parts(self.drive, self.root, tail)
|
||||
|
||||
def with_stem(self, stem):
|
||||
"""Return a new path with the stem changed."""
|
||||
suffix = self.suffix
|
||||
if not suffix:
|
||||
return self.with_name(stem)
|
||||
elif not stem:
|
||||
# If the suffix is non-empty, we can't make the stem empty.
|
||||
raise ValueError(f"{self!r} has a non-empty suffix")
|
||||
else:
|
||||
return self.with_name(stem + suffix)
|
||||
|
||||
def with_suffix(self, suffix):
|
||||
"""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.
|
||||
"""
|
||||
stem = self.stem
|
||||
if not stem:
|
||||
# If the stem is empty, we can't make the suffix non-empty.
|
||||
raise ValueError(f"{self!r} has an empty name")
|
||||
elif suffix and not suffix.startswith('.'):
|
||||
raise ValueError(f"Invalid suffix {suffix!r}")
|
||||
else:
|
||||
return self.with_name(stem + suffix)
|
||||
|
||||
@property
|
||||
def stem(self):
|
||||
"""The final path component, minus its last suffix."""
|
||||
|
|
@ -584,7 +608,7 @@ class PureWindowsPath(PurePath):
|
|||
__slots__ = ()
|
||||
|
||||
|
||||
class Path(WritablePath, ReadablePath, PurePath):
|
||||
class Path(PurePath):
|
||||
"""PurePath subclass that can make system calls.
|
||||
|
||||
Path represents a filesystem path but unlike PurePath, also offers
|
||||
|
|
@ -1058,6 +1082,37 @@ def replace(self, target):
|
|||
_copy_reader = property(LocalCopyReader)
|
||||
_copy_writer = property(LocalCopyWriter)
|
||||
|
||||
def copy(self, target, follow_symlinks=True, dirs_exist_ok=False,
|
||||
preserve_metadata=False):
|
||||
"""
|
||||
Recursively copy this file or directory tree to the given destination.
|
||||
"""
|
||||
if not hasattr(target, '_copy_writer'):
|
||||
target = self.with_segments(target)
|
||||
|
||||
# Delegate to the target path's CopyWriter object.
|
||||
try:
|
||||
create = target._copy_writer._create
|
||||
except AttributeError:
|
||||
raise TypeError(f"Target is not writable: {target}") from None
|
||||
return create(self, follow_symlinks, dirs_exist_ok, preserve_metadata)
|
||||
|
||||
def copy_into(self, target_dir, *, follow_symlinks=True,
|
||||
dirs_exist_ok=False, preserve_metadata=False):
|
||||
"""
|
||||
Copy this file or directory tree into the given existing directory.
|
||||
"""
|
||||
name = self.name
|
||||
if not name:
|
||||
raise ValueError(f"{self!r} has an empty name")
|
||||
elif hasattr(target_dir, '_copy_writer'):
|
||||
target = target_dir / name
|
||||
else:
|
||||
target = self.with_segments(target_dir, name)
|
||||
return self.copy(target, follow_symlinks=follow_symlinks,
|
||||
dirs_exist_ok=dirs_exist_ok,
|
||||
preserve_metadata=preserve_metadata)
|
||||
|
||||
def move(self, target):
|
||||
"""
|
||||
Recursively move this file or directory tree to the given destination.
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ def test_is_notimplemented(self):
|
|||
# Tests for the pure classes.
|
||||
#
|
||||
|
||||
class PurePathTest(test_pathlib_abc.DummyJoinablePathTest):
|
||||
class PurePathTest(test_pathlib_abc.JoinablePathTest):
|
||||
cls = pathlib.PurePath
|
||||
|
||||
# Make sure any symbolic links in the base test path are resolved.
|
||||
|
|
@ -1002,7 +1002,7 @@ class cls(pathlib.PurePath):
|
|||
# Tests for the concrete classes.
|
||||
#
|
||||
|
||||
class PathTest(test_pathlib_abc.DummyRWPathTest, PurePathTest):
|
||||
class PathTest(test_pathlib_abc.RWPathTest, PurePathTest):
|
||||
"""Tests for the FS-accessing functionalities of the Path classes."""
|
||||
cls = pathlib.Path
|
||||
can_symlink = os_helper.can_symlink()
|
||||
|
|
@ -3119,7 +3119,7 @@ def test_group_windows(self):
|
|||
P('c:/').group()
|
||||
|
||||
|
||||
class PathWalkTest(test_pathlib_abc.DummyReadablePathWalkTest):
|
||||
class PathWalkTest(test_pathlib_abc.ReadablePathWalkTest):
|
||||
cls = pathlib.Path
|
||||
base = PathTest.base
|
||||
can_symlink = PathTest.can_symlink
|
||||
|
|
|
|||
|
|
@ -31,29 +31,11 @@ def needs_windows(fn):
|
|||
#
|
||||
|
||||
|
||||
class JoinablePathTest(unittest.TestCase):
|
||||
cls = JoinablePath
|
||||
|
||||
def test_magic_methods(self):
|
||||
P = self.cls
|
||||
self.assertFalse(hasattr(P, '__fspath__'))
|
||||
self.assertFalse(hasattr(P, '__bytes__'))
|
||||
self.assertIs(P.__reduce__, object.__reduce__)
|
||||
self.assertIs(P.__repr__, object.__repr__)
|
||||
self.assertIs(P.__hash__, object.__hash__)
|
||||
self.assertIs(P.__eq__, object.__eq__)
|
||||
self.assertIs(P.__lt__, object.__lt__)
|
||||
self.assertIs(P.__le__, object.__le__)
|
||||
self.assertIs(P.__gt__, object.__gt__)
|
||||
self.assertIs(P.__ge__, object.__ge__)
|
||||
|
||||
def test_parser(self):
|
||||
self.assertIs(self.cls.parser, posixpath)
|
||||
|
||||
|
||||
class DummyJoinablePath(JoinablePath):
|
||||
__slots__ = ('_segments',)
|
||||
|
||||
parser = posixpath
|
||||
|
||||
def __init__(self, *segments):
|
||||
self._segments = segments
|
||||
|
||||
|
|
@ -77,7 +59,7 @@ def with_segments(self, *pathsegments):
|
|||
return type(self)(*pathsegments)
|
||||
|
||||
|
||||
class DummyJoinablePathTest(unittest.TestCase):
|
||||
class JoinablePathTest(unittest.TestCase):
|
||||
cls = DummyJoinablePath
|
||||
|
||||
# Use a base path that's unrelated to any real filesystem path.
|
||||
|
|
@ -94,6 +76,10 @@ def setUp(self):
|
|||
self.sep = self.parser.sep
|
||||
self.altsep = self.parser.altsep
|
||||
|
||||
def test_is_joinable(self):
|
||||
p = self.cls(self.base)
|
||||
self.assertIsInstance(p, JoinablePath)
|
||||
|
||||
def test_parser(self):
|
||||
self.assertIsInstance(self.cls.parser, _PathParser)
|
||||
|
||||
|
|
@ -878,6 +864,7 @@ class DummyReadablePath(ReadablePath, DummyJoinablePath):
|
|||
|
||||
_files = {}
|
||||
_directories = {}
|
||||
parser = posixpath
|
||||
|
||||
def __init__(self, *segments):
|
||||
super().__init__(*segments)
|
||||
|
|
@ -909,6 +896,9 @@ def iterdir(self):
|
|||
else:
|
||||
raise FileNotFoundError(errno.ENOENT, "File not found", path)
|
||||
|
||||
def readlink(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DummyWritablePath(WritablePath, DummyJoinablePath):
|
||||
__slots__ = ()
|
||||
|
|
@ -942,8 +932,11 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
|
|||
self.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.mkdir(mode, parents=False, exist_ok=exist_ok)
|
||||
|
||||
def symlink_to(self, target, target_is_directory=False):
|
||||
raise NotImplementedError
|
||||
|
||||
class DummyReadablePathTest(DummyJoinablePathTest):
|
||||
|
||||
class ReadablePathTest(JoinablePathTest):
|
||||
"""Tests for ReadablePathTest methods that use stat(), open() and iterdir()."""
|
||||
|
||||
cls = DummyReadablePath
|
||||
|
|
@ -1010,6 +1003,10 @@ def assertEqualNormCase(self, path_a, path_b):
|
|||
normcase = self.parser.normcase
|
||||
self.assertEqual(normcase(path_a), normcase(path_b))
|
||||
|
||||
def test_is_readable(self):
|
||||
p = self.cls(self.base)
|
||||
self.assertIsInstance(p, ReadablePath)
|
||||
|
||||
def test_exists(self):
|
||||
P = self.cls
|
||||
p = P(self.base)
|
||||
|
|
@ -1378,15 +1375,19 @@ def test_is_symlink(self):
|
|||
self.assertIs((P / 'linkA\x00').is_file(), False)
|
||||
|
||||
|
||||
class DummyWritablePathTest(DummyJoinablePathTest):
|
||||
class WritablePathTest(JoinablePathTest):
|
||||
cls = DummyWritablePath
|
||||
|
||||
def test_is_writable(self):
|
||||
p = self.cls(self.base)
|
||||
self.assertIsInstance(p, WritablePath)
|
||||
|
||||
|
||||
class DummyRWPath(DummyWritablePath, DummyReadablePath):
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class DummyRWPathTest(DummyWritablePathTest, DummyReadablePathTest):
|
||||
class RWPathTest(WritablePathTest, ReadablePathTest):
|
||||
cls = DummyRWPath
|
||||
can_symlink = False
|
||||
|
||||
|
|
@ -1598,9 +1599,9 @@ def test_copy_into_empty_name(self):
|
|||
self.assertRaises(ValueError, source.copy_into, target_dir)
|
||||
|
||||
|
||||
class DummyReadablePathWalkTest(unittest.TestCase):
|
||||
class ReadablePathWalkTest(unittest.TestCase):
|
||||
cls = DummyReadablePath
|
||||
base = DummyReadablePathTest.base
|
||||
base = ReadablePathTest.base
|
||||
can_symlink = False
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue