Skip to content

Commit ae234fb

Browse files
authored
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.
1 parent 7d2dcc5 commit ae234fb

File tree

3 files changed

+20
-52
lines changed

3 files changed

+20
-52
lines changed

Lib/pathlib.py

+13-43
Original file line numberDiff line numberDiff line change
@@ -632,57 +632,27 @@ def relative_to(self, *other, walk_up=False):
632632
The *walk_up* parameter controls whether `..` may be used to resolve
633633
the path.
634634
"""
635-
# For the purpose of this method, drive and root are considered
636-
# separate parts, i.e.:
637-
# Path('c:/').relative_to('c:') gives Path('/')
638-
# Path('c:/').relative_to('/') raise ValueError
639635
if not other:
640636
raise TypeError("need at least one argument")
641-
parts = self._parts
642-
drv = self._drv
643-
root = self._root
644-
if root:
645-
abs_parts = [drv, root] + parts[1:]
646-
else:
647-
abs_parts = parts
648-
other_drv, other_root, other_parts = self._parse_args(other)
649-
if other_root:
650-
other_abs_parts = [other_drv, other_root] + other_parts[1:]
651-
else:
652-
other_abs_parts = other_parts
653-
num_parts = len(other_abs_parts)
654-
casefold = self._flavour.casefold_parts
655-
num_common_parts = 0
656-
for part, other_part in zip(casefold(abs_parts), casefold(other_abs_parts)):
657-
if part != other_part:
637+
path_cls = type(self)
638+
other = path_cls(*other)
639+
for step, path in enumerate([other] + list(other.parents)):
640+
if self.is_relative_to(path):
658641
break
659-
num_common_parts += 1
660-
if walk_up:
661-
failure = root != other_root
662-
if drv or other_drv:
663-
failure = casefold([drv]) != casefold([other_drv]) or (failure and num_parts > 1)
664-
error_message = "{!r} is not on the same drive as {!r}"
665-
up_parts = (num_parts-num_common_parts)*['..']
666642
else:
667-
failure = (root or drv) if num_parts == 0 else num_common_parts != num_parts
668-
error_message = "{!r} is not in the subpath of {!r}"
669-
up_parts = []
670-
error_message += " OR one path is relative and the other is absolute."
671-
if failure:
672-
formatted = self._format_parsed_parts(other_drv, other_root, other_parts)
673-
raise ValueError(error_message.format(str(self), str(formatted)))
674-
path_parts = up_parts + abs_parts[num_common_parts:]
675-
new_root = root if num_common_parts == 1 else ''
676-
return self._from_parsed_parts('', new_root, path_parts)
643+
raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
644+
if step and not walk_up:
645+
raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
646+
parts = ('..',) * step + self.parts[len(path.parts):]
647+
return path_cls(*parts)
677648

678649
def is_relative_to(self, *other):
679650
"""Return True if the path is relative to another path or False.
680651
"""
681-
try:
682-
self.relative_to(*other)
683-
return True
684-
except ValueError:
685-
return False
652+
if not other:
653+
raise TypeError("need at least one argument")
654+
other = type(self)(*other)
655+
return other == self or other in self.parents
686656

687657
@property
688658
def parts(self):

Lib/test/test_pathlib.py

+5-9
Original file line numberDiff line numberDiff line change
@@ -1183,21 +1183,13 @@ def test_relative_to(self):
11831183
self.assertRaises(ValueError, p.relative_to, P('/Foo'), walk_up=True)
11841184
self.assertRaises(ValueError, p.relative_to, P('C:/Foo'), walk_up=True)
11851185
p = P('C:/Foo/Bar')
1186-
self.assertEqual(p.relative_to(P('c:')), P('/Foo/Bar'))
1187-
self.assertEqual(p.relative_to('c:'), P('/Foo/Bar'))
1188-
self.assertEqual(str(p.relative_to(P('c:'))), '\\Foo\\Bar')
1189-
self.assertEqual(str(p.relative_to('c:')), '\\Foo\\Bar')
11901186
self.assertEqual(p.relative_to(P('c:/')), P('Foo/Bar'))
11911187
self.assertEqual(p.relative_to('c:/'), P('Foo/Bar'))
11921188
self.assertEqual(p.relative_to(P('c:/foO')), P('Bar'))
11931189
self.assertEqual(p.relative_to('c:/foO'), P('Bar'))
11941190
self.assertEqual(p.relative_to('c:/foO/'), P('Bar'))
11951191
self.assertEqual(p.relative_to(P('c:/foO/baR')), P())
11961192
self.assertEqual(p.relative_to('c:/foO/baR'), P())
1197-
self.assertEqual(p.relative_to(P('c:'), walk_up=True), P('/Foo/Bar'))
1198-
self.assertEqual(p.relative_to('c:', walk_up=True), P('/Foo/Bar'))
1199-
self.assertEqual(str(p.relative_to(P('c:'), walk_up=True)), '\\Foo\\Bar')
1200-
self.assertEqual(str(p.relative_to('c:', walk_up=True)), '\\Foo\\Bar')
12011193
self.assertEqual(p.relative_to(P('c:/'), walk_up=True), P('Foo/Bar'))
12021194
self.assertEqual(p.relative_to('c:/', walk_up=True), P('Foo/Bar'))
12031195
self.assertEqual(p.relative_to(P('c:/foO'), walk_up=True), P('Bar'))
@@ -1209,6 +1201,8 @@ def test_relative_to(self):
12091201
self.assertEqual(p.relative_to('C:/Foo/Bar/Baz', walk_up=True), P('..'))
12101202
self.assertEqual(p.relative_to('C:/Foo/Baz', walk_up=True), P('../Bar'))
12111203
# Unrelated paths.
1204+
self.assertRaises(ValueError, p.relative_to, 'c:')
1205+
self.assertRaises(ValueError, p.relative_to, P('c:'))
12121206
self.assertRaises(ValueError, p.relative_to, P('C:/Baz'))
12131207
self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Bar/Baz'))
12141208
self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Baz'))
@@ -1218,6 +1212,8 @@ def test_relative_to(self):
12181212
self.assertRaises(ValueError, p.relative_to, P('/'))
12191213
self.assertRaises(ValueError, p.relative_to, P('/Foo'))
12201214
self.assertRaises(ValueError, p.relative_to, P('//C/Foo'))
1215+
self.assertRaises(ValueError, p.relative_to, 'c:', walk_up=True)
1216+
self.assertRaises(ValueError, p.relative_to, P('c:'), walk_up=True)
12211217
self.assertRaises(ValueError, p.relative_to, P('C:Foo'), walk_up=True)
12221218
self.assertRaises(ValueError, p.relative_to, P('d:'), walk_up=True)
12231219
self.assertRaises(ValueError, p.relative_to, P('d:/'), walk_up=True)
@@ -1275,13 +1271,13 @@ def test_is_relative_to(self):
12751271
self.assertFalse(p.is_relative_to(P('C:Foo/Bar/Baz')))
12761272
self.assertFalse(p.is_relative_to(P('C:Foo/Baz')))
12771273
p = P('C:/Foo/Bar')
1278-
self.assertTrue(p.is_relative_to('c:'))
12791274
self.assertTrue(p.is_relative_to(P('c:/')))
12801275
self.assertTrue(p.is_relative_to(P('c:/foO')))
12811276
self.assertTrue(p.is_relative_to('c:/foO/'))
12821277
self.assertTrue(p.is_relative_to(P('c:/foO/baR')))
12831278
self.assertTrue(p.is_relative_to('c:/foO/baR'))
12841279
# Unrelated paths.
1280+
self.assertFalse(p.is_relative_to('c:'))
12851281
self.assertFalse(p.is_relative_to(P('C:/Baz')))
12861282
self.assertFalse(p.is_relative_to(P('C:/Foo/Bar/Baz')))
12871283
self.assertFalse(p.is_relative_to(P('C:/Foo/Baz')))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:meth:`pathlib.PurePath.relative_to()` now treats naked Windows drive paths
2+
as relative. This brings its behaviour in line with other parts of pathlib.

0 commit comments

Comments
 (0)