Skip to content

Commit ea98944

Browse files
committed
pathlib ABCs: defer path joining
Defer joining of path segments in the private `PurePathBase` ABC. The new behaviour matches how the public `PurePath` class handles path segments. This slightly reduces the size of `PurePath` objects by eliminating a `_raw_path` slot.
1 parent 9b7294c commit ea98944

File tree

3 files changed

+44
-48
lines changed

3 files changed

+44
-48
lines changed

Lib/pathlib/_abc.py

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class PathGlobber(_GlobberBase):
9999
@staticmethod
100100
def concat_path(path, text):
101101
"""Appends text to the given path."""
102-
return path.with_segments(path._raw_path + text)
102+
return path.with_segments(str(path) + text)
103103

104104

105105
class PurePathBase:
@@ -112,9 +112,13 @@ class PurePathBase:
112112
"""
113113

114114
__slots__ = (
115-
# The `_raw_path` slot store a joined string path. This is set in the
116-
# `__init__()` method.
117-
'_raw_path',
115+
# The `_raw_paths` slot stores unjoined string paths. This is set in
116+
# the `__init__()` method.
117+
'_raw_paths',
118+
119+
# The `_str` slot stores the string representation of the path,
120+
# computed when `__str__()` is called for the first time.
121+
'_str',
118122

119123
# The '_resolving' slot stores a boolean indicating whether the path
120124
# is being processed by `PathBase.resolve()`. This prevents duplicate
@@ -124,11 +128,14 @@ class PurePathBase:
124128
parser = ParserBase()
125129
_globber = PathGlobber
126130

127-
def __init__(self, path, *paths):
128-
self._raw_path = self.parser.join(path, *paths) if paths else path
129-
if not isinstance(self._raw_path, str):
130-
raise TypeError(
131-
f"path should be a str, not {type(self._raw_path).__name__!r}")
131+
def __init__(self, arg, *args):
132+
paths = [arg]
133+
paths.extend(args)
134+
for path in paths:
135+
if not isinstance(path, str):
136+
raise TypeError(
137+
f"path should be a str, not {type(path).__name__!r}")
138+
self._raw_paths = paths
132139
self._resolving = False
133140

134141
def with_segments(self, *pathsegments):
@@ -138,10 +145,25 @@ def with_segments(self, *pathsegments):
138145
"""
139146
return type(self)(*pathsegments)
140147

148+
@property
149+
def _raw_path(self):
150+
paths = self._raw_paths
151+
if len(paths) == 0:
152+
path = ''
153+
elif len(paths) == 1:
154+
path = paths[0]
155+
else:
156+
path = self.parser.join(*paths)
157+
return path
158+
141159
def __str__(self):
142160
"""Return the string representation of the path, suitable for
143161
passing to system calls."""
144-
return self._raw_path
162+
try:
163+
return self._str
164+
except AttributeError:
165+
self._str = self._raw_path
166+
return self._str
145167

146168
def as_posix(self):
147169
"""Return the string representation of the path with forward (/)
@@ -166,7 +188,7 @@ def anchor(self):
166188
@property
167189
def name(self):
168190
"""The final path component, if any."""
169-
return self.parser.split(self._raw_path)[1]
191+
return self.parser.split(str(self))[1]
170192

171193
@property
172194
def suffix(self):
@@ -202,7 +224,7 @@ def with_name(self, name):
202224
split = self.parser.split
203225
if split(name)[0]:
204226
raise ValueError(f"Invalid name {name!r}")
205-
return self.with_segments(split(self._raw_path)[0], name)
227+
return self.with_segments(split(str(self))[0], name)
206228

207229
def with_stem(self, stem):
208230
"""Return a new path with the stem changed."""
@@ -242,17 +264,17 @@ def relative_to(self, other, *, walk_up=False):
242264
anchor0, parts0 = self._stack
243265
anchor1, parts1 = other._stack
244266
if anchor0 != anchor1:
245-
raise ValueError(f"{self._raw_path!r} and {other._raw_path!r} have different anchors")
267+
raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
246268
while parts0 and parts1 and parts0[-1] == parts1[-1]:
247269
parts0.pop()
248270
parts1.pop()
249271
for part in parts1:
250272
if not part or part == '.':
251273
pass
252274
elif not walk_up:
253-
raise ValueError(f"{self._raw_path!r} is not in the subpath of {other._raw_path!r}")
275+
raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
254276
elif part == '..':
255-
raise ValueError(f"'..' segment in {other._raw_path!r} cannot be walked")
277+
raise ValueError(f"'..' segment in {str(other)!r} cannot be walked")
256278
else:
257279
parts0.append('..')
258280
return self.with_segments('', *reversed(parts0))
@@ -289,17 +311,17 @@ def joinpath(self, *pathsegments):
289311
paths) or a totally different path (if one of the arguments is
290312
anchored).
291313
"""
292-
return self.with_segments(self._raw_path, *pathsegments)
314+
return self.with_segments(*self._raw_paths, *pathsegments)
293315

