diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index dead49b630dcdf..a68faca98c903f 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -536,7 +536,7 @@ Pure paths provide the following methods and properties: True -.. method:: PurePath.relative_to(*other) +.. method:: PurePath.relative_to(*other, strict=True) Compute a version of this path relative to the path represented by *other*. If it's impossible, ValueError is raised:: @@ -549,9 +549,25 @@ Pure paths provide the following methods and properties: >>> p.relative_to('/usr') Traceback (most recent call last): File "", line 1, in - File "pathlib.py", line 694, in relative_to - .format(str(self), str(formatted))) + File "pathlib.py", line 940, in relative_to + raise ValueError(error_message.format(str(self), str(formatted))) ValueError: '/etc/passwd' does not start with '/usr' + >>> p.relative_to('/usr', strict=False) + PurePosixPath('../etc/passwd') + >>> p.relative_to('foo', strict=False) + Traceback (most recent call last): + File "", line 1, in + File "pathlib.py", line 940, in relative_to + raise ValueError(error_message.format(str(self), str(formatted))) + ValueError: '/etc/passwd' is not related to 'foo' + + If the path doesn't start with *other* and *strict* is ``True``, + :exc:`ValueError` is raised. If *strict* is ``False`` and the paths are + not both relative or both absolute :exc:`ValueError` is raised (on Windows + both paths must reference the same drive as well). + + .. versionadded:: 3.9 + The *strict* parameter was added. .. method:: PurePath.with_name(name) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index f98d69eb04ac31..67e4c400937b5f 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -895,10 +895,10 @@ def with_suffix(self, suffix): return self._from_parsed_parts(self._drv, self._root, self._parts[:-1] + [name]) - def relative_to(self, *other): + def relative_to(self, *other, strict=True): """Return the relative path to another path identified by the passed arguments. If the operation is not possible (because this is not - a subpath of the other path), raise ValueError. + related to the other path), raise ValueError. """ # For the purpose of this method, drive and root are considered # separate parts, i.e.: @@ -920,12 +920,26 @@ def relative_to(self, *other): to_abs_parts = to_parts n = len(to_abs_parts) cf = self._flavour.casefold_parts - if (root or drv) if n == 0 else cf(abs_parts[:n]) != cf(to_abs_parts): + common = 0 + for p, tp in zip(cf(abs_parts), cf(to_abs_parts)): + if p != tp: + break + common += 1 + if strict: + failure = (root or drv) if n == 0 else common != n + error_message = "{!r} does not start with {!r}" + up_parts = [] + else: + failure = root != to_root + if drv or to_drv: + failure = cf([drv]) != cf([to_drv]) or (failure and n > 1) + error_message = "{!r} is not related to {!r}" + up_parts = (n-common)*['..'] + if failure: formatted = self._format_parsed_parts(to_drv, to_root, to_parts) - raise ValueError("{!r} does not start with {!r}" - .format(str(self), str(formatted))) - return self._from_parsed_parts('', root if n == 1 else '', - abs_parts[n:]) + raise ValueError(error_message.format(str(self), str(formatted))) + return self._from_parsed_parts('', root if common == 1 else '', + up_parts+abs_parts[common:]) def is_relative_to(self, *other): """Return True if the path is relative to another path or False. diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 1589282886b6b8..5324f9b0e2ca10 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -613,13 +613,29 @@ def test_relative_to_common(self): self.assertEqual(p.relative_to('a/'), P('b')) self.assertEqual(p.relative_to(P('a/b')), P()) self.assertEqual(p.relative_to('a/b'), P()) + self.assertEqual(p.relative_to(P(), strict=False), P('a/b')) + self.assertEqual(p.relative_to('', strict=False), P('a/b')) + self.assertEqual(p.relative_to(P('a'), strict=False), P('b')) + self.assertEqual(p.relative_to('a', strict=False), P('b')) + self.assertEqual(p.relative_to('a/', strict=False), P('b')) + self.assertEqual(p.relative_to(P('a/b'), strict=False), P()) + self.assertEqual(p.relative_to('a/b', strict=False), P()) + self.assertEqual(p.relative_to(P('a/c'), strict=False), P('../b')) + self.assertEqual(p.relative_to('a/c', strict=False), P('../b')) + self.assertEqual(p.relative_to(P('a/b/c'), strict=False), P('..')) + self.assertEqual(p.relative_to('a/b/c', strict=False), P('..')) + self.assertEqual(p.relative_to(P('c'), strict=False), P('../a/b')) + self.assertEqual(p.relative_to('c', strict=False), P('../a/b')) # With several args. self.assertEqual(p.relative_to('a', 'b'), P()) + self.assertEqual(p.relative_to('a', 'b', strict=False), P()) # Unrelated paths. 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('/'), strict=False) + self.assertRaises(ValueError, p.relative_to, P('/a'), strict=False) p = P('/a/b') self.assertEqual(p.relative_to(P('/')), P('a/b')) self.assertEqual(p.relative_to('/'), P('a/b')) @@ -628,6 +644,19 @@ def test_relative_to_common(self): self.assertEqual(p.relative_to('/a/'), P('b')) self.assertEqual(p.relative_to(P('/a/b')), P()) self.assertEqual(p.relative_to('/a/b'), P()) + self.assertEqual(p.relative_to(P('/'), strict=False), P('a/b')) + self.assertEqual(p.relative_to('/', strict=False), P('a/b')) + self.assertEqual(p.relative_to(P('/a'), strict=False), P('b')) + self.assertEqual(p.relative_to('/a', strict=False), P('b')) + self.assertEqual(p.relative_to('/a/', strict=False), P('b')) + self.assertEqual(p.relative_to(P('/a/b'), strict=False), P()) + self.assertEqual(p.relative_to('/a/b', strict=False), P()) + self.assertEqual(p.relative_to(P('/a/c'), strict=False), P('../b')) + self.assertEqual(p.relative_to('/a/c', strict=False), P('../b')) + self.assertEqual(p.relative_to(P('/a/b/c'), strict=False), P('..')) + self.assertEqual(p.relative_to('/a/b/c', strict=False), P('..')) + self.assertEqual(p.relative_to(P('/c'), strict=False), P('../a/b')) + self.assertEqual(p.relative_to('/c', strict=False), P('../a/b')) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('/c')) self.assertRaises(ValueError, p.relative_to, P('/a/b/c')) @@ -635,6 +664,8 @@ def test_relative_to_common(self): self.assertRaises(ValueError, p.relative_to, P()) self.assertRaises(ValueError, p.relative_to, '') self.assertRaises(ValueError, p.relative_to, P('a')) + self.assertRaises(ValueError, p.relative_to, P(''), strict=False) + self.assertRaises(ValueError, p.relative_to, P('a'), strict=False) def test_is_relative_to_common(self): P = self.cls @@ -1079,6 +1110,16 @@ 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:'), strict=False), P('Foo/Bar')) + self.assertEqual(p.relative_to('c:', strict=False), P('Foo/Bar')) + self.assertEqual(p.relative_to(P('c:foO'), strict=False), P('Bar')) + self.assertEqual(p.relative_to('c:foO', strict=False), P('Bar')) + self.assertEqual(p.relative_to('c:foO/', strict=False), P('Bar')) + self.assertEqual(p.relative_to(P('c:foO/baR'), strict=False), P()) + self.assertEqual(p.relative_to('c:foO/baR', strict=False), P()) + self.assertEqual(p.relative_to(P('C:Foo/Bar/Baz'), strict=False), P('..')) + self.assertEqual(p.relative_to(P('C:Foo/Baz'), strict=False), P('../Bar')) + self.assertEqual(p.relative_to(P('C:Baz/Bar'), strict=False), P('../../Foo/Bar')) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P()) self.assertRaises(ValueError, p.relative_to, '') @@ -1089,6 +1130,13 @@ def test_relative_to(self): self.assertRaises(ValueError, p.relative_to, P('C:/Foo')) self.assertRaises(ValueError, p.relative_to, P('C:Foo/Bar/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:Foo/Baz')) + self.assertRaises(ValueError, p.relative_to, P(), strict=False) + self.assertRaises(ValueError, p.relative_to, '', strict=False) + self.assertRaises(ValueError, p.relative_to, P('d:'), strict=False) + self.assertRaises(ValueError, p.relative_to, P('/'), strict=False) + self.assertRaises(ValueError, p.relative_to, P('Foo'), strict=False) + self.assertRaises(ValueError, p.relative_to, P('/Foo'), strict=False) + self.assertRaises(ValueError, p.relative_to, P('C:/Foo'), strict=False) p = P('C:/Foo/Bar') self.assertEqual(p.relative_to(P('c:')), P('/Foo/Bar')) self.assertEqual(p.relative_to('c:'), P('/Foo/Bar')) @@ -1101,6 +1149,20 @@ 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:'), strict=False), P('/Foo/Bar')) + self.assertEqual(p.relative_to('c:', strict=False), P('/Foo/Bar')) + self.assertEqual(str(p.relative_to(P('c:'), strict=False)), '\\Foo\\Bar') + self.assertEqual(str(p.relative_to('c:', strict=False)), '\\Foo\\Bar') + self.assertEqual(p.relative_to(P('c:/'), strict=False), P('Foo/Bar')) + self.assertEqual(p.relative_to('c:/', strict=False), P('Foo/Bar')) + self.assertEqual(p.relative_to(P('c:/foO'), strict=False), P('Bar')) + self.assertEqual(p.relative_to('c:/foO', strict=False), P('Bar')) + self.assertEqual(p.relative_to('c:/foO/', strict=False), P('Bar')) + self.assertEqual(p.relative_to(P('c:/foO/baR'), strict=False), P()) + self.assertEqual(p.relative_to('c:/foO/baR', strict=False), P()) + self.assertEqual(p.relative_to('C:/Baz', strict=False), P('../Foo/Bar')) + self.assertEqual(p.relative_to('C:/Foo/Bar/Baz', strict=False), P('..')) + self.assertEqual(p.relative_to('C:/Foo/Baz', strict=False), P('../Bar')) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('C:/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Bar/Baz')) @@ -1111,6 +1173,12 @@ 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, P('C:Foo'), strict=False) + self.assertRaises(ValueError, p.relative_to, P('d:'), strict=False) + self.assertRaises(ValueError, p.relative_to, P('d:/'), strict=False) + self.assertRaises(ValueError, p.relative_to, P('/'), strict=False) + self.assertRaises(ValueError, p.relative_to, P('/Foo'), strict=False) + self.assertRaises(ValueError, p.relative_to, P('//C/Foo'), strict=False) # UNC paths. p = P('//Server/Share/Foo/Bar') self.assertEqual(p.relative_to(P('//sErver/sHare')), P('Foo/Bar')) @@ -1121,11 +1189,25 @@ def test_relative_to(self): self.assertEqual(p.relative_to('//sErver/sHare/Foo/'), P('Bar')) self.assertEqual(p.relative_to(P('//sErver/sHare/Foo/Bar')), P()) self.assertEqual(p.relative_to('//sErver/sHare/Foo/Bar'), P()) + self.assertEqual(p.relative_to(P('//sErver/sHare'), strict=False), P('Foo/Bar')) + self.assertEqual(p.relative_to('//sErver/sHare', strict=False), P('Foo/Bar')) + self.assertEqual(p.relative_to('//sErver/sHare/', strict=False), P('Foo/Bar')) + self.assertEqual(p.relative_to(P('//sErver/sHare/Foo'), strict=False), P('Bar')) + self.assertEqual(p.relative_to('//sErver/sHare/Foo', strict=False), P('Bar')) + self.assertEqual(p.relative_to('//sErver/sHare/Foo/', strict=False), P('Bar')) + self.assertEqual(p.relative_to(P('//sErver/sHare/Foo/Bar'), strict=False), P()) + self.assertEqual(p.relative_to('//sErver/sHare/Foo/Bar', strict=False), P()) + self.assertEqual(p.relative_to(P('//sErver/sHare/bar'), strict=False), P('../Foo/Bar')) + self.assertEqual(p.relative_to('//sErver/sHare/bar', strict=False), P('../Foo/Bar')) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('/Server/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('c:/Server/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('//z/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('//Server/z/Foo')) + self.assertRaises(ValueError, p.relative_to, P('/Server/Share/Foo'), strict=False) + self.assertRaises(ValueError, p.relative_to, P('c:/Server/Share/Foo'), strict=False) + self.assertRaises(ValueError, p.relative_to, P('//z/Share/Foo'), strict=False) + self.assertRaises(ValueError, p.relative_to, P('//Server/z/Foo'), strict=False) def test_is_relative_to(self): P = self.cls diff --git a/Misc/NEWS.d/next/Library/2020-04-30-02-15-08.bpo-40358.A4ygqe.rst b/Misc/NEWS.d/next/Library/2020-04-30-02-15-08.bpo-40358.A4ygqe.rst new file mode 100644 index 00000000000000..219c41ae4e5f07 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-04-30-02-15-08.bpo-40358.A4ygqe.rst @@ -0,0 +1,2 @@ +Add strict parameter in :meth:`pathlib.PurePath.relative_to`. Patch by +Domenico Ragusa.