Skip to content

Commit a7d41a8

Browse files
authored
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.
1 parent 359c7dd commit a7d41a8

File tree

4 files changed

+125
-49
lines changed

4 files changed

+125
-49
lines changed

‎Lib/pathlib/_abc.py‎

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
"""
1313

1414
importfunctools
15-
importposixpath
15+
fromabcimportABC, abstractmethod
1616
fromglobimport_PathGlobber, _no_recurse_symlinks
17+
frompathlibimportPurePath, Path
1718
frompathlib._osimportmagic_open, CopyReader, CopyWriter
1819

1920

@@ -39,24 +40,32 @@ def _explode_path(path):
3940
returnpath, names
4041

4142

42-
classJoinablePath:
43-
"""Base class for pure path objects.
43+
classJoinablePath(ABC):
44+
"""Abstract base class for pure path objects.
4445
4546
This class *does not* provide several magic methods that are defined in
46-
its subclass PurePath. They are: __init__, __fspath__, __bytes__,
47+
its implementation PurePath. They are: __init__, __fspath__, __bytes__,
4748
__reduce__, __hash__, __eq__, __lt__, __le__, __gt__, __ge__.
4849
"""
49-
5050
__slots__= ()
51-
parser=posixpath
5251

52+
@property
53+
@abstractmethod
54+
defparser(self):
55+
"""Implementation of pathlib._types.Parser used for low-level path
56+
parsing and manipulation.
57+
"""
58+
raiseNotImplementedError
59+
60+
@abstractmethod
5361
defwith_segments(self, *pathsegments):
5462
"""Construct a new path object from any number of path-like objects.
5563
Subclasses may override this method to customize how new path objects
5664
are created from methods like `iterdir()`.
5765
"""
5866
raiseNotImplementedError
5967

68+
@abstractmethod
6069
def__str__(self):
6170
"""Return the string representation of the path, suitable for
6271
passing to system calls."""
@@ -198,23 +207,17 @@ def full_match(self, pattern, *, case_sensitive=None):
198207
returnmatch(str(self)) isnotNone
199208

200209

201-
202210
classReadablePath(JoinablePath):
203-
"""Base class for concrete path objects.
211+
"""Abstract base class for readable path objects.
204212
205-
This class provides dummy implementations for many methods that derived
206-
classes can override selectively; the default implementations raise
207-
NotImplementedError. The most basic methods, such as stat() and open(),
208-
directly raise NotImplementedError; these basic methods are called by
209-
other methods such as is_dir() and read_text().
210-
211-
The Path class derives this class to implement local filesystem paths.
212-
Users may derive their own classes to implement virtual filesystem paths,
213-
such as paths in archive files or on remote storage systems.
213+
The Path class implements this ABC for local filesystem paths. Users may
214+
create subclasses to implement readable virtual filesystem paths, such as
215+
paths in archive files or on remote storage systems.
214216
"""
215217
__slots__= ()
216218

217219
@property
220+
@abstractmethod
218221
definfo(self):
219222
"""
220223
A PathInfo object that exposes the file type and other file attributes
@@ -254,6 +257,7 @@ def is_symlink(self):
254257
info=self.joinpath().info
255258
returninfo.is_symlink()
256259

260+
@abstractmethod
257261
def__open_rb__(self, buffering=-1):
258262
"""
259263
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):
275279
withmagic_open(self, mode='r', encoding=encoding, errors=errors, newline=newline) asf:
276280
returnf.read()
277281

282+
@abstractmethod
278283
defiterdir(self):
279284
"""Yield path objects of the directory contents.
280285
@@ -348,6 +353,7 @@ def walk(self, top_down=True, on_error=None, follow_symlinks=False):
348353
yieldpath, dirnames, filenames
349354
paths+= [path.joinpath(d) fordinreversed(dirnames)]
350355