294316
def __truediv__(self, key):
295317
try:
296-
return self.with_segments(self._raw_path, key)
318+
return self.with_segments(*self._raw_paths, key)
297319
except TypeError:
298320
return NotImplemented
299321

300322
def __rtruediv__(self, key):
301323
try:
302-
return self.with_segments(key, self._raw_path)
324+
return self.with_segments(key, *self._raw_paths)
303325
except TypeError:
304326
return NotImplemented
305327

@@ -311,7 +333,7 @@ def _stack(self):
311333
*parts* is a reversed list of parts following the anchor.
312334
"""
313335
split = self.parser.split
314-
path = self._raw_path
336+
path = str(self)
315337
parent, name = split(path)
316338
names = []
317339
while path != parent:
@@ -323,7 +345,7 @@ def _stack(self):
323345
@property
324346
def parent(self):
325347
"""The logical parent of the path."""
326-
path = self._raw_path
348+
path = str(self)
327349
parent = self.parser.split(path)[0]
328350
if path != parent:
329351
parent = self.with_segments(parent)
@@ -335,7 +357,7 @@ def parent(self):
335357
def parents(self):
336358
"""A sequence of this path's logical parents."""
337359
split = self.parser.split
338-
path = self._raw_path
360+
path = str(self)
339361
parent = split(path)[0]
340362
parents = []
341363
while path != parent:
@@ -347,7 +369,7 @@ def parents(self):
347369
def is_absolute(self):
348370
"""True if the path is absolute (has both a root and, if applicable,
349371
a drive)."""
350-
return self.parser.isabs(self._raw_path)
372+
return self.parser.isabs(str(self))
351373

352374
@property
353375
def _pattern_str(self):

Lib/pathlib/_local.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,6 @@ class PurePath(PurePathBase):
6868
"""
6969

7070
__slots__ = (
71-
# The `_raw_paths` slot stores unnormalized string paths. This is set
72-
# in the `__init__()` method.
73-
'_raw_paths',
74-
7571
# The `_drv`, `_root` and `_tail_cached` slots store parsed and
7672
# normalized parts of the path. They are set when any of the `drive`,
7773
# `root` or `_tail` properties are accessed for the first time. The
@@ -81,11 +77,6 @@ class PurePath(PurePathBase):
8177
# tail are normalized.
8278
'_drv', '_root', '_tail_cached',
8379

84-
# The `_str` slot stores the string representation of the path,
85-
# computed from the drive, root and tail when `__str__()` is called
86-
# for the first time. It's used to implement `_str_normcase`
87-
'_str',
88-
8980
# The `_str_normcase_cached` slot stores the string path with
9081
# normalized case. It is set when the `_str_normcase` property is
9182
# accessed for the first time. It's used to implement `__eq__()`
@@ -299,18 +290,6 @@ def _parse_pattern(cls, pattern):
299290
parts.append('')
300291
return parts
301292

302-
@property
303-
def _raw_path(self):
304-
"""The joined but unnormalized path."""
305-
paths = self._raw_paths
306-
if len(paths) == 0:
307-
path = ''
308-
elif len(paths) == 1:
309-
path = paths[0]
310-
else:
311-
path = self.parser.join(*paths)
312-
return path
313-
314293
@property
315294
def drive(self):
316295
"""The drive prefix (letter or UNC path), if any."""

Lib/test/test_pathlib/test_pathlib_abc.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,6 @@ def test_unsupported_operation_pure(self):
8686
p.suffix
8787
with self.assertRaises(e):
8888
p.suffixes
89-
with self.assertRaises(e):
90-
p / 'bar'
91-
with self.assertRaises(e):
92-
'bar' / p
93-
self.assertRaises(e, p.joinpath, 'bar')
9489
self.assertRaises(e, p.with_name, 'bar')
9590
self.assertRaises(e, p.with_stem, 'bar')
9691
self.assertRaises(e, p.with_suffix, '.txt')

0 commit comments

Comments
 (0)