Skip to content

Commit f86d754

Browse files
barneygalesrinivasreddy
authored andcommitted
pythonGH-127807: pathlib ABCs: remove PurePathBase._raw_paths (python#127883)
Remove the `PurePathBase` initializer, and make `with_segments()` and `__str__()` abstract. This allows us to drop the `_raw_paths` attribute, and also the `Parser.join()` protocol method.
1 parent ff26364 commit f86d754

File tree

5 files changed

+92
-96
lines changed

5 files changed

+92
-96
lines changed

Lib/pathlib/_abc.py

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -44,49 +44,25 @@ class PurePathBase:
4444
"""Base class for pure path objects.
4545
4646
This class *does not* provide several magic methods that are defined in
47-
its subclass PurePath. They are: __fspath__, __bytes__, __reduce__,
48-
__hash__, __eq__, __lt__, __le__, __gt__, __ge__. Its initializer and path
49-
joining methods accept only strings, not os.PathLike objects more broadly.
47+
its subclass PurePath. They are: __init__, __fspath__, __bytes__,
48+
__reduce__, __hash__, __eq__, __lt__, __le__, __gt__, __ge__.
5049
"""
5150

52-
__slots__ = (
53-
# The `_raw_paths` slot stores unjoined string paths. This is set in
54-
# the `__init__()` method.
55-
'_raw_paths',
56-
)
51+
__slots__ = ()
5752
parser = posixpath
5853
_globber = PathGlobber
5954

60-
def __init__(self, *args):
61-
for arg in args:
62-
if not isinstance(arg, str):
63-
raise TypeError(
64-
f"argument should be a str, not {type(arg).__name__!r}")
65-
self._raw_paths = list(args)
66-
6755
def with_segments(self, *pathsegments):
6856
"""Construct a new path object from any number of path-like objects.
6957
Subclasses may override this method to customize how new path objects
7058
are created from methods like `iterdir()`.
7159
"""
72-
return type(self)(*pathsegments)
60+
raise NotImplementedError
7361

7462
def __str__(self):
7563
"""Return the string representation of the path, suitable for
7664
passing to system calls."""
77-
paths = self._raw_paths
78-
if len(paths) == 1:
79-
return paths[0]
80-
elif paths:
81-
# Join path segments from the initializer.
82-
path = self.parser.join(*paths)
83-
# Cache the joined path.
84-
paths.clear()
85-
paths.append(path)
86-
return path
87-
else:
88-
paths.append('')
89-
return ''
65+
raise NotImplementedError
9066

9167
def as_posix(self):
9268
"""Return the string representation of the path with forward (/)
@@ -234,17 +210,17 @@ def joinpath(self, *pathsegments):
234210
paths) or a totally different path (if one of the arguments is
235211
anchored).
236212
"""
237-
return self.with_segments(*self._raw_paths, *pathsegments)
213+
return self.with_segments(str(self), *pathsegments)
238214

239215
def __truediv__(self, key):
240216
try:
241-
return self.with_segments(*self._raw_paths, key)
217+
return self.with_segments(str(self), key)
242218
except TypeError:
243219
return NotImplemented
244220

245221
def __rtruediv__(self, key):
246222
try:
247-
return self.with_segments(key, *self._raw_paths)
223+
return self.with_segments(key, str(self))
248224
except TypeError:
249225
return NotImplemented
250226

Lib/pathlib/_local.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ class PurePath(PurePathBase):
7777
"""
7878

7979
__slots__ = (
80+
# The `_raw_paths` slot stores unjoined string paths. This is set in
81+
# the `__init__()` method.
82+
'_raw_paths',
83+
8084
# The `_drv`, `_root` and `_tail_cached` slots store parsed and
8185
# normalized parts of the path. They are set when any of the `drive`,
8286
# `root` or `_tail` properties are accessed for the first time. The
@@ -140,9 +144,15 @@ def __init__(self, *args):
140144
"object where __fspath__ returns a str, "
141145
f"not {type(path).__name__!r}")
142146
paths.append(path)
143-
# Avoid calling super().__init__, as an optimisation
144147
self._raw_paths = paths
145148

149+
def with_segments(self, *pathsegments):
150+
"""Construct a new path object from any number of path-like objects.
151+
Subclasses may override this method to customize how new path objects
152+
are created from methods like `iterdir()`.
153+
"""
154+
return type(self)(*pathsegments)
155+
146156
def joinpath(self, *pathsegments):
147157
"""Combine this path with one or several arguments, and return a
148158
new path representing either a subpath (if all arguments are relative
@@ -304,14 +314,29 @@ def _parse_pattern(cls, pattern):
304314
parts.append('')
305315
return parts
306316

317+
@property
318+
def _raw_path(self):
319+
paths = self._raw_paths
320+
if len(paths) == 1:
321+
return paths[0]
322+
elif paths:
323+
# Join path segments from the initializer.
324+
path = self.parser.join(*paths)
325+
# Cache the joined path.
326+
paths.clear()
327+
paths.append(path)
328+
return path
329+
else:
330+
paths.append('')
331+
return ''
332+
307333
@property
308334
def drive(self):
309335
"""The drive prefix (letter or UNC path), if any."""
310336
try:
311337
return self._drv
312338
except AttributeError:
313-
raw_path = PurePathBase.__str__(self)
314-
self._drv, self._root, self._tail_cached = self._parse_path(raw_path)
339+
self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
315340
return self._drv
316341

317342
@property
@@ -320,17 +345,15 @@ def root(self):
320345
try:
321346
return self._root
322347
except AttributeError:
323-
raw_path = PurePathBase.__str__(self)
324-
self._drv, self._root, self._tail_cached = self._parse_path(raw_path)
348+
self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
325349
return self._root
326350

327351
@property
328352
def _tail(self):
329353
try:
330354
return self._tail_cached
331355
except AttributeError:
332-
raw_path = PurePathBase.__str__(self)
333-
self._drv, self._root, self._tail_cached = self._parse_path(raw_path)
356+
self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
334357
return self._tail_cached
335358

336359
@property

Lib/pathlib/_types.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ class Parser(Protocol):
1414
"""
1515

1616
sep: str
17-
def join(self, path: str, *paths: str) -> str: ...
1817
def split(self, path: str) -> tuple[str, str]: ...
1918
def splitdrive(self, path: str) -> tuple[str, str]: ...
2019
def splitext(self, path: str) -> tuple[str, str]: ...

Lib/test/test_pathlib/test_pathlib.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,31 @@ def test_fspath_common(self):
229229
self._check_str(p.__fspath__(), ('a/b',))
230230
self._check_str(os.fspath(p), ('a/b',))
231231

232+
def test_bytes(self):
233+
P = self.cls
234+
with self.assertRaises(TypeError):
235+
P(b'a')
236+
with self.assertRaises(TypeError):
237+
P(b'a', 'b')
238+
with self.assertRaises(TypeError):
239+
P('a', b'b')
240+
with self.assertRaises(TypeError):
241+
P('a').joinpath(b'b')
242+
with self.assertRaises(TypeError):
243+
P('a') / b'b'
244+
with self.assertRaises(TypeError):
245+
b'a' / P('b')
246+
with self.assertRaises(TypeError):
247+
P('a').match(b'b')
248+
with self.assertRaises(TypeError):
249+
P('a').relative_to(b'b')
250+
with self.assertRaises(TypeError):
251+
P('a').with_name(b'b')
252+
with self.assertRaises(TypeError):
253+
P('a').with_stem(b'b')
254+
with self.assertRaises(TypeError):
255+
P('a').with_suffix(b'b')
256+
232257
def test_bytes_exc_message(self):
233258
P = self.cls
234259
message = (r"argument should be a str or an os\.PathLike object "

Lib/test/test_pathlib/test_pathlib_abc.py

Lines changed: 29 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,15 @@ def test_parser(self):
5353

5454

5555
class DummyPurePath(PurePathBase):
56-
__slots__ = ()
56+
__slots__ = ('_segments',)
57+
58+
def __init__(self, *segments):
59+
self._segments = segments
60+
61+
def __str__(self):
62+
if self._segments:
63+
return self.parser.join(*self._segments)
64+
return ''
5765

5866
def __eq__(self, other):
5967
if not isinstance(other, DummyPurePath):
@@ -66,6 +74,9 @@ def __hash__(self):
6674
def __repr__(self):
6775
return "{}({!r})".format(self.__class__.__name__, self.as_posix())
6876

77+
def with_segments(self, *pathsegments):
78+
return type(self)(*pathsegments)
79+
6980

7081
class DummyPurePathTest(unittest.TestCase):
7182
cls = DummyPurePath
@@ -97,30 +108,11 @@ def test_constructor_common(self):
97108
P('a/b/c')
98109
P('/a/b/c')
99110

100-
def test_bytes(self):
101-
P = self.cls
102-
with self.assertRaises(TypeError):
103-
P(b'a')
104-
with self.assertRaises(TypeError):
105-
P(b'a', 'b')
106-
with self.assertRaises(TypeError):
107-
P('a', b'b')
108-
with self.assertRaises(TypeError):
109-
P('a').joinpath(b'b')
110-
with self.assertRaises(TypeError):
111-
P('a') / b'b'
112-
with self.assertRaises(TypeError):
113-
b'a' / P('b')
114-
with self.assertRaises(TypeError):
115-
P('a').match(b'b')
116-
with self.assertRaises(TypeError):
117-
P('a').relative_to(b'b')
118-
with self.assertRaises(TypeError):
119-
P('a').with_name(b'b')
120-
with self.assertRaises(TypeError):
121-
P('a').with_stem(b'b')
122-
with self.assertRaises(TypeError):
123-
P('a').with_suffix(b'b')
111+
def test_fspath_common(self):
112+
self.assertRaises(TypeError, os.fspath, self.cls(''))
113+
114+
def test_as_bytes_common(self):
115+
self.assertRaises(TypeError, bytes, self.cls(''))
124116

125117
def _check_str_subclass(self, *args):
126118
# Issue #21127: it should be possible to construct a PurePath object
@@ -1286,36 +1278,6 @@ def test_is_absolute_windows(self):
12861278
# Tests for the virtual classes.
12871279
#
12881280

1289-
class PathBaseTest(PurePathBaseTest):
1290-
cls = PathBase
1291-
1292-
def test_not_implemented_error(self):
1293-
p = self.cls('')
1294-
e = NotImplementedError
1295-
self.assertRaises(e, p.stat)
1296-
self.assertRaises(e, p.exists)
1297-
self.assertRaises(e, p.is_dir)
1298-
self.assertRaises(e, p.is_file)
1299-
self.assertRaises(e, p.is_symlink)
1300-
self.assertRaises(e, p.open)
1301-
self.assertRaises(e, p.read_bytes)
1302-
self.assertRaises(e, p.read_text)
1303-
self.assertRaises(e, p.write_bytes, b'foo')
1304-
self.assertRaises(e, p.write_text, 'foo')
1305-
self.assertRaises(e, p.iterdir)
1306-
self.assertRaises(e, lambda: list(p.glob('*')))
1307-
self.assertRaises(e, lambda: list(p.rglob('*')))
1308-
self.assertRaises(e, lambda: list(p.walk()))
1309-
self.assertRaises(e, p.readlink)
1310-
self.assertRaises(e, p.symlink_to, 'foo')
1311-
self.assertRaises(e, p.mkdir)
1312-
1313-
def test_fspath_common(self):
1314-
self.assertRaises(TypeError, os.fspath, self.cls(''))
1315-
1316-
def test_as_bytes_common(self):
1317-
self.assertRaises(TypeError, bytes, self.cls(''))
1318-
13191281

13201282
class DummyPathIO(io.BytesIO):
13211283
"""
@@ -1342,11 +1304,19 @@ class DummyPath(PathBase):
13421304
Simple implementation of PathBase that keeps files and directories in
13431305
memory.
13441306
"""
1345-
__slots__ = ()
1307+
__slots__ = ('_segments')
13461308

13471309
_files = {}
13481310
_directories = {}
13491311

1312+
def __init__(self, *segments):
1313+
self._segments = segments
1314+
1315+
def __str__(self):
1316+
if self._segments:
1317+
return self.parser.join(*self._segments)
1318+
return ''
1319+
13501320
def __eq__(self, other):
13511321
if not isinstance(other, DummyPath):
13521322
return NotImplemented
@@ -1358,6 +1328,9 @@ def __hash__(self):
13581328
def __repr__(self):
13591329
return "{}({!r})".format(self.__class__.__name__, self.as_posix())
13601330

1331+
def with_segments(self, *pathsegments):
1332+
return type(self)(*pathsegments)
1333+
13611334
def stat(self, *, follow_symlinks=True):
13621335
path = str(self).rstrip('/')
13631336
if path in self._files:

0 commit comments

Comments
 (0)