https://github.com/python/cpython/commit/49aaee7978c54211967392678072accc403d15f2
commit: 49aaee7978c54211967392678072accc403d15f2
branch: main
author: Barney Gale <[email protected]>
committer: barneygale <[email protected]>
date: 2025-10-10T19:08:55+01:00
summary:
pathlib ABCs: restore `relative_to()` and `is_relative_to()` (#138853)
Restore `JoinablePath.[is_]relative_to()`, which were deleted in
ef63cca494571f50906baae1d176469a3dcf8838. These methods are too useful to
forgo. Restore old tests, and add new tests covering path classes with
non-overridden `__eq__()` and `__hash__()`.
Slightly simplify `PurePath.relative_to()` while we're in the area.
No change to public APIs, because the pathlib ABCs are still private.
files:
M Lib/pathlib/__init__.py
M Lib/pathlib/types.py
M Lib/test/test_pathlib/test_join.py
diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py
index 8a892102cc00ea..6c07cd9ab010ad 100644
--- a/Lib/pathlib/__init__.py
+++ b/Lib/pathlib/__init__.py
@@ -490,16 +490,19 @@ def relative_to(self, other, *, walk_up=False):
"""
if not hasattr(other, 'with_segments'):
other = self.with_segments(other)
- for step, path in enumerate(chain([other], other.parents)):
+ parts = []
+ for path in 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:
+ parts.append('..')
else:
raise ValueError(f"{str(self)!r} and {str(other)!r} have different
anchors")
- parts = ['..'] * step + self._tail[len(path._tail):]
+ parts.extend(self._tail[len(path._tail):])
return self._from_parsed_parts('', '', parts)
def is_relative_to(self, other):
diff --git a/Lib/pathlib/types.py b/Lib/pathlib/types.py
index fea0dd305fe2a3..f21ce0774548f8 100644
--- a/Lib/pathlib/types.py
+++ b/Lib/pathlib/types.py
@@ -234,6 +234,33 @@ def parents(self):
parent = split(path)[0]
return tuple(parents)
+ def relative_to(self, other, *, walk_up=False):
+ """Return the relative path to another path identified by the passed
+ arguments. If the operation is not possible (because this is not
+ related to the other path), raise ValueError.
+
+ The *walk_up* parameter controls whether `..` may be used to resolve
+ the path.
+ """
+ parts = []
+ for path in (other,) + other.parents:
+ if self.is_relative_to(path):
+ break
+ elif not walk_up:
+ raise ValueError(f"{self!r} is not in the subpath of
{other!r}")
+ elif path.name == '..':
+ raise ValueError(f"'..' segment in {other!r} cannot be walked")
+ else:
+ parts.append('..')
+ else:
+ raise ValueError(f"{self!r} and {other!r} have different anchors")
+ return self.with_segments(*parts, *self.parts[len(path.parts):])
+
+ def is_relative_to(self, other):
+ """Return True if the path is relative to another path or False.
+ """
+ return other == self or other in self.parents
+
def full_match(self, pattern):
"""
Return True if this path matches the given glob-style pattern. The
diff --git a/Lib/test/test_pathlib/test_join.py
b/Lib/test/test_pathlib/test_join.py
index f1a24204b4c30a..2f4e79345f3652 100644
--- a/Lib/test/test_pathlib/test_join.py
+++ b/Lib/test/test_pathlib/test_join.py
@@ -354,6 +354,61 @@ def test_with_suffix(self):
self.assertRaises(ValueError, P('a/b').with_suffix, '.d/.')
self.assertRaises(TypeError, P('a/b').with_suffix, None)
+ def test_relative_to(self):
+ P = self.cls
+ p = P('a/b')
+ self.assertEqual(p.relative_to(P('')), P('a', 'b'))
+ self.assertEqual(p.relative_to(P('a')), P('b'))
+ self.assertEqual(p.relative_to(P('a/b')), P(''))
+ self.assertEqual(p.relative_to(P(''), walk_up=True), P('a', 'b'))
+ self.assertEqual(p.relative_to(P('a'), walk_up=True), P('b'))
+ self.assertEqual(p.relative_to(P('a/b'), walk_up=True), P(''))
+ self.assertEqual(p.relative_to(P('a/c'), walk_up=True), P('..', 'b'))
+ self.assertEqual(p.relative_to(P('a/b/c'), walk_up=True), P('..'))
+ self.assertEqual(p.relative_to(P('c'), walk_up=True), P('..', 'a',
'b'))
+ self.assertRaises(ValueError, p.relative_to, P('c'))
+ self.assertRaises(ValueError, p.relative_to, P('a/b/c'))
+ self.assertRaises(ValueError, p.relative_to, P('a/c'))
+ self.assertRaises(ValueError, p.relative_to, P('/a'))
+ self.assertRaises(ValueError, p.relative_to, P('../a'))
+ self.assertRaises(ValueError, p.relative_to, P('a/..'))
+ self.assertRaises(ValueError, p.relative_to, P('/a/..'))
+ self.assertRaises(ValueError, p.relative_to, P('/'), walk_up=True)
+ self.assertRaises(ValueError, p.relative_to, P('/a'), walk_up=True)
+ self.assertRaises(ValueError, p.relative_to, P('../a'), walk_up=True)
+ self.assertRaises(ValueError, p.relative_to, P('a/..'), walk_up=True)
+ self.assertRaises(ValueError, p.relative_to, P('/a/..'), walk_up=True)
+ class Q(self.cls):
+ __eq__ = object.__eq__
+ __hash__ = object.__hash__
+ q = Q('a/b')
+ self.assertTrue(q.relative_to(q))
+ self.assertRaises(ValueError, q.relative_to, Q(''))
+ self.assertRaises(ValueError, q.relative_to, Q('a'))
+ self.assertRaises(ValueError, q.relative_to, Q('a'), walk_up=True)
+ self.assertRaises(ValueError, q.relative_to, Q('a/b'))
+ self.assertRaises(ValueError, q.relative_to, Q('c'))
+
+ def test_is_relative_to(self):
+ P = self.cls
+ p = P('a/b')
+ self.assertTrue(p.is_relative_to(P('')))
+ self.assertTrue(p.is_relative_to(P('a')))
+ self.assertTrue(p.is_relative_to(P('a/b')))
+ self.assertFalse(p.is_relative_to(P('c')))
+ self.assertFalse(p.is_relative_to(P('a/b/c')))
+ self.assertFalse(p.is_relative_to(P('a/c')))
+ self.assertFalse(p.is_relative_to(P('/a')))
+ class Q(self.cls):
+ __eq__ = object.__eq__
+ __hash__ = object.__hash__
+ q = Q('a/b')
+ self.assertTrue(q.is_relative_to(q))
+ self.assertFalse(q.is_relative_to(Q('')))
+ self.assertFalse(q.is_relative_to(Q('a')))
+ self.assertFalse(q.is_relative_to(Q('a/b')))
+ self.assertFalse(q.is_relative_to(Q('c')))
+
class LexicalPathJoinTest(JoinTestBase, unittest.TestCase):
cls = LexicalPath
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]