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]

Reply via email to