From ae234fbc5ce045066448f2f0cda2f1c3c7ddebea Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Fri, 25 Nov 2022 19:15:57 +0000 Subject: [PATCH] gh-99029: Fix handling of `PureWindowsPath('C:\').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. --- Lib/pathlib.py | 56 +++++-------------- Lib/test/test_pathlib.py | 14 ++--- ...2-11-02-23-47-07.gh-issue-99029.7uCiIB.rst | 2 + 3 files changed, 20 insertions(+), 52 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-11-02-23-47-07.gh-issue-99029.7uCiIB.rst diff --git a/Lib/pathlib.py b/Lib/pathlib.py index bc57ae60e72..f31eb301036 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -632,57 +632,27 @@ class PurePath(object): 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 94401e5429c..1d01d3cbd91 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1183,10 +1183,6 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase): 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 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase): 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 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase): 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 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase): 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 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase): 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 00000000000..0bfba5e1e32 --- /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.