diff options
author | Barney Gale <barney.gale@gmail.com> | 2024-01-09 23:04:14 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-09 23:04:14 (GMT) |
commit | cdca0ce0ad47604b7007229415817a7a152f7f9a (patch) | |
tree | 843651d436302f0e1fba686623e86bfb48dc51f5 | |
parent | 5c7bd0e39839b27bc524e1790fe4936d987f384a (diff) | |
download | cpython-cdca0ce0ad47604b7007229415817a7a152f7f9a.zip cpython-cdca0ce0ad47604b7007229415817a7a152f7f9a.tar.gz cpython-cdca0ce0ad47604b7007229415817a7a152f7f9a.tar.bz2 |
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.
-rw-r--r-- | Lib/pathlib/__init__.py | 22 | ||||
-rw-r--r-- | Lib/pathlib/_abc.py | 35 |
2 files changed, 42 insertions, 15 deletions
diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 26e14b3..ccdd9c3 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -11,6 +11,7 @@ import os import posixpath import sys import warnings +from itertools import chain from _collections_abc import Sequence try: @@ -254,10 +255,19 @@ class PurePath(_abc.PurePathBase): "scheduled for removal in Python 3.14") warnings.warn(msg, DeprecationWarning, stacklevel=2) other = self.with_segments(other, *_deprecated) - path = _abc.PurePathBase.relative_to(self, other, walk_up=walk_up) - path._drv = path._root = '' - path._tail_cached = path._raw_paths.copy() - return path + elif not isinstance(other, PurePath): + other = self.with_segments(other) + for step, path in enumerate(chain([other], other.parents)): + 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): """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") warnings.warn(msg, DeprecationWarning, stacklevel=2) 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): """Return the path as a URI.""" diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index c16beca..5caad3c 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -3,7 +3,6 @@ import ntpath import posixpath import sys 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 # @@ -358,24 +357,40 @@ class PurePathBase: """ if not isinstance(other, PurePathBase): other = self.with_segments(other) - for step, path in enumerate(chain([other], other.parents)): - if path == self or path in self.parents: - break + anchor0, parts0 = self._stack + anchor1, parts1 = other._stack + 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: 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") - else: - raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors") - parts = ['..'] * step + self._tail[len(path._tail):] - return self.with_segments(*parts) + else: + parts0.append('..') + return self.with_segments('', *reversed(parts0)) def is_relative_to(self, other): """Return True if the path is relative to another path or False. """ if not isinstance(other, PurePathBase): 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 def parts(self): |