diff --git a/Lib/pathlib.py b/Lib/pathlib.py index f98d69e..eb25761 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -895,10 +895,10 @@ class PurePath(object): 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.: @@ -918,14 +918,31 @@ class PurePath(object): to_abs_parts = [to_drv, to_root] + to_parts[1:] else: 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 1589282..a6d8fe4 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -613,13 +613,30 @@ class _BasePurePathTest(object): 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 +645,19 @@ class _BasePurePathTest(object): 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 +665,8 @@ class _BasePurePathTest(object): 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 +1111,17 @@ 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:'), 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 +1132,13 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase): 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 +1151,20 @@ 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:'), 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 +1175,13 @@ 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, 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 +1192,25 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase): 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