Skip to content

Commit 95688bb

Browse files
committed
GH-128520: Subclass abc.ABC in pathlib._abc
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.
1 parent 22a4421 commit 95688bb

File tree

4 files changed

+86
-51
lines changed

4 files changed

+86
-51
lines changed

Lib/pathlib/_abc.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import functools
1515
import operator
16-
import posixpath
16+
from abc import ABC, abstractmethod
1717
from errno import EINVAL
1818
from glob import _GlobberBase, _no_recurse_symlinks
1919
from pathlib._os import copyfileobj
@@ -190,24 +190,32 @@ def _ensure_distinct_path(self, source):
190190
raise err
191191

192192

193-
class JoinablePath:
194-
"""Base class for pure path objects.
193+
class JoinablePath(ABC):
194+
"""Abstract base class for pure path objects.
195195
196196
This class *does not* provide several magic methods that are defined in
197-
its subclass PurePath. They are: __init__, __fspath__, __bytes__,
197+
its implementation PurePath. They are: __init__, __fspath__, __bytes__,
198198
__reduce__, __hash__, __eq__, __lt__, __le__, __gt__, __ge__.
199199
"""
200-
201200
__slots__ = ()
202-
parser = posixpath
203201

202+
@property
203+
@abstractmethod
204+
def parser(self):
205+
"""Implementation of pathlib._types.Parser used for low-level path
206+
parsing and manipulation.
207+
"""
208+
raise NotImplementedError
209+
210+
@abstractmethod
204211
def with_segments(self, *pathsegments):
205212
"""Construct a new path object from any number of path-like objects.
206213
Subclasses may override this method to customize how new path objects
207214
are created from methods like `iterdir()`.
208215
"""
209216
raise NotImplementedError
210217

218+
@abstractmethod
211219
def __str__(self):
212220
"""Return the string representation of the path, suitable for
213221
passing to system calls."""
@@ -378,20 +386,15 @@ def full_match(self, pattern, *, case_sensitive=None):
378386

379387

380388
class ReadablePath(JoinablePath):
381-
"""Base class for concrete path objects.
382-
383-
This class provides dummy implementations for many methods that derived
384-
classes can override selectively; the default implementations raise
385-
NotImplementedError. The most basic methods, such as stat() and open(),
386-
directly raise NotImplementedError; these basic methods are called by
387-
other methods such as is_dir() and read_text().
389+
"""Abstract base class for readable path objects.
388390
389-
The Path class derives this class to implement local filesystem paths.
390-
Users may derive their own classes to implement virtual filesystem paths,
391-
such as paths in archive files or on remote storage systems.
391+
The Path class implements this ABC for local filesystem paths. Users may
392+
create subclasses to implement readable virtual filesystem paths, such as
393+
paths in archive files or on remote storage systems.
392394
"""
393395
__slots__ = ()
394396

397+
@abstractmethod
395398
def exists(self, *, follow_symlinks=True):
396399
"""
397400
Whether this path exists.
@@ -401,25 +404,29 @@ def exists(self, *, follow_symlinks=True):
401404
"""
402405
raise NotImplementedError
403406

407+
@abstractmethod
404408
def is_dir(self, *, follow_symlinks=True):
405409
"""
406410
Whether this path is a directory.
407411
"""
408412
raise NotImplementedError
409413

414+
@abstractmethod
410415
def is_file(self, *, follow_symlinks=True):
411416
"""
412417
Whether this path is a regular file (also True for symlinks pointing
413418
to regular files).
414419
"""
415420
raise NotImplementedError
416421

422+
@abstractmethod
417423
def is_symlink(self):
418424
"""
419425
Whether this path is a symbolic link.
420426
"""
421427
raise NotImplementedError
422428

429+
@abstractmethod
423430
def open(self, mode='r', buffering=-1, encoding=None,
424431
errors=None, newline=None):
425432
"""
@@ -451,6 +458,7 @@ def _scandir(self):
451458
import contextlib
452459
return contextlib.nullcontext(self.iterdir())
453460

461+
@abstractmethod
454462
def iterdir(self):
455463
"""Yield path objects of the directory contents.
456464
@@ -526,6 +534,7 @@ def walk(self, top_down=True, on_error=None, follow_symlinks=False):
526534
yield path, dirnames, filenames
527535
paths += [path.joinpath(d) for d in reversed(dirnames)]
528536

537+
@abstractmethod
529538
def readlink(self):
530539
"""
531540
Return the path to which the symbolic link points.
@@ -552,15 +561,23 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
552561

553562

554563
class WritablePath(ReadablePath):
564+
"""Abstract base class for writable path objects.
565+
566+
The Path class implements this ABC for local filesystem paths. Users may
567+
create subclasses to implement writable virtual filesystem paths, such as
568+
paths in archive files or on remote storage systems.
569+
"""
555570
__slots__ = ()
556571

572+
@abstractmethod
557573
def symlink_to(self, target, target_is_directory=False):
558574
"""
559575
Make this path a symlink pointing to the target path.
560576
Note the order of arguments (link, target) is the reverse of os.symlink.
561577
"""
562578
raise NotImplementedError
563579

580+
@abstractmethod
564581
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
565582
"""
566583
Create a new directory at this given path.

