Skip to content

Commit f20b151

Browse files
authored
pathlib ABCs: add _raw_path property (#113976)
It's wrong for the `PurePathBase` methods to rely so much on `__str__()`. Instead, they should treat the raw path(s) as opaque objects and leave the details to `pathmod`. This commit adds a `PurePathBase._raw_path` property and uses it through many of the other ABC methods. These methods are all redefined in `PurePath` and `Path`, so this has no effect on the public classes.
1 parent e4ff131 commit f20b151

File tree

2 files changed

+31
-20
lines changed

2 files changed

+31
-20
lines changed

Lib/pathlib/__init__.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,23 +257,25 @@ def _parse_path(cls, path):
257257
parsed = [sys.intern(str(x)) for x in rel.split(sep) if x and x != '.']
258258
return drv, root, parsed
259259

260-
def _load_parts(self):
260+
@property
261+
def _raw_path(self):
262+
"""The joined but unnormalized path."""
261263
paths = self._raw_paths
262264
if len(paths) == 0:
263265
path = ''
264266
elif len(paths) == 1:
265267
path = paths[0]
266268
else:
267269
path = self.pathmod.join(*paths)
268-
self._drv, self._root, self._tail_cached = self._parse_path(path)
270+
return path
269271

270272
@property
271273
def drive(self):
272274
"""The drive prefix (letter or UNC path), if any."""
273275
try:
274276
return self._drv
275277
except AttributeError:
276-
self._load_parts()
278+
self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
277279
return self._drv
278280

279281
@property
@@ -282,15 +284,15 @@ def root(self):
282284
try:
283285
return self._root
284286
except AttributeError:
285-
self._load_parts()
287+
self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
286288
return self._root
287289

288290
@property
289291
def _tail(self):
290292
try:
291293
return self._tail_cached
292294
except AttributeError:
293-
self._load_parts()
295+
self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
294296
return self._tail_cached
295297

296298
@property

Lib/pathlib/_abc.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,15 @@ def with_segments(self, *pathsegments):
163163
"""
164164
return type(self)(*pathsegments)
165165

166+
@property
167+
def _raw_path(self):
168+
"""The joined but unnormalized path."""
169+
return self.pathmod.join(*self._raw_paths)
170+
166171
def __str__(self):
167172
"""Return the string representation of the path, suitable for
168173
passing to system calls."""
169-
return self.pathmod.join(*self._raw_paths)
174+
return self._raw_path
170175

171176
def as_posix(self):
172177
"""Return the string representation of the path with forward (/)
@@ -176,23 +181,23 @@ def as_posix(self):
176181
@property
177182
def drive(self):
178183
"""The drive prefix (letter or UNC path), if any."""
179-
return self.pathmod.splitdrive(str(self))[0]
184+
return self.pathmod.splitdrive(self._raw_path)[0]
180185

181186
@property
182187
def root(self):
183188
"""The root of the path, if any."""
184-
return self.pathmod.splitroot(str(self))[1]
189+
return self.pathmod.splitroot(self._raw_path)[1]
185190

186191
@property
187192
def anchor(self):
188193
"""The concatenation of the drive and root, or ''."""
189-
drive, root, _ = self.pathmod.splitroot(str(self))
194+
drive, root, _ = self.pathmod.splitroot(self._raw_path)
190195
return drive + root
191196

192197
@property
193198
def name(self):
194199
"""The final path component, if any."""
195-
return self.pathmod.basename(str(self))
200+
return self.pathmod.basename(self._raw_path)
196201

197202
@property
198203
def suffix(self):
@@ -236,7 +241,7 @@ def with_name(self, name):
236241
dirname = self.pathmod.dirname
237242
if dirname(name):
238243
raise ValueError(f"Invalid name {name!r}")
239-
return self.with_segments(dirname(str(self)), name)
244+
return self.with_segments(dirname(self._raw_path), name)
240245

241246
def with_stem(self, stem):
242247
"""Return a new path with the stem changed."""
@@ -266,18 +271,20 @@ def relative_to(self, other, *, walk_up=False):
266271
other = self.with_segments(other)
267272
anchor0, parts0 = self._stack
268273
anchor1, parts1 = other._stack
274+
if isinstance(anchor0, str) != isinstance(anchor1, str):
275+
raise TypeError(f"{self._raw_path!r} and {other._raw_path!r} have different types")
269276
if anchor0 != anchor1:
270-
raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
277+
raise ValueError(f"{self._raw_path!r} and {other._raw_path!r} have different anchors")
271278
while parts0 and parts1 and parts0[-1] == parts1[-1]:
272279
parts0.pop()
273280
parts1.pop()
274281
for part in parts1:
275282
if not part or part == '.':
276283
pass
277284
elif not walk_up:
278-
raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
285+
raise ValueError(f"{self._raw_path!r} is not in the subpath of {other._raw_path!r}")
279286
elif part == '..':
280-
raise ValueError(f"'..' segment in {str(other)!r} cannot be walked")
287+
raise ValueError(f"'..' segment in {other._raw_path!r} cannot be walked")
281288
else:
282289
parts0.append('..')
283290
return self.with_segments('', *reversed(parts0))
@@ -289,6 +296,8 @@ def is_relative_to(self, other):
289296
other = self.with_segments(other)
290297
anchor0, parts0 = self._stack
291298
anchor1, parts1 = other._stack
299+
if isinstance(anchor0, str) != isinstance(anchor1, str):
300+
raise TypeError(f"{self._raw_path!r} and {other._raw_path!r} have different types")
292301
if anchor0 != anchor1:
293302
return False
294303
while parts0 and parts1 and parts0[-1] == parts1[-1]:
@@ -336,7 +345,7 @@ def _stack(self):
336345
*parts* is a reversed list of parts following the anchor.
337346
"""
338347
split = self.pathmod.split
339-
path = str(self)
348+
path = self._raw_path
340349
parent, name = split(path)
341350
names = []
342351
while path != parent:
@@ -348,7 +357,7 @@ def _stack(self):
348357
@property
349358
def parent(self):
350359
"""The logical parent of the path."""
351-
path = str(self)
360+
path = self._raw_path
352361
parent = self.pathmod.dirname(path)
353362
if path != parent:
354363
parent = self.with_segments(parent)
@@ -360,7 +369,7 @@ def parent(self):
360369
def parents(self):
361370
"""A sequence of this path's logical parents."""
362371
dirname = self.pathmod.dirname
363-
path = str(self)
372+
path = self._raw_path
364373
parent = dirname(path)
365374
parents = []
366375
while path != parent:
@@ -379,7 +388,7 @@ def is_absolute(self):
379388
return True
380389
return False
381390
else:
382-
return self.pathmod.isabs(str(self))
391+
return self.pathmod.isabs(self._raw_path)
383392

384393
def is_reserved(self):
385394
"""Return True if the path contains one of the special names reserved
@@ -894,7 +903,7 @@ def resolve(self, strict=False):
894903
# encountered during resolution.
895904
link_count += 1
896905
if link_count >= self._max_symlinks:
897-
raise OSError(ELOOP, "Too many symbolic links in path", str(self))
906+
raise OSError(ELOOP, "Too many symbolic links in path", self._raw_path)
898907
target_root, target_parts = path.readlink()._stack
899908
# If the symlink target is absolute (like '/etc/hosts'), set the current
900909
# path to its uppermost parent (like '/').
@@ -908,7 +917,7 @@ def resolve(self, strict=False):
908917
parts.extend(target_parts)
909918
continue
910919
elif parts and not S_ISDIR(st.st_mode):
911-
raise NotADirectoryError(ENOTDIR, "Not a directory", str(self))
920+
raise NotADirectoryError(ENOTDIR, "Not a directory", self._raw_path)
912921
except OSError:
913922
if strict:
914923
raise

0 commit comments

Comments
 (0)