From cdca0ce0ad47604b7007229415817a7a152f7f9a Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Tue, 9 Jan 2024 23:04:14 +0000 Subject: [PATCH] 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. --- Lib/pathlib/__init__.py | 22 +++++++++++++++++----- 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 26e14b3f7b2005..ccdd9c3d547c8e 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -11,6 +11,7 @@ import posixpath import sys import warnings +from itertools import chain from _collections_abc import Sequence try: @@ -254,10 +255,19 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): "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 @@ def is_relative_to(self, other, /, *_deprecated): "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 c16beca71aa7c7..5caad3c0502399 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -3,7 +3,6 @@ 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 @@ def relative_to(self, other, *, walk_up=False): """ 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):