Lib/pathlib/_local.py

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

2222
from pathlib._os import copyfile
23-
from pathlib._abc import CopyWriter, JoinablePath, WritablePath
23+
from pathlib._abc import CopyWriter, JoinablePath, ReadablePath, WritablePath
2424

2525

2626
__all__ = [
@@ -190,7 +190,7 @@ def _ensure_different_file(self, source):
190190
raise err
191191

192192

193-
class PurePath(JoinablePath):
193+
class PurePath:
194194
"""Base class for manipulating paths without I/O.
195195
196196
PurePath represents a filesystem path and offers operations which
@@ -534,6 +534,9 @@ def with_name(self, name):
534534
tail[-1] = name
535535
return self._from_parsed_parts(self.drive, self.root, tail)
536536

537+
with_stem = JoinablePath.with_stem
538+
with_suffix = JoinablePath.with_suffix
539+
537540
@property
538541
def stem(self):
539542
"""The final path component, minus its last suffix."""
@@ -641,6 +644,8 @@ def as_uri(self):
641644
from urllib.parse import quote_from_bytes
642645
return prefix + quote_from_bytes(os.fsencode(path))
643646

647+
match = JoinablePath.match
648+
644649
def full_match(self, pattern, *, case_sensitive=None):
645650
"""
646651
Return True if this path matches the given glob-style pattern. The
@@ -658,9 +663,10 @@ def full_match(self, pattern, *, case_sensitive=None):
658663
globber = _StringGlobber(self.parser.sep, case_sensitive, recursive=True)
659664
return globber.compile(pattern)(path) is not None
660665

661-
# Subclassing os.PathLike makes isinstance() checks slower,
662-
# which in turn makes Path construction slower. Register instead!
666+
# Subclassing abc.ABC makes isinstance() checks slower,
667+
# which in turn makes path construction slower. Register instead!
663668
os.PathLike.register(PurePath)
669+
JoinablePath.register(PurePath)
664670

665671

666672
class PurePosixPath(PurePath):
@@ -683,7 +689,7 @@ class PureWindowsPath(PurePath):
683689
__slots__ = ()
684690

685691

686-
class Path(WritablePath, PurePath):
692+
class Path(PurePath):
687693
"""PurePath subclass that can make system calls.
688694
689695
Path represents a filesystem path but unlike PurePath, also offers
@@ -823,14 +829,18 @@ def open(self, mode='r', buffering=-1, encoding=None,
823829
encoding = io.text_encoding(encoding)
824830
return io.open(self, mode, buffering, encoding, errors, newline)
825831

832+
read_bytes = ReadablePath.read_bytes
833+
826834
def read_text(self, encoding=None, errors=None, newline=None):
827835
"""
828836
Open the file in text mode, read it, and close the file.
829837
"""
830838
# Call io.text_encoding() here to ensure any warning is raised at an
831839
# appropriate stack level.
832840
encoding = io.text_encoding(encoding)
833-
return super().read_text(encoding, errors, newline)
841+
return ReadablePath.read_text(self, encoding, errors, newline)
842+
843+
write_bytes = WritablePath.write_bytes
834844

835845
def write_text(self, data, encoding=None, errors=None, newline=None):
836846
"""
@@ -839,7 +849,7 @@ def write_text(self, data, encoding=None, errors=None, newline=None):
839849
# Call io.text_encoding() here to ensure any warning is raised at an
840850
# appropriate stack level.
841851
encoding = io.text_encoding(encoding)
842-
return super().write_text(data, encoding, errors, newline)
852+
return WritablePath.write_text(self, data, encoding, errors, newline)
843853

844854
_remove_leading_dot = operator.itemgetter(slice(2, None))
845855
_remove_trailing_slash = operator.itemgetter(slice(-1))
@@ -1124,6 +1134,8 @@ def replace(self, target):
11241134

11251135
copy = property(_LocalCopyWriter, doc=_LocalCopyWriter.__call__.__doc__)
11261136

1137+
copy_into = ReadablePath.copy_into
1138+
11271139
def move(self, target):
11281140
"""
11291141
Recursively move this file or directory tree to the given destination.
@@ -1242,6 +1254,11 @@ def from_uri(cls, uri):
12421254
raise ValueError(f"URI is not absolute: {uri!r}")
12431255
return path
12441256

1257+
# Subclassing abc.ABC makes isinstance() checks slower,
1258+
# which in turn makes path construction slower. Register instead!
1259+
ReadablePath.register(Path)
1260+
WritablePath.register(Path)
1261+
12451262

12461263
class PosixPath(Path, PurePosixPath):
12471264
"""Path subclass for non-Windows systems.

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-
class PurePathTest(test_pathlib_abc.DummyJoinablePathTest):
78+
class PurePathTest(test_pathlib_abc.JoinablePathTest):
7979
cls = pathlib.PurePath
8080

8181
# Make sure any symbolic links in the base test path are resolved.
@@ -924,7 +924,7 @@ class cls(pathlib.PurePath):
924924
# Tests for the concrete classes.
925925
#
926926

927-
class PathTest(test_pathlib_abc.DummyWritablePathTest, PurePathTest):
927+
class PathTest(test_pathlib_abc.WritablePathTest, PurePathTest):
928928
"""Tests for the FS-accessing functionalities of the Path classes."""
929929
cls = pathlib.Path
930930
can_symlink = os_helper.can_symlink()
@@ -3019,7 +3019,7 @@ def test_group_windows(self):
30193019
P('c:/').group()
30203020

30213021

3022-
class PathWalkTest(test_pathlib_abc.DummyReadablePathWalkTest):
3022+
class PathWalkTest(test_pathlib_abc.ReadablePathWalkTest):
30233023
cls = pathlib.Path
30243024
base = PathTest.base
30253025
can_symlink = PathTest.can_symlink

Lib/test/test_pathlib/test_pathlib_abc.py

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

3333

34-
class JoinablePathTest(unittest.TestCase):
35-
cls = JoinablePath
36-
37-
def test_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-
def test_parser(self):
51-
self.assertIs(self.cls.parser, posixpath)
52-
53-
5434
class DummyJoinablePath(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
return type(self)(*pathsegments)
7860

7961

80-
class DummyJoinablePathTest(unittest.TestCase):
62+
class JoinablePathTest(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+
def test_is_joinable(self):
80+
p = self.cls(self.base)
81+
self.assertIsInstance(p, JoinablePath)
82+
9783
def test_parser(self):
9884
self.assertIsInstance(self.cls.parser, Parser)
9985

@@ -940,6 +926,7 @@ class DummyReadablePath(ReadablePath):
940926

941927
_files = {}
942928
_directories = {}
929+
parser = posixpath
943930

944931
def __init__(self, *segments):
945932
self._segments = segments
@@ -1012,6 +999,9 @@ def iterdir(self):
1012999
else:
10131000
raise FileNotFoundError(errno.ENOENT, "File not found", path)
10141001

1002+
def readlink(self):
1003+
raise NotImplementedError
1004+
10151005

10161006
class DummyWritablePath(DummyReadablePath, WritablePath):
10171007
__slots__ = ()
@@ -1034,8 +1024,11 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
10341024
self.parent.mkdir(parents=True, exist_ok=True)
10351025
self.mkdir(mode, parents=False, exist_ok=exist_ok)
10361026

1027+
def symlink_to(self, target, target_is_directory=False):
1028+
raise NotImplementedError
1029+
10371030

1038-
class DummyReadablePathTest(DummyJoinablePathTest):
1031+
class ReadablePathTest(JoinablePathTest):
10391032
"""Tests for ReadablePathTest methods that use stat(), open() and iterdir()."""
10401033

10411034
cls = DummyReadablePath
@@ -1102,6 +1095,10 @@ def assertEqualNormCase(self, path_a, path_b):
11021095
normcase = self.parser.normcase
11031096
self.assertEqual(normcase(path_a), normcase(path_b))
11041097

1098+
def test_is_readable(self):
1099+
p = self.cls(self.base)
1100+
self.assertIsInstance(p, ReadablePath)
1101+
11051102
def test_exists(self):
11061103
P = self.cls
11071104
p = P(self.base)
@@ -1359,9 +1356,13 @@ def test_is_symlink(self):
13591356
self.assertIs((P / 'linkA\x00').is_file(), False)
13601357

13611358

1362-
class DummyWritablePathTest(DummyReadablePathTest):
1359+
class WritablePathTest(ReadablePathTest):
13631360
cls = DummyWritablePath
13641361

1362+
def test_is_writable(self):
1363+
p = self.cls(self.base)
1364+
self.assertIsInstance(p, WritablePath)
1365+
13651366
def test_read_write_bytes(self):
13661367
p = self.cls(self.base)
13671368
(p / 'fileA').write_bytes(b'abcdefg')
@@ -1570,9 +1571,9 @@ def test_copy_into_empty_name(self):
15701571
self.assertRaises(ValueError, source.copy_into, target_dir)
15711572

15721573

1573-
class DummyReadablePathWalkTest(unittest.TestCase):
1574+
class ReadablePathWalkTest(unittest.TestCase):
15741575
cls = DummyReadablePath
1575-
base = DummyReadablePathTest.base
1576+
base = ReadablePathTest.base
15761577
can_symlink = False
15771578

15781579
def setUp(self):

0 commit comments

Comments
 (0)