diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index 38bc660e0aeb30..a0657dff4c24ee 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -13,7 +13,7 @@ import functools import operator -import posixpath +from abc import ABC, abstractmethod from errno import EINVAL from glob import _GlobberBase, _no_recurse_symlinks from pathlib._os import copyfileobj @@ -190,17 +190,24 @@ def _ensure_distinct_path(self, source): raise err -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 @@ -208,6 +215,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.""" @@ -378,20 +386,15 @@ def full_match(self, pattern, *, case_sensitive=None): class ReadablePath(JoinablePath): - """Base class for concrete 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(). + """Abstract base class for readable path objects. - 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__ = () + @abstractmethod def exists(self, *, follow_symlinks=True): """ Whether this path exists. @@ -401,12 +404,14 @@ def exists(self, *, follow_symlinks=True): """ raise NotImplementedError + @abstractmethod def is_dir(self, *, follow_symlinks=True): """ Whether this path is a directory. """ raise NotImplementedError + @abstractmethod def is_file(self, *, follow_symlinks=True): """ Whether this path is a regular file (also True for symlinks pointing @@ -414,12 +419,14 @@ def is_file(self, *, follow_symlinks=True): """ raise NotImplementedError + @abstractmethod def is_symlink(self): """ Whether this path is a symbolic link. """ raise NotImplementedError + @abstractmethod def open(self, mode='r', buffering=-1, encoding=None, errors=None, newline=None): """ @@ -451,6 +458,7 @@ def _scandir(self): import contextlib return contextlib.nullcontext(self.iterdir()) + @abstractmethod def iterdir(self): """Yield path objects of the directory contents. @@ -526,6 +534,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. @@ -552,8 +561,15 @@ def copy_into(self, target_dir, *, follow_symlinks=True, class WritablePath(ReadablePath): + """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. @@ -561,6 +577,7 @@ 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. diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index d6afb31424265c..c65a083d6786b3 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -20,7 +20,7 @@ grp = None from pathlib._os import copyfile -from pathlib._abc import CopyWriter, JoinablePath, WritablePath +from pathlib._abc import CopyWriter, JoinablePath, ReadablePath, WritablePath __all__ = [ @@ -190,7 +190,7 @@ def _ensure_different_file(self, source): raise err -class PurePath(JoinablePath): +class PurePath: """Base class for manipulating paths without I/O. PurePath represents a filesystem path and offers operations which @@ -534,6 +534,9 @@ def with_name(self, name): tail[-1] = name return self._from_parsed_parts(self.drive, self.root, tail) + with_stem = JoinablePath.with_stem + with_suffix = JoinablePath.with_suffix + @property def stem(self): """The final path component, minus its last suffix.""" @@ -641,6 +644,8 @@ def as_uri(self): from urllib.parse import quote_from_bytes return prefix + quote_from_bytes(os.fsencode(path)) + match = JoinablePath.match + def full_match(self, pattern, *, case_sensitive=None): """ Return True if this path matches the given glob-style pattern. The @@ -658,9 +663,10 @@ def full_match(self, pattern, *, case_sensitive=None): globber = _StringGlobber(self.parser.sep, case_sensitive, recursive=True) return globber.compile(pattern)(path) is not None -# Subclassing os.PathLike makes isinstance() checks slower, -# which in turn makes Path construction slower. Register instead! +# Subclassing abc.ABC makes isinstance() checks slower, +# which in turn makes path construction slower. Register instead! os.PathLike.register(PurePath) +JoinablePath.register(PurePath) class PurePosixPath(PurePath): @@ -683,7 +689,7 @@ class PureWindowsPath(PurePath): __slots__ = () -class Path(WritablePath, PurePath): +class Path(PurePath): """PurePath subclass that can make system calls. Path represents a filesystem path but unlike PurePath, also offers @@ -823,6 +829,8 @@ def open(self, mode='r', buffering=-1, encoding=None, encoding = io.text_encoding(encoding) return io.open(self, mode, buffering, encoding, errors, newline) + read_bytes = ReadablePath.read_bytes + def read_text(self, encoding=None, errors=None, newline=None): """ Open the file in text mode, read it, and close the file. @@ -830,7 +838,9 @@ def read_text(self, encoding=None, errors=None, newline=None): # Call io.text_encoding() here to ensure any warning is raised at an # appropriate stack level. encoding = io.text_encoding(encoding) - return super().read_text(encoding, errors, newline) + return ReadablePath.read_text(self, encoding, errors, newline) + + write_bytes = WritablePath.write_bytes def write_text(self, data, encoding=None, errors=None, newline=None): """ @@ -839,7 +849,7 @@ def write_text(self, data, encoding=None, errors=None, newline=None): # Call io.text_encoding() here to ensure any warning is raised at an # appropriate stack level. encoding = io.text_encoding(encoding) - return super().write_text(data, encoding, errors, newline) + return WritablePath.write_text(self, data, encoding, errors, newline) _remove_leading_dot = operator.itemgetter(slice(2, None)) _remove_trailing_slash = operator.itemgetter(slice(-1)) @@ -1124,6 +1134,8 @@ def replace(self, target): copy = property(_LocalCopyWriter, doc=_LocalCopyWriter.__call__.__doc__) + copy_into = ReadablePath.copy_into + def move(self, target): """ Recursively move this file or directory tree to the given destination. @@ -1242,6 +1254,11 @@ def from_uri(cls, uri): raise ValueError(f"URI is not absolute: {uri!r}") return path +# Subclassing abc.ABC makes isinstance() checks slower, +# which in turn makes path construction slower. Register instead! +ReadablePath.register(Path) +WritablePath.register(Path) + class PosixPath(Path, PurePosixPath): """Path subclass for non-Windows systems. diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index ad5a9f9c8de9d6..f308684b339f32 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -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. @@ -924,7 +924,7 @@ class cls(pathlib.PurePath): # Tests for the concrete classes. # -class PathTest(test_pathlib_abc.DummyWritablePathTest, PurePathTest): +class PathTest(test_pathlib_abc.WritablePathTest, PurePathTest): """Tests for the FS-accessing functionalities of the Path classes.""" cls = pathlib.Path can_symlink = os_helper.can_symlink() @@ -3019,7 +3019,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 diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index 6ba012e0208a53..ddda0d02450612 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -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, Parser) @@ -940,6 +926,7 @@ class DummyReadablePath(ReadablePath): _files = {} _directories = {} + parser = posixpath def __init__(self, *segments): self._segments = segments @@ -1012,6 +999,9 @@ def iterdir(self): else: raise FileNotFoundError(errno.ENOENT, "File not found", path) + def readlink(self): + raise NotImplementedError + class DummyWritablePath(DummyReadablePath, WritablePath): __slots__ = () @@ -1034,8 +1024,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 @@ -1102,6 +1095,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) @@ -1359,9 +1356,13 @@ def test_is_symlink(self): self.assertIs((P / 'linkA\x00').is_file(), False) -class DummyWritablePathTest(DummyReadablePathTest): +class WritablePathTest(ReadablePathTest): cls = DummyWritablePath + def test_is_writable(self): + p = self.cls(self.base) + self.assertIsInstance(p, WritablePath) + def test_read_write_bytes(self): p = self.cls(self.base) (p / 'fileA').write_bytes(b'abcdefg') @@ -1570,9 +1571,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):