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.
This commit is contained in:
Barney Gale 2024-01-13 08:03:21 +00:00 committed by GitHub
parent e4ff131e01
commit f20b151a1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 31 additions and 20 deletions

View File

@ -257,7 +257,9 @@ def _parse_path(cls, path):
parsed = [sys.intern(str(x)) for x in rel.split(sep) if x and x != '.'] parsed = [sys.intern(str(x)) for x in rel.split(sep) if x and x != '.']
return drv, root, parsed return drv, root, parsed
def _load_parts(self): @property
def _raw_path(self):
"""The joined but unnormalized path."""
paths = self._raw_paths paths = self._raw_paths
if len(paths) == 0: if len(paths) == 0:
path = '' path = ''
@ -265,7 +267,7 @@ def _load_parts(self):
path = paths[0] path = paths[0]
else: else:
path = self.pathmod.join(*paths) path = self.pathmod.join(*paths)
self._drv, self._root, self._tail_cached = self._parse_path(path) return path
@property @property
def drive(self): def drive(self):
@ -273,7 +275,7 @@ def drive(self):
try: try:
return self._drv return self._drv
except AttributeError: except AttributeError:
self._load_parts() self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
return self._drv return self._drv
@property @property
@ -282,7 +284,7 @@ def root(self):
try: try:
return self._root return self._root
except AttributeError: except AttributeError:
self._load_parts() self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
return self._root return self._root
@property @property
@ -290,7 +292,7 @@ def _tail(self):
try: try:
return self._tail_cached return self._tail_cached
except AttributeError: except AttributeError:
self._load_parts() self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
return self._tail_cached return self._tail_cached
@property @property

View File

@ -163,10 +163,15 @@ def with_segments(self, *pathsegments):
""" """
return type(self)(*pathsegments) return type(self)(*pathsegments)
@property
def _raw_path(self):
"""The joined but unnormalized path."""
return self.pathmod.join(*self._raw_paths)
def __str__(self): def __str__(self):
"""Return the string representation of the path, suitable for """Return the string representation of the path, suitable for
passing to system calls.""" passing to system calls."""
return self.pathmod.join(*self._raw_paths) return self._raw_path
def as_posix(self): def as_posix(self):
"""Return the string representation of the path with forward (/) """Return the string representation of the path with forward (/)
@ -176,23 +181,23 @@ def as_posix(self):
@property @property
def drive(self): def drive(self):
"""The drive prefix (letter or UNC path), if any.""" """The drive prefix (letter or UNC path), if any."""
return self.pathmod.splitdrive(str(self))[0] return self.pathmod.splitdrive(self._raw_path)[0]
@property @property
def root(self): def root(self):
"""The root of the path, if any.""" """The root of the path, if any."""
return self.pathmod.splitroot(str(self))[1] return self.pathmod.splitroot(self._raw_path)[1]
@property @property
def anchor(self): def anchor(self):
"""The concatenation of the drive and root, or ''.""" """The concatenation of the drive and root, or ''."""
drive, root, _ = self.pathmod.splitroot(str(self)) drive, root, _ = self.pathmod.splitroot(self._raw_path)
return drive + root return drive + root
@property @property
def name(self): def name(self):
"""The final path component, if any.""" """The final path component, if any."""
return self.pathmod.basename(str(self)) return self.pathmod.basename(self._raw_path)
@property @property
def suffix(self): def suffix(self):
@ -236,7 +241,7 @@ def with_name(self, name):
dirname = self.pathmod.dirname dirname = self.pathmod.dirname
if dirname(name): if dirname(name):
raise ValueError(f"Invalid name {name!r}") raise ValueError(f"Invalid name {name!r}")
return self.with_segments(dirname(str(self)), name) return self.with_segments(dirname(self._raw_path), name)
def with_stem(self, stem): def with_stem(self, stem):
"""Return a new path with the stem changed.""" """Return a new path with the stem changed."""
@ -266,8 +271,10 @@ def relative_to(self, other, *, walk_up=False):
other = self.with_segments(other) other = self.with_segments(other)
anchor0, parts0 = self._stack anchor0, parts0 = self._stack
anchor1, parts1 = other._stack anchor1, parts1 = other._stack
if isinstance(anchor0, str) != isinstance(anchor1, str):
raise TypeError(f"{self._raw_path!r} and {other._raw_path!r} have different types")
if anchor0 != anchor1: if anchor0 != anchor1:
raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors") raise ValueError(f"{self._raw_path!r} and {other._raw_path!r} have different anchors")
while parts0 and parts1 and parts0[-1] == parts1[-1]: while parts0 and parts1 and parts0[-1] == parts1[-1]:
parts0.pop() parts0.pop()
parts1.pop() parts1.pop()
@ -275,9 +282,9 @@ def relative_to(self, other, *, walk_up=False):
if not part or part == '.': if not part or part == '.':
pass pass
elif not walk_up: elif not walk_up:
raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}") raise ValueError(f"{self._raw_path!r} is not in the subpath of {other._raw_path!r}")
elif part == '..': elif part == '..':
raise ValueError(f"'..' segment in {str(other)!r} cannot be walked") raise ValueError(f"'..' segment in {other._raw_path!r} cannot be walked")
else: else:
parts0.append('..') parts0.append('..')
return self.with_segments('', *reversed(parts0)) return self.with_segments('', *reversed(parts0))
@ -289,6 +296,8 @@ def is_relative_to(self, other):
other = self.with_segments(other) other = self.with_segments(other)
anchor0, parts0 = self._stack anchor0, parts0 = self._stack
anchor1, parts1 = other._stack anchor1, parts1 = other._stack
if isinstance(anchor0, str) != isinstance(anchor1, str):
raise TypeError(f"{self._raw_path!r} and {other._raw_path!r} have different types")
if anchor0 != anchor1: if anchor0 != anchor1:
return False return False
while parts0 and parts1 and parts0[-1] == parts1[-1]: while parts0 and parts1 and parts0[-1] == parts1[-1]:
@ -336,7 +345,7 @@ def _stack(self):
*parts* is a reversed list of parts following the anchor. *parts* is a reversed list of parts following the anchor.
""" """
split = self.pathmod.split split = self.pathmod.split
path = str(self) path = self._raw_path
parent, name = split(path) parent, name = split(path)
names = [] names = []
while path != parent: while path != parent:
@ -348,7 +357,7 @@ def _stack(self):
@property @property
def parent(self): def parent(self):
"""The logical parent of the path.""" """The logical parent of the path."""
path = str(self) path = self._raw_path
parent = self.pathmod.dirname(path) parent = self.pathmod.dirname(path)
if path != parent: if path != parent:
parent = self.with_segments(parent) parent = self.with_segments(parent)
@ -360,7 +369,7 @@ def parent(self):
def parents(self): def parents(self):
"""A sequence of this path's logical parents.""" """A sequence of this path's logical parents."""
dirname = self.pathmod.dirname dirname = self.pathmod.dirname
path = str(self) path = self._raw_path
parent = dirname(path) parent = dirname(path)
parents = [] parents = []
while path != parent: while path != parent:
@ -379,7 +388,7 @@ def is_absolute(self):
return True return True
return False return False
else: else:
return self.pathmod.isabs(str(self)) return self.pathmod.isabs(self._raw_path)
def is_reserved(self): def is_reserved(self):
"""Return True if the path contains one of the special names reserved """Return True if the path contains one of the special names reserved
@ -894,7 +903,7 @@ def resolve(self, strict=False):
# encountered during resolution. # encountered during resolution.
link_count += 1 link_count += 1
if link_count >= self._max_symlinks: if link_count >= self._max_symlinks:
raise OSError(ELOOP, "Too many symbolic links in path", str(self)) raise OSError(ELOOP, "Too many symbolic links in path", self._raw_path)
target_root, target_parts = path.readlink()._stack target_root, target_parts = path.readlink()._stack
# If the symlink target is absolute (like '/etc/hosts'), set the current # If the symlink target is absolute (like '/etc/hosts'), set the current
# path to its uppermost parent (like '/'). # path to its uppermost parent (like '/').
@ -908,7 +917,7 @@ def resolve(self, strict=False):
parts.extend(target_parts) parts.extend(target_parts)
continue continue
elif parts and not S_ISDIR(st.st_mode): elif parts and not S_ISDIR(st.st_mode):
raise NotADirectoryError(ENOTDIR, "Not a directory", str(self)) raise NotADirectoryError(ENOTDIR, "Not a directory", self._raw_path)
except OSError: except OSError:
if strict: if strict:
raise raise