356+
@abstractmethod
351357
defreadlink(self):
352358
"""
353359
Return the path to which the symbolic link points.
@@ -389,21 +395,30 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
389395

390396

391397
classWritablePath(JoinablePath):
398+
"""Abstract base class for writable path objects.
399+
400+
The Path class implements this ABC for local filesystem paths. Users may
401+
create subclasses to implement writable virtual filesystem paths, such as
402+
paths in archive files or on remote storage systems.
403+
"""
392404
__slots__= ()
393405

406+
@abstractmethod
394407
defsymlink_to(self, target, target_is_directory=False):
395408
"""
396409
Make this path a symlink pointing to the target path.
397410
Note the order of arguments (link, target) is the reverse of os.symlink.
398411
"""
399412
raiseNotImplementedError
400413

414+
@abstractmethod
401415
defmkdir(self, mode=0o777, parents=False, exist_ok=False):
402416
"""
403417
Create a new directory at this given path.
404418
"""
405419
raiseNotImplementedError
406420

421+
@abstractmethod
407422
def__open_wb__(self, buffering=-1):
408423
"""
409424
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):
431446
returnf.write(data)
432447

433448
_copy_writer=property(CopyWriter)
449+
450+
451+
JoinablePath.register(PurePath)
452+
ReadablePath.register(Path)
453+
WritablePath.register(Path)

‎Lib/pathlib/_local.py‎

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
grp=None
2121

2222
frompathlib._osimportLocalCopyReader, LocalCopyWriter, PathInfo, DirEntryInfo
23-
frompathlib._abcimportJoinablePath, ReadablePath, WritablePath
2423

2524

2625
__all__= [
@@ -65,7 +64,7 @@ def __repr__(self):
6564
return"<{}.parents>".format(type(self._path).__name__)
6665

6766

68-
classPurePath(JoinablePath):
67+
classPurePath:
6968
"""Base class for manipulating paths without I/O.
7069
7170
PurePath represents a filesystem path and offers operations which
@@ -409,6 +408,31 @@ def with_name(self, name):
409408
tail[-1] =name
410409
returnself._from_parsed_parts(self.drive, self.root, tail)
411410

411+
defwith_stem(self, stem):
412+
"""Return a new path with the stem changed."""
413+
suffix=self.suffix
414+
ifnotsuffix:
415+
returnself.with_name(stem)
416+
elifnotstem:
417+
# If the suffix is non-empty, we can't make the stem empty.
418+
raiseValueError(f"{self!r} has a non-empty suffix")
419+
else:
420+
returnself.with_name(stem+suffix)
421+
422+
defwith_suffix(self, suffix):
423+
"""Return a new path with the file suffix changed. If the path
424+
has no suffix, add given suffix. If the given suffix is an empty
425+
string, remove the suffix from the path.
426+
"""
427+
stem=self.stem
428+
ifnotstem:
429+
# If the stem is empty, we can't make the suffix non-empty.
430+
raiseValueError(f"{self!r} has an empty name")
431+
elifsuffixandnotsuffix.startswith('.'):
432+
raiseValueError(f"Invalid suffix {suffix!r}")
433+
else:
434+
returnself.with_name(stem+suffix)
435+
412436
@property
413437
defstem(self):
414438
"""The final path component, minus its last suffix."""
@@ -584,7 +608,7 @@ class PureWindowsPath(PurePath):
584608
__slots__= ()
585609

586610

587-
classPath(WritablePath, ReadablePath, PurePath):
611+
classPath(PurePath):
588612
"""PurePath subclass that can make system calls.
589613
590614
Path represents a filesystem path but unlike PurePath, also offers
@@ -1058,6 +1082,37 @@ def replace(self, target):
10581082
_copy_reader=property(LocalCopyReader)
10591083
_copy_writer=property(LocalCopyWriter)
10601084

1085+
defcopy(self, target, follow_symlinks=True, dirs_exist_ok=False,
1086+
preserve_metadata=False):
1087+
"""
1088+
Recursively copy this file or directory tree to the given destination.
1089+
"""
1090+
ifnothasattr(target, '_copy_writer'):
1091+
target=self.with_segments(target)
1092+
1093+
# Delegate to the target path's CopyWriter object.
1094+
try:
1095+
create=target._copy_writer._create
1096+
exceptAttributeError:
1097+
raiseTypeError(f"Target is not writable: {target}") fromNone
1098+
returncreate(self, follow_symlinks, dirs_exist_ok, preserve_metadata)
1099+
1100+
defcopy_into(self, target_dir, *, follow_symlinks=True,
1101+
dirs_exist_ok=False, preserve_metadata=False):
1102+
"""
1103+
Copy this file or directory tree into the given existing directory.
1104+
"""
1105+
name=self.name
1106+
ifnotname:
1107+
raiseValueError(f"{self!r} has an empty name")
1108+
elifhasattr(target_dir, '_copy_writer'):
1109+
target=target_dir/name
1110+
else:
1111+
target=self.with_segments(target_dir, name)
1112+
returnself.copy(target, follow_symlinks=follow_symlinks,
1113+
dirs_exist_ok=dirs_exist_ok,
1114+
preserve_metadata=preserve_metadata)
1115+
10611116
defmove(self, target):
10621117
"""
10631118
Recursively move this file or directory tree to the given destination.

‎Lib/test/test_pathlib/test_pathlib.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def test_is_notimplemented(self):
7575
# Tests for the pure classes.
7676
#
7777

78-
classPurePathTest(test_pathlib_abc.DummyJoinablePathTest):
78+
classPurePathTest(test_pathlib_abc.JoinablePathTest):
7979
cls=pathlib.PurePath
8080

8181
# Make sure any symbolic links in the base test path are resolved.
@@ -1002,7 +1002,7 @@ class cls(pathlib.PurePath):
10021002
# Tests for the concrete classes.
10031003
#
10041004

1005-
classPathTest(test_pathlib_abc.DummyRWPathTest, PurePathTest):
1005+
classPathTest(test_pathlib_abc.RWPathTest, PurePathTest):
10061006
"""Tests for the FS-accessing functionalities of the Path classes."""
10071007
cls=pathlib.Path
10081008
can_symlink=os_helper.can_symlink()
@@ -3119,7 +3119,7 @@ def test_group_windows(self):
31193119
P('c:/').group()
31203120

31213121

3122-
classPathWalkTest(test_pathlib_abc.DummyReadablePathWalkTest):
3122+
classPathWalkTest(test_pathlib_abc.ReadablePathWalkTest):
31233123
cls=pathlib.Path
31243124
base=PathTest.base
31253125
can_symlink=PathTest.can_symlink

‎Lib/test/test_pathlib/test_pathlib_abc.py‎

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,11 @@ def needs_windows(fn):
3131
#
3232

3333

34-
classJoinablePathTest(unittest.TestCase):
35-
cls=JoinablePath
36-
37-
deftest_magic_methods(self):
38-
P=self.cls
39-
self.assertFalse(hasattr(P, '__fspath__'))
40-
self.assertFalse(hasattr(P, '__bytes__'))
41-
self.assertIs(P.__reduce__, object.__reduce__)
42-
self.assertIs(P.__repr__, object.__repr__)
43-
self.assertIs(P.__hash__, object.__hash__)
44-
self.assertIs(P.__eq__, object.__eq__)
45-
self.assertIs(P.__lt__, object.__lt__)
46-
self.assertIs(P.__le__, object.__le__)
47-
self.assertIs(P.__gt__, object.__gt__)
48-
self.assertIs(P.__ge__, object.__ge__)
49-
50-
deftest_parser(self):
51-
self.assertIs(self.cls.parser, posixpath)
52-
53-
5434
classDummyJoinablePath(JoinablePath):
5535
__slots__= ('_segments',)
5636

37+
parser=posixpath
38+
5739
def__init__(self, *segments):
5840
self._segments=segments
5941

@@ -77,7 +59,7 @@ def with_segments(self, *pathsegments):
7759
returntype(self)(*pathsegments)
7860

7961

80-
classDummyJoinablePathTest(unittest.TestCase):
62+
classJoinablePathTest(unittest.TestCase):
8163
cls=DummyJoinablePath
8264

8365
# Use a base path that's unrelated to any real filesystem path.
@@ -94,6 +76,10 @@ def setUp(self):
9476
self.sep=self.parser.sep
9577
self.altsep=self.parser.altsep
9678

79+
deftest_is_joinable(self):
80+
p=self.cls(self.base)
81+
self.assertIsInstance(p, JoinablePath)
82+
9783
deftest_parser(self):
9884
self.assertIsInstance(self.cls.parser, _PathParser)
9985

@@ -878,6 +864,7 @@ class DummyReadablePath(ReadablePath, DummyJoinablePath):
878864

879865
_files={}
880866
_directories={}
867+
parser=posixpath
881868

882869
def__init__(self, *segments):
883870
super().__init__(*segments)
@@ -909,6 +896,9 @@ def iterdir(self):
909896
else:
910897
raiseFileNotFoundError(errno.ENOENT, "File not found", path)
911898

899+
defreadlink(self):
900+
raiseNotImplementedError
901+
912902

913903
classDummyWritablePath(WritablePath, DummyJoinablePath):
914904
__slots__= ()
@@ -942,8 +932,11 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
942932
self.parent.mkdir(parents=True, exist_ok=True)
943933
self.mkdir(mode, parents=False, exist_ok=exist_ok)
944934

935+
defsymlink_to(self, target, target_is_directory=False):
936+
raiseNotImplementedError
937+
945938

946-
classDummyReadablePathTest(DummyJoinablePathTest):
939+
classReadablePathTest(JoinablePathTest):
947940
"""Tests for ReadablePathTest methods that use stat(), open() and iterdir()."""
948941

949942
cls=DummyReadablePath
@@ -1010,6 +1003,10 @@ def assertEqualNormCase(self, path_a, path_b):
10101003
normcase=self.parser.normcase
10111004
self.assertEqual(normcase(path_a), normcase(path_b))
10121005

1006+
deftest_is_readable(self):
1007+
p=self.cls(self.base)
1008+
self.assertIsInstance(p, ReadablePath)
1009+
10131010
deftest_exists(self):
10141011
P=self.cls
10151012
p=P(self.base)
@@ -1378,15 +1375,19 @@ def test_is_symlink(self):
13781375
self.assertIs((P/'linkA\x00').is_file(), False)
13791376

13801377

1381-
classDummyWritablePathTest(DummyJoinablePathTest):
1378+
classWritablePathTest(JoinablePathTest):
13821379
cls=DummyWritablePath
13831380

1381+
deftest_is_writable(self):
1382+
p=self.cls(self.base)
1383+
self.assertIsInstance(p, WritablePath)
1384+
13841385

13851386
classDummyRWPath(DummyWritablePath, DummyReadablePath):
13861387
__slots__= ()
13871388

13881389

1389-
classDummyRWPathTest(DummyWritablePathTest, DummyReadablePathTest):
1390+
classRWPathTest(WritablePathTest, ReadablePathTest):
13901391
cls=DummyRWPath
13911392
can_symlink=False
13921393

@@ -1598,9 +1599,9 @@ def test_copy_into_empty_name(self):
15981599
self.assertRaises(ValueError, source.copy_into, target_dir)
15991600

16001601

1601-
classDummyReadablePathWalkTest(unittest.TestCase):
1602+
classReadablePathWalkTest(unittest.TestCase):
16021603
cls=DummyReadablePath
1603-
base=DummyReadablePathTest.base
1604+
base=ReadablePathTest.base
16041605
can_symlink=False
16051606

16061607
defsetUp(self):

0 commit comments

Comments
(0)