GH-113528: Deoptimise pathlib._abc.PurePathBase.relative_to() (again) (#113882)

Restore full battle-tested implementations of `PurePath.[is_]relative_to()`. These were recently split up in 3375dfe and a15a773.

In `PurePathBase`, add entirely new implementations based on `_stack`, which itself calls `pathmod.split()` repeatedly to disassemble a path. These new implementations preserve features like trailing slashes where possible, while still observing that a `..` segment cannot be added to traverse an empty or `.` segment in *walk_up* mode. They do not rely on `parents` nor `__eq__()`, nor do they spin up temporary path objects.

Unfortunately calling `pathmod.relpath()` isn't an option, as it calls `abspath()` and in turn `os.getcwd()`, which is impure.
This commit is contained in:
Barney Gale 2024-01-09 23:04:14 +00:00 committed by GitHub
parent 5c7bd0e398
commit cdca0ce0ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 42 additions and 15 deletions

View file

@ -11,6 +11,7 @@ import os
import posixpath import posixpath
import sys import sys
import warnings import warnings
from itertools import chain
from _collections_abc import Sequence from _collections_abc import Sequence
try: try:
@ -254,10 +255,19 @@ class PurePath(_abc.PurePathBase):
"scheduled for removal in Python 3.14") "scheduled for removal in Python 3.14")
warnings.warn(msg, DeprecationWarning, stacklevel=2) warnings.warn(msg, DeprecationWarning, stacklevel=2)
other = self.with_segments(other, *_deprecated) other = self.with_segments(other, *_deprecated)
path = _abc.PurePathBase.relative_to(self, other, walk_up=walk_up) elif not isinstance(other, PurePath):
path._drv = path._root = '' other = self.with_segments(other)
path._tail_cached = path._raw_paths.copy() for step, path in enumerate(chain([other], other.parents)):
return path if path == self or path in self.parents:
break
elif not walk_up:
raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
elif path.name == '..':
raise ValueError(f"'..' segment in {str(other)!r} cannot be walked")
else:
raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
parts = ['..'] * step + self._tail[len(path._tail):]
return self._from_parsed_parts('', '', parts)
def is_relative_to(self, other, /, *_deprecated): def is_relative_to(self, other, /, *_deprecated):
"""Return True if the path is relative to another path or False. """Return True if the path is relative to another path or False.
@ -268,7 +278,9 @@ class PurePath(_abc.PurePathBase):
"scheduled for removal in Python 3.14") "scheduled for removal in Python 3.14")
warnings.warn(msg, DeprecationWarning, stacklevel=2) warnings.warn(msg, DeprecationWarning, stacklevel=2)
other = self.with_segments(other, *_deprecated) other = self.with_segments(other, *_deprecated)
return _abc.PurePathBase.is_relative_to(self, other) elif not isinstance(other, PurePath):
other = self.with_segments(other)
return other == self or other in self.parents
def as_uri(self): def as_uri(self):
"""Return the path as a URI.""" """Return the path as a URI."""

View file

@ -3,7 +3,6 @@ import ntpath
import posixpath import posixpath
import sys import sys
from errno import ENOENT, ENOTDIR, EBADF, ELOOP, EINVAL from errno import ENOENT, ENOTDIR, EBADF, ELOOP, EINVAL
from itertools import chain
from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
# #
@ -358,24 +357,40 @@ class PurePathBase:
""" """
if not isinstance(other, PurePathBase): if not isinstance(other, PurePathBase):
other = self.with_segments(other) other = self.with_segments(other)
for step, path in enumerate(chain([other], other.parents)): anchor0, parts0 = self._stack
if path == self or path in self.parents: anchor1, parts1 = other._stack
break if anchor0 != anchor1:
raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
while parts0 and parts1 and parts0[-1] == parts1[-1]:
parts0.pop()
parts1.pop()
for part in parts1:
if not part or part == '.':
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"{str(self)!r} is not in the subpath of {str(other)!r}")
elif path.name == '..': elif part == '..':
raise ValueError(f"'..' segment in {str(other)!r} cannot be walked") raise ValueError(f"'..' segment in {str(other)!r} cannot be walked")
else: else:
raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors") parts0.append('..')
parts = ['..'] * step + self._tail[len(path._tail):] return self.with_segments('', *reversed(parts0))
return self.with_segments(*parts)
def is_relative_to(self, other): def is_relative_to(self, other):
"""Return True if the path is relative to another path or False. """Return True if the path is relative to another path or False.
""" """
if not isinstance(other, PurePathBase): if not isinstance(other, PurePathBase):
other = self.with_segments(other) other = self.with_segments(other)
return other == self or other in self.parents anchor0, parts0 = self._stack
anchor1, parts1 = other._stack
if anchor0 != anchor1:
return False
while parts0 and parts1 and parts0[-1] == parts1[-1]:
parts0.pop()
parts1.pop()
for part in parts1:
if part and part != '.':
return False
return True
@property @property
def parts(self): def parts(self):