[Python-checkins] gh-99029: Fix handling of `PureWindowsPath('C:\<blah>').relative_to('C:')` (GH-99031)

brettcannon webhook-mailer at python.org
Fri Nov 25 14:16:02 EST 2022


https://github.com/python/cpython/commit/ae234fbc5ce045066448f2f0cda2f1c3c7ddebea
commit: ae234fbc5ce045066448f2f0cda2f1c3c7ddebea
branch: main
author: Barney Gale <barney.gale at gmail.com>
committer: brettcannon <brett at python.org>
date: 2022-11-25T11:15:57-08:00
summary:

gh-99029: Fix handling of `PureWindowsPath('C:\<blah>').relative_to('C:')` (GH-99031)

`relative_to()` now treats naked drive paths as relative. This brings its
behaviour in line with other parts of pathlib, and with `ntpath.relpath()`,
and so allows us to factor out the pathlib-specific implementation.

files:
A Misc/NEWS.d/next/Library/2022-11-02-23-47-07.gh-issue-99029.7uCiIB.rst
M Lib/pathlib.py
M Lib/test/test_pathlib.py

diff --git a/Lib/pathlib.py b/Lib/pathlib.py
index bc57ae60e725..f31eb3010368 100644
--- a/Lib/pathlib.py
+++ b/Lib/pathlib.py
@@ -632,57 +632,27 @@ def relative_to(self, *other, walk_up=False):
         The *walk_up* parameter controls whether `..` may be used to resolve
         the path.
         """
-        # For the purpose of this method, drive and root are considered
-        # separate parts, i.e.:
-        #   Path('c:/').relative_to('c:')  gives Path('/')
-        #   Path('c:/').relative_to('/')   raise ValueError
         if not other:
             raise TypeError("need at least one argument")
-        parts = self._parts
-        drv = self._drv
-        root = self._root
-        if root:
-            abs_parts = [drv, root] + parts[1:]
-        else:
-            abs_parts = parts
-        other_drv, other_root, other_parts = self._parse_args(other)
-        if other_root:
-            other_abs_parts = [other_drv, other_root] + other_parts[1:]
-        else:
-            other_abs_parts = other_parts
-        num_parts = len(other_abs_parts)
-        casefold = self._flavour.casefold_parts
-        num_common_parts = 0
-        for part, other_part in zip(casefold(abs_parts), casefold(other_abs_parts)):
-            if part != other_part:
+        path_cls = type(self)
+        other = path_cls(*other)
+        for step, path in enumerate([other] + list(other.parents)):
+            if self.is_relative_to(path):
                 break
-            num_common_parts += 1
-        if walk_up:
-            failure = root != other_root
-            if drv or other_drv:
-                failure = casefold([drv]) != casefold([other_drv]) or (failure and num_parts > 1)
-            error_message = "{!r} is not on the same drive as {!r}"
-            up_parts = (num_parts-num_common_parts)*['..']
         else:
-            failure = (root or drv) if num_parts == 0 else num_common_parts != num_parts
-            error_message = "{!r} is not in the subpath of {!r}"
-            up_parts = []
-        error_message += " OR one path is relative and the other is absolute."
-        if failure:
-            formatted = self._format_parsed_parts(other_drv, other_root, other_parts)
-            raise ValueError(error_message.format(str(self), str(formatted)))
-        path_parts = up_parts + abs_parts[num_common_parts:]
-        new_root = root if num_common_parts == 1 else ''
-        return self._from_parsed_parts('', new_root, path_parts)
+            raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
+        if step and not walk_up:
+            raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
+        parts = ('..',) * step + self.parts[len(path.parts):]
+        return path_cls(*parts)
 
     def is_relative_to(self, *other):
         """Return True if the path is relative to another path or False.
         """
-        try:
-            self.relative_to(*other)
-            return True
-        except ValueError:
-            return False
+        if not other:
+            raise TypeError("need at least one argument")
+        other = type(self)(*other)
+        return other == self or other in self.parents
 
     @property
     def parts(self):
diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py
index 94401e5429cd..1d01d3cbd91d 100644
--- a/Lib/test/test_pathlib.py
+++ b/Lib/test/test_pathlib.py
@@ -1183,10 +1183,6 @@ def test_relative_to(self):
         self.assertRaises(ValueError, p.relative_to, P('/Foo'), walk_up=True)
         self.assertRaises(ValueError, p.relative_to, P('C:/Foo'), walk_up=True)
         p = P('C:/Foo/Bar')
-        self.assertEqual(p.relative_to(P('c:')), P('/Foo/Bar'))
-        self.assertEqual(p.relative_to('c:'), P('/Foo/Bar'))
-        self.assertEqual(str(p.relative_to(P('c:'))), '\\Foo\\Bar')
-        self.assertEqual(str(p.relative_to('c:')), '\\Foo\\Bar')
         self.assertEqual(p.relative_to(P('c:/')), P('Foo/Bar'))
         self.assertEqual(p.relative_to('c:/'), P('Foo/Bar'))
         self.assertEqual(p.relative_to(P('c:/foO')), P('Bar'))
@@ -1194,10 +1190,6 @@ def test_relative_to(self):
         self.assertEqual(p.relative_to('c:/foO/'), P('Bar'))
         self.assertEqual(p.relative_to(P('c:/foO/baR')), P())
         self.assertEqual(p.relative_to('c:/foO/baR'), P())
-        self.assertEqual(p.relative_to(P('c:'), walk_up=True), P('/Foo/Bar'))
-        self.assertEqual(p.relative_to('c:', walk_up=True), P('/Foo/Bar'))
-        self.assertEqual(str(p.relative_to(P('c:'), walk_up=True)), '\\Foo\\Bar')
-        self.assertEqual(str(p.relative_to('c:', walk_up=True)), '\\Foo\\Bar')
         self.assertEqual(p.relative_to(P('c:/'), walk_up=True), P('Foo/Bar'))
         self.assertEqual(p.relative_to('c:/', walk_up=True), P('Foo/Bar'))
         self.assertEqual(p.relative_to(P('c:/foO'), walk_up=True), P('Bar'))
@@ -1209,6 +1201,8 @@ def test_relative_to(self):
         self.assertEqual(p.relative_to('C:/Foo/Bar/Baz', walk_up=True), P('..'))
         self.assertEqual(p.relative_to('C:/Foo/Baz', walk_up=True), P('../Bar'))
         # Unrelated paths.
+        self.assertRaises(ValueError, p.relative_to, 'c:')
+        self.assertRaises(ValueError, p.relative_to, P('c:'))
         self.assertRaises(ValueError, p.relative_to, P('C:/Baz'))
         self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Bar/Baz'))
         self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Baz'))
@@ -1218,6 +1212,8 @@ def test_relative_to(self):
         self.assertRaises(ValueError, p.relative_to, P('/'))
         self.assertRaises(ValueError, p.relative_to, P('/Foo'))
         self.assertRaises(ValueError, p.relative_to, P('//C/Foo'))
+        self.assertRaises(ValueError, p.relative_to, 'c:', walk_up=True)
+        self.assertRaises(ValueError, p.relative_to, P('c:'), walk_up=True)
         self.assertRaises(ValueError, p.relative_to, P('C:Foo'), walk_up=True)
         self.assertRaises(ValueError, p.relative_to, P('d:'), walk_up=True)
         self.assertRaises(ValueError, p.relative_to, P('d:/'), walk_up=True)
@@ -1275,13 +1271,13 @@ def test_is_relative_to(self):
         self.assertFalse(p.is_relative_to(P('C:Foo/Bar/Baz')))
         self.assertFalse(p.is_relative_to(P('C:Foo/Baz')))
         p = P('C:/Foo/Bar')
-        self.assertTrue(p.is_relative_to('c:'))
         self.assertTrue(p.is_relative_to(P('c:/')))
         self.assertTrue(p.is_relative_to(P('c:/foO')))
         self.assertTrue(p.is_relative_to('c:/foO/'))
         self.assertTrue(p.is_relative_to(P('c:/foO/baR')))
         self.assertTrue(p.is_relative_to('c:/foO/baR'))
         # Unrelated paths.
+        self.assertFalse(p.is_relative_to('c:'))
         self.assertFalse(p.is_relative_to(P('C:/Baz')))
         self.assertFalse(p.is_relative_to(P('C:/Foo/Bar/Baz')))
         self.assertFalse(p.is_relative_to(P('C:/Foo/Baz')))
diff --git a/Misc/NEWS.d/next/Library/2022-11-02-23-47-07.gh-issue-99029.7uCiIB.rst b/Misc/NEWS.d/next/Library/2022-11-02-23-47-07.gh-issue-99029.7uCiIB.rst
new file mode 100644
index 000000000000..0bfba5e1e326
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-11-02-23-47-07.gh-issue-99029.7uCiIB.rst
@@ -0,0 +1,2 @@
+:meth:`pathlib.PurePath.relative_to()` now treats naked Windows drive paths
+as relative. This brings its behaviour in line with other parts of pathlib.



More information about the Python-checkins mailing list