Skip to content

Commit cdca0ce

Browse files
authored
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.
1 parent 5c7bd0e commit cdca0ce

File tree

2 files changed

+42
-15
lines changed

2 files changed

+42
-15
lines changed

Lib/pathlib/__init__.py

+17-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import posixpath
1212
import sys
1313
import warnings
14+
from itertools import chain
1415
from _collections_abc import Sequence
1516

1617
try:
@@ -254,10 +255,19 @@ def relative_to(self, other, /, *_deprecated, walk_up=False):
254255
"scheduled for removal in Python 3.14")
255256
warnings.warn(msg, DeprecationWarning, stacklevel=2)
256257
other = self.with_segments(other, *_deprecated)
257-
path = _abc.PurePathBase.relative_to(self, other, walk_up=walk_up)
258-
path._drv = path._root = ''
259-
path._tail_cached = path._raw_paths.copy()
260-
return path
258+
elif not isinstance(other, PurePath):
259+
other = self.with_segments(other)
260+
for step, path in enumerate(chain([other], other.parents)):
261+
if path == self or path in self.parents:
262+
break
263+
elif not walk_up:
264+
raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
265+
elif path.name == '..':
266+
raise ValueError(f"'..' segment in {str(other)!r} cannot be walked")
267+
else:
268+
raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
269+
parts = ['..'] * step + self._tail[len(path._tail):]
270+
return self._from_parsed_parts('', '', parts)
261271

262272
def is_relative_to(self, other, /, *_deprecated):
263273
"""Return True if the path is relative to another path or False.
@@ -268,7 +278,9 @@ def is_relative_to(self, other, /, *_deprecated):
268278
"scheduled for removal in Python 3.14")
269279
warnings.warn(msg, DeprecationWarning, stacklevel=2)
270280
other = self.with_segments(other, *_deprecated)
271-
return _abc.PurePathBase.is_relative_to(self, other)
281+
elif not isinstance(other, PurePath):
282+
other = self.with_segments(other)
283+
return other == self or other in self.parents
272284

273285
def as_uri(self):
274286
"""Return the path as a URI."""

Lib/pathlib/_abc.py

+25-10
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import posixpath
44
import sys
55
from errno import ENOENT, ENOTDIR, EBADF, ELOOP, EINVAL
6-
from itertools import chain
76
from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
87

98
#
@@ -358,24 +357,40 @@ def relative_to(self, other, *, walk_up=False):
358357
"""
359358
if not isinstance(other, PurePathBase):
360359
other = self.with_segments(other)
361-
for step, path in enumerate(chain([other], other.parents)):
362-
if path == self or path in self.parents:
363-
break
360+
anchor0, parts0 = self._stack
361+
anchor1, parts1 = other._stack
362+
if anchor0 != anchor1:
363+
raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
364+
while parts0 and parts1 and parts0[-1] == parts1[-1]:
365+
parts0.pop()
366+
parts1.pop()
367+
for part in parts1:
368+
if not part or part == '.':
369+
pass
364370
elif not walk_up:
365371
raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
366-
elif path.name == '..':
372+
elif part == '..':
367373
raise ValueError(f"'..' segment in {str(other)!r} cannot be walked")
368-
else:
369-
raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
370-
parts = ['..'] * step + self._tail[len(path._tail):]
371-
return self.with_segments(*parts)
374+
else:
375+
parts0.append('..')
376+
return self.with_segments('', *reversed(parts0))
372377

373378
def is_relative_to(self, other):
374379
"""Return True if the path is relative to another path or False.
375380
"""
376381
if not isinstance(other, PurePathBase):
377382
other = self.with_segments(other)
378-
return other == self or other in self.parents
383+
anchor0, parts0 = self._stack
384+
anchor1, parts1 = other._stack
385+
if anchor0 != anchor1:
386+
return False
387+
while parts0 and parts1 and parts0[-1] == parts1[-1]:
388+
parts0.pop()
389+
parts1.pop()
390+
for part in parts1:
391+
if part and part != '.':
392+
return False
393+
return True
379394

380395
@property
381396
def parts(self):

0 commit comments

Comments
 (0)