]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
pathlib ABCs: restore `relative_to()` and `is_relative_to()` (#138853)
authorBarney Gale <barney.gale@gmail.com>
Fri, 10 Oct 2025 18:08:55 +0000 (19:08 +0100)
committerGitHub <noreply@github.com>
Fri, 10 Oct 2025 18:08:55 +0000 (19:08 +0100)
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.

Lib/pathlib/__init__.py
Lib/pathlib/types.py
Lib/test/test_pathlib/test_join.py

index 8a892102cc00ea4915b25e77aba509ccadd9b85a..6c07cd9ab010ad57bf5670e3900b35aca9ac3b46 100644 (file)
@@ -490,16 +490,19 @@ class PurePath:
         """
         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):
index fea0dd305fe2a3aa8ddfaa7da6d2e9e368da0e1c..f21ce0774548f8705c84ae058c52d3d3ac793794 100644 (file)
@@ -234,6 +234,33 @@ class _JoinablePath(ABC):
             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
index f1a24204b4c30aab772683c5d00e6e99abd75cd3..2f4e79345f36527d09ef611529879ff8e296e6d7 100644 (file)
@@ -354,6 +354,61 @@ class JoinTestBase:
         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