From ec643681ce35ad01f84986dc935ad7787b62f2b4 Mon Sep 17 00:00:00 2001 From: Domenico Ragusa Date: Thu, 30 Apr 2020 12:18:49 +0200 Subject: [PATCH 01/12] bpo-40358: add strict param to pathlib.PurePath.relative_to --- Doc/library/pathlib.rst | 22 ++++- Lib/pathlib.py | 28 +++++-- Lib/test/test_pathlib.py | 82 +++++++++++++++++++ Misc/ACKS | 1 + .../2020-04-30-02-15-08.bpo-40358.A4ygqe.rst | 2 + 5 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-04-30-02-15-08.bpo-40358.A4ygqe.rst 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/ACKS b/Misc/ACKS index 89f37e584ef8ba..54c7c3b9c761a3 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1367,6 +1367,7 @@ Pierre Quentel Brian Quinlan Anders Qvist Thomas Rachel +Domenico Ragusa Ram Rachum Jeffrey Rackauckas Jérôme Radix 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. From d9c07c2f0f73dcf5c7dc863f9d4eeaeb3cfe64c4 Mon Sep 17 00:00:00 2001 From: domragusa <64558788+domragusa@users.noreply.github.com> Date: Sat, 15 May 2021 12:30:27 +0200 Subject: [PATCH 02/12] Update Doc/library/pathlib.rst Co-authored-by: Terry Jan Reedy --- Doc/library/pathlib.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 3a38e956311cd2..9641ceb973d80f 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -565,8 +565,8 @@ Pure paths provide the following methods and properties: If the path doesn't start with *other* and *strict* is ``True``, :exc:`ValueError` is raised. If *strict* is ``False`` and one path is relative and the other is absolute or if they reference different drives :exc:`ValueError` is raised. - NOTE: This function is part of :class:`PurePath` and works with strings. It does not check or access the underlying file structure. - + .. versionadded:: 3.11 + The *strict* argument (pre-3.11 behavior is strict). .. versionadded:: 3.10 The *strict* argument (pre-3.10 behavior is strict). From 029c3d5ce9acb7dc1bfc8e2b9cce1e0e54cc0b51 Mon Sep 17 00:00:00 2001 From: Domenico Ragusa Date: Mon, 17 May 2021 03:11:49 +0200 Subject: [PATCH 03/12] fix wording in documentation as requested --- Doc/library/pathlib.rst | 11 ++++++++--- Doc/whatsnew/3.11.rst | 7 +++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 9641ceb973d80f..e3da81bc2627a0 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -563,12 +563,17 @@ Pure paths provide the following methods and properties: raise ValueError(error_message.format(str(self), str(formatted))) ValueError: '/etc/passwd' is not on the same drive as 'foo' OR one path is relative and the other is absolute. - If the path doesn't start with *other* and *strict* is ``True``, :exc:`ValueError` is raised. If *strict* is ``False`` and one path is relative and the other is absolute or if they reference different drives :exc:`ValueError` is raised. + In strict mode (the default), the path must start with *other*. In non-strict + mode, ``..`` entries may be added to form the relative path. In all other + cases, such as the paths referencing different drives, :exe:`ValueError` is + raised. + + .. warning:: + Non-strict mode assumes that no symlinks are present in the path; you + should call :meth:`~Path.resolve` first to ensure this. .. versionadded:: 3.11 The *strict* argument (pre-3.11 behavior is strict). - .. versionadded:: 3.10 - The *strict* argument (pre-3.10 behavior is strict). .. method:: PurePath.with_name(name) diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 503688294072a3..beb829ca360f05 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -86,6 +86,13 @@ New Modules Improved Modules ================ +pathlib +------- + +:meth:`pathlib.PurePath.relative_to` now supports generating relative paths +containing ``..`` entries if its new *strict* keyword-only argument is set to +``False``. The new behavior is more consistent with :func:`os.path.relpath`. +(Contributed by Domenico Ragusa in :issue:`40358`.) Optimizations ============= From 01e789b0e9493c45745865419203749f3920a0f5 Mon Sep 17 00:00:00 2001 From: Domenico Ragusa Date: Tue, 18 May 2021 01:49:58 +0200 Subject: [PATCH 04/12] fix typo --- Doc/library/pathlib.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index e3da81bc2627a0..8abeefaddf4bef 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -541,7 +541,7 @@ Pure paths provide the following methods and properties: .. 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:: + *other*. If it's impossible, :exc:`ValueError` is raised:: >>> p = PurePosixPath('/etc/passwd') >>> p.relative_to('/') @@ -565,7 +565,7 @@ Pure paths provide the following methods and properties: In strict mode (the default), the path must start with *other*. In non-strict mode, ``..`` entries may be added to form the relative path. In all other - cases, such as the paths referencing different drives, :exe:`ValueError` is + cases, such as the paths referencing different drives, :exc:`ValueError` is raised. .. warning:: From 39b24f3bc17010d930f3572c98635730fb085466 Mon Sep 17 00:00:00 2001 From: Domenico Ragusa Date: Sat, 29 May 2021 13:05:09 +0200 Subject: [PATCH 05/12] change strict argument to walk_up in pathlib.PurePath.relative_to --- Doc/library/pathlib.rst | 18 +- Doc/whatsnew/3.11.rst | 4 +- Lib/pathlib.py | 12 +- Lib/test/test_pathlib.py | 164 +++++++++--------- .../2020-04-30-02-15-08.bpo-40358.A4ygqe.rst | 2 +- 5 files changed, 100 insertions(+), 100 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 8abeefaddf4bef..f17adaaa65fda9 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -538,7 +538,7 @@ Pure paths provide the following methods and properties: True -.. method:: PurePath.relative_to(*other, strict=True) +.. method:: PurePath.relative_to(*other, walk_up=False) Compute a version of this path relative to the path represented by *other*. If it's impossible, :exc:`ValueError` is raised:: @@ -554,26 +554,26 @@ Pure paths provide the following methods and properties: File "pathlib.py", line 941, in relative_to raise ValueError(error_message.format(str(self), str(formatted))) ValueError: '/etc/passwd' is not in the subpath of '/usr' OR one path is relative and the other is absolute. - >>> p.relative_to('/usr', strict=False) + >>> p.relative_to('/usr', walk_up=True) PurePosixPath('../etc/passwd') - >>> p.relative_to('foo', strict=False) + >>> p.relative_to('foo', walk_up=True) Traceback (most recent call last): File "", line 1, in File "pathlib.py", line 941, in relative_to raise ValueError(error_message.format(str(self), str(formatted))) ValueError: '/etc/passwd' is not on the same drive as 'foo' OR one path is relative and the other is absolute. - In strict mode (the default), the path must start with *other*. In non-strict - mode, ``..`` entries may be added to form the relative path. In all other - cases, such as the paths referencing different drives, :exc:`ValueError` is - raised. + When *walk_up* is False, the default, the path must start with *other*, + when it's True ``..`` entries may be added to form the relative path. In + all other cases, such as the paths referencing different drives, + :exc:`ValueError` is raised. .. warning:: - Non-strict mode assumes that no symlinks are present in the path; you + The *walk_up* option assumes that no symlinks are present in the path; you should call :meth:`~Path.resolve` first to ensure this. .. versionadded:: 3.11 - The *strict* argument (pre-3.11 behavior is strict). + The *walk_up* argument (pre-3.11 behavior is the same as walk_up=False). .. method:: PurePath.with_name(name) diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index beb829ca360f05..7ad08d35701939 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -90,8 +90,8 @@ pathlib ------- :meth:`pathlib.PurePath.relative_to` now supports generating relative paths -containing ``..`` entries if its new *strict* keyword-only argument is set to -``False``. The new behavior is more consistent with :func:`os.path.relpath`. +containing ``..`` entries if its new *walk_up* keyword-only argument is set to +``True``. The new behavior is more consistent with :func:`os.path.relpath`. (Contributed by Domenico Ragusa in :issue:`40358`.) Optimizations diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 38759ff5dec849..b00eb10609dc74 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -776,7 +776,7 @@ def with_suffix(self, suffix): return self._from_parsed_parts(self._drv, self._root, self._parts[:-1] + [name]) - def relative_to(self, *other, strict=True): + def relative_to(self, *other, walk_up=False): """Return the relative path to another path identified by the passed arguments. If the operation is not possible (because this is not related to the other path), raise ValueError. @@ -806,16 +806,16 @@ def relative_to(self, *other, strict=True): if p != tp: break common += 1 - if strict: - failure = (root or drv) if n == 0 else common != n - error_message = "{!r} is not in the subpath of {!r}" - up_parts = [] - else: + if walk_up: 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 on the same drive as {!r}" up_parts = (n-common)*['..'] + else: + failure = (root or drv) if n == 0 else common != n + 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(to_drv, to_root, to_parts) diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 492a12e4970fb4..9a64bb7d83a3fd 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -629,29 +629,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')) + self.assertEqual(p.relative_to(P(), walk_up=True), P('a/b')) + self.assertEqual(p.relative_to('', walk_up=True), P('a/b')) + self.assertEqual(p.relative_to(P('a'), walk_up=True), P('b')) + self.assertEqual(p.relative_to('a', walk_up=True), P('b')) + self.assertEqual(p.relative_to('a/', walk_up=True), P('b')) + self.assertEqual(p.relative_to(P('a/b'), walk_up=True), P()) + self.assertEqual(p.relative_to('a/b', walk_up=True), P()) + self.assertEqual(p.relative_to(P('a/c'), walk_up=True), P('../b')) + self.assertEqual(p.relative_to('a/c', walk_up=True), P('../b')) + self.assertEqual(p.relative_to(P('a/b/c'), walk_up=True), P('..')) + self.assertEqual(p.relative_to('a/b/c', walk_up=True), P('..')) + self.assertEqual(p.relative_to(P('c'), walk_up=True), P('../a/b')) + self.assertEqual(p.relative_to('c', walk_up=True), P('../a/b')) # With several args. self.assertEqual(p.relative_to('a', 'b'), P()) - self.assertEqual(p.relative_to('a', 'b', strict=False), P()) + self.assertEqual(p.relative_to('a', 'b', walk_up=True), 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) + self.assertRaises(ValueError, p.relative_to, P('/'), walk_up=True) + self.assertRaises(ValueError, p.relative_to, P('/a'), walk_up=True) p = P('/a/b') self.assertEqual(p.relative_to(P('/')), P('a/b')) self.assertEqual(p.relative_to('/'), P('a/b')) @@ -660,19 +660,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')) + self.assertEqual(p.relative_to(P('/'), walk_up=True), P('a/b')) + self.assertEqual(p.relative_to('/', walk_up=True), P('a/b')) + self.assertEqual(p.relative_to(P('/a'), walk_up=True), P('b')) + self.assertEqual(p.relative_to('/a', walk_up=True), P('b')) + self.assertEqual(p.relative_to('/a/', walk_up=True), P('b')) + self.assertEqual(p.relative_to(P('/a/b'), walk_up=True), P()) + self.assertEqual(p.relative_to('/a/b', walk_up=True), P()) + self.assertEqual(p.relative_to(P('/a/c'), walk_up=True), P('../b')) + self.assertEqual(p.relative_to('/a/c', walk_up=True), P('../b')) + self.assertEqual(p.relative_to(P('/a/b/c'), walk_up=True), P('..')) + self.assertEqual(p.relative_to('/a/b/c', walk_up=True), P('..')) + self.assertEqual(p.relative_to(P('/c'), walk_up=True), P('../a/b')) + self.assertEqual(p.relative_to('/c', walk_up=True), P('../a/b')) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('/c')) self.assertRaises(ValueError, p.relative_to, P('/a/b/c')) @@ -680,8 +680,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) + self.assertRaises(ValueError, p.relative_to, P(''), walk_up=True) + self.assertRaises(ValueError, p.relative_to, P('a'), walk_up=True) def test_is_relative_to_common(self): P = self.cls @@ -1144,16 +1144,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')) + 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')) + self.assertEqual(p.relative_to('c:foO', walk_up=True), P('Bar')) + self.assertEqual(p.relative_to('c:foO/', walk_up=True), P('Bar')) + self.assertEqual(p.relative_to(P('c:foO/baR'), walk_up=True), P()) + self.assertEqual(p.relative_to('c:foO/baR', walk_up=True), P()) + self.assertEqual(p.relative_to(P('C:Foo/Bar/Baz'), walk_up=True), P('..')) + self.assertEqual(p.relative_to(P('C:Foo/Baz'), walk_up=True), P('../Bar')) + self.assertEqual(p.relative_to(P('C:Baz/Bar'), walk_up=True), P('../../Foo/Bar')) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P()) self.assertRaises(ValueError, p.relative_to, '') @@ -1164,13 +1164,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) + self.assertRaises(ValueError, p.relative_to, P(), walk_up=True) + self.assertRaises(ValueError, p.relative_to, '', walk_up=True) + self.assertRaises(ValueError, p.relative_to, P('d:'), walk_up=True) + self.assertRaises(ValueError, p.relative_to, P('/'), walk_up=True) + self.assertRaises(ValueError, p.relative_to, P('Foo'), walk_up=True) + 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')) @@ -1183,20 +1183,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')) + 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')) + self.assertEqual(p.relative_to('c:/foO', walk_up=True), P('Bar')) + self.assertEqual(p.relative_to('c:/foO/', walk_up=True), P('Bar')) + self.assertEqual(p.relative_to(P('c:/foO/baR'), walk_up=True), P()) + self.assertEqual(p.relative_to('c:/foO/baR', walk_up=True), P()) + self.assertEqual(p.relative_to('C:/Baz', walk_up=True), P('../Foo/Bar')) + 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, P('C:/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Bar/Baz')) @@ -1207,12 +1207,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) + 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) + self.assertRaises(ValueError, p.relative_to, P('/'), walk_up=True) + self.assertRaises(ValueError, p.relative_to, P('/Foo'), walk_up=True) + self.assertRaises(ValueError, p.relative_to, P('//C/Foo'), walk_up=True) # UNC paths. p = P('//Server/Share/Foo/Bar') self.assertEqual(p.relative_to(P('//sErver/sHare')), P('Foo/Bar')) @@ -1223,25 +1223,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')) + self.assertEqual(p.relative_to(P('//sErver/sHare'), walk_up=True), P('Foo/Bar')) + self.assertEqual(p.relative_to('//sErver/sHare', walk_up=True), P('Foo/Bar')) + self.assertEqual(p.relative_to('//sErver/sHare/', walk_up=True), P('Foo/Bar')) + self.assertEqual(p.relative_to(P('//sErver/sHare/Foo'), walk_up=True), P('Bar')) + self.assertEqual(p.relative_to('//sErver/sHare/Foo', walk_up=True), P('Bar')) + self.assertEqual(p.relative_to('//sErver/sHare/Foo/', walk_up=True), P('Bar')) + self.assertEqual(p.relative_to(P('//sErver/sHare/Foo/Bar'), walk_up=True), P()) + self.assertEqual(p.relative_to('//sErver/sHare/Foo/Bar', walk_up=True), P()) + self.assertEqual(p.relative_to(P('//sErver/sHare/bar'), walk_up=True), P('../Foo/Bar')) + self.assertEqual(p.relative_to('//sErver/sHare/bar', walk_up=True), 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) + self.assertRaises(ValueError, p.relative_to, P('/Server/Share/Foo'), walk_up=True) + self.assertRaises(ValueError, p.relative_to, P('c:/Server/Share/Foo'), walk_up=True) + self.assertRaises(ValueError, p.relative_to, P('//z/Share/Foo'), walk_up=True) + self.assertRaises(ValueError, p.relative_to, P('//Server/z/Foo'), walk_up=True) 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 index 72fbede2448efc..a2815f54b031a1 100644 --- 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 @@ -1 +1 @@ -Add strict argument in :meth:`pathlib.PurePath.relative_to`. +Add walk_up argument in :meth:`pathlib.PurePath.relative_to`. From 8401974d96fdbc035162eaddf49e903e96ecc979 Mon Sep 17 00:00:00 2001 From: Domenico Ragusa Date: Sat, 15 Oct 2022 01:49:42 +0200 Subject: [PATCH 06/12] make the documentation clearer --- Doc/library/pathlib.rst | 15 ++++++++------- Doc/whatsnew/3.12.rst | 5 +++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 11af0299993350..f63e33bf4519a5 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -580,6 +580,12 @@ Pure paths provide the following methods and properties: File "pathlib.py", line 941, in relative_to raise ValueError(error_message.format(str(self), str(formatted))) ValueError: '/etc/passwd' is not in the subpath of '/usr' OR one path is relative and the other is absolute. + + When *walk_up* is False, the default, the path must start with *other*, + when it's True ``..`` entries may be added to form the relative path. In + all other cases, such as the paths referencing different drives, + :exc:`ValueError` is raised.:: + >>> p.relative_to('/usr', walk_up=True) PurePosixPath('../etc/passwd') >>> p.relative_to('foo', walk_up=True) @@ -589,17 +595,12 @@ Pure paths provide the following methods and properties: raise ValueError(error_message.format(str(self), str(formatted))) ValueError: '/etc/passwd' is not on the same drive as 'foo' OR one path is relative and the other is absolute. - When *walk_up* is False, the default, the path must start with *other*, - when it's True ``..`` entries may be added to form the relative path. In - all other cases, such as the paths referencing different drives, - :exc:`ValueError` is raised. - .. warning:: The *walk_up* option assumes that no symlinks are present in the path; you should call :meth:`~Path.resolve` first to ensure this. - .. versionadded:: 3.11 - The *walk_up* argument (pre-3.11 behavior is the same as walk_up=False). + .. versionadded:: 3.12 + The *walk_up* argument (pre-3.12 behavior is the same as walk_up=False). .. method:: PurePath.with_name(name) diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index ebc490691e308b..ad2cd6901032c6 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -134,6 +134,11 @@ pathlib all file or directory names within them, similar to :func:`os.walk`. (Contributed by Stanislav Zmiev in :gh:`90385`.) +* Add *walk_up* optional parameter to :meth:`pathlib.PurePath.relative_to` + to allow the insertion of ``..`` entries in the result; this behavior is + more consistent with :func:`os.path.relpath`. + (Contributed by Domenico Ragusa in :issue:`40358`.) + dis --- From e255f99c8cedf21212988f1ca3d1a52bb1d6304d Mon Sep 17 00:00:00 2001 From: domragusa <64558788+domragusa@users.noreply.github.com> Date: Sat, 22 Oct 2022 02:16:33 +0200 Subject: [PATCH 07/12] Apply suggestions from code review Co-authored-by: Brett Cannon --- Doc/library/pathlib.rst | 14 +++++++------- Lib/pathlib.py | 3 +++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index f63e33bf4519a5..417e3a8657c25f 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -581,10 +581,10 @@ Pure paths provide the following methods and properties: raise ValueError(error_message.format(str(self), str(formatted))) ValueError: '/etc/passwd' is not in the subpath of '/usr' OR one path is relative and the other is absolute. - When *walk_up* is False, the default, the path must start with *other*, - when it's True ``..`` entries may be added to form the relative path. In - all other cases, such as the paths referencing different drives, - :exc:`ValueError` is raised.:: + When *walk_up* is False (the default), the path must start with *other*. + When the argument is True, ``..`` entries may be added to form the + relative path. In all other cases, such as the paths referencing + different drives, :exc:`ValueError` is raised.:: >>> p.relative_to('/usr', walk_up=True) PurePosixPath('../etc/passwd') @@ -596,11 +596,11 @@ Pure paths provide the following methods and properties: ValueError: '/etc/passwd' is not on the same drive as 'foo' OR one path is relative and the other is absolute. .. warning:: - The *walk_up* option assumes that no symlinks are present in the path; you - should call :meth:`~Path.resolve` first to ensure this. + The *walk_up* option assumes that no symlinks are present in the path; + call :meth:`~Path.resolve` first if necessary to resolve symlinks. .. versionadded:: 3.12 - The *walk_up* argument (pre-3.12 behavior is the same as walk_up=False). + The *walk_up* argument (old behavior is the same as ``walk_up=False``). .. method:: PurePath.with_name(name) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 76d559fbe8dc34..8de3d6008ec095 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -630,6 +630,9 @@ def relative_to(self, *other, walk_up=False): """Return the relative path to another path identified by the passed arguments. If the operation is not possible (because this is not related to the other path), raise ValueError. + + 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.: From b7813562abff8f0af8b56c5dd110d209cc817312 Mon Sep 17 00:00:00 2001 From: domragusa <64558788+domragusa@users.noreply.github.com> Date: Sat, 22 Oct 2022 02:30:01 +0200 Subject: [PATCH 08/12] Update Lib/pathlib.py Co-authored-by: Brett Cannon --- Lib/pathlib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 8de3d6008ec095..b6f2ccb2a85a98 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -673,8 +673,9 @@ def relative_to(self, *other, walk_up=False): if failure: formatted = self._format_parsed_parts(to_drv, to_root, to_parts) raise ValueError(error_message.format(str(self), str(formatted))) + path_parts = up_parts + abs_parts[common:] return self._from_parsed_parts('', root if common == 1 else '', - up_parts+abs_parts[common:]) + path_parts) def is_relative_to(self, *other): """Return True if the path is relative to another path or False. From 7974ee93be5aae33e0719615c69ff10df71d69b7 Mon Sep 17 00:00:00 2001 From: domragusa <64558788+domragusa@users.noreply.github.com> Date: Sat, 22 Oct 2022 02:48:28 +0200 Subject: [PATCH 09/12] fix doc warning Co-authored-by: C.A.M. Gerlach --- Doc/library/pathlib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 417e3a8657c25f..3898a6c5e04243 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -581,7 +581,7 @@ Pure paths provide the following methods and properties: raise ValueError(error_message.format(str(self), str(formatted))) ValueError: '/etc/passwd' is not in the subpath of '/usr' OR one path is relative and the other is absolute. - When *walk_up* is False (the default), the path must start with *other*. +When *walk_up* is False (the default), the path must start with *other*. When the argument is True, ``..`` entries may be added to form the relative path. In all other cases, such as the paths referencing different drives, :exc:`ValueError` is raised.:: From 8a4edb5dac97c6c076d31ebebc8a3bbc1466e4c0 Mon Sep 17 00:00:00 2001 From: Domenico Ragusa Date: Sat, 22 Oct 2022 02:49:29 +0200 Subject: [PATCH 10/12] improve variable names --- Lib/pathlib.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index b6f2ccb2a85a98..3b7ce7d79e6aaa 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -647,35 +647,35 @@ def relative_to(self, *other, walk_up=False): abs_parts = [drv, root] + parts[1:] else: abs_parts = parts - to_drv, to_root, to_parts = self._parse_args(other) - if to_root: - to_abs_parts = [to_drv, to_root] + to_parts[1:] + other_drv, other_root, other_parts = self._parse_args(other) + if other_root: + other_abs_parts = [other_drv, other_root] + other_parts[1:] else: - to_abs_parts = to_parts - n = len(to_abs_parts) - cf = self._flavour.casefold_parts - common = 0 - for p, tp in zip(cf(abs_parts), cf(to_abs_parts)): - if p != tp: + 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: break - common += 1 + num_common_parts += 1 if walk_up: - failure = root != to_root - if drv or to_drv: - failure = cf([drv]) != cf([to_drv]) or (failure and n > 1) + 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 = (n-common)*['..'] + up_parts = (num_parts-num_common_parts)*['..'] else: - failure = (root or drv) if n == 0 else common != n + 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(to_drv, to_root, to_parts) + 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[common:] - return self._from_parsed_parts('', root if common == 1 else '', - path_parts) + 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) def is_relative_to(self, *other): """Return True if the path is relative to another path or False. From e6cf222257b744a714ed3ab438da833e0ab2da0d Mon Sep 17 00:00:00 2001 From: Domenico Ragusa Date: Sat, 22 Oct 2022 03:43:19 +0200 Subject: [PATCH 11/12] remove trailing spaces --- Lib/pathlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 3b7ce7d79e6aaa..1498ce08be4012 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -630,7 +630,7 @@ def relative_to(self, *other, walk_up=False): """Return the relative path to another path identified by the passed arguments. If the operation is not possible (because this is not related to the other path), raise ValueError. - + The *walk_up* parameter controls whether `..` may be used to resolve the path. """ From a4c77c2fbb6d7094b1e0b0d6443eb4a4c68edbc4 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 28 Oct 2022 15:54:43 -0700 Subject: [PATCH 12/12] Update Doc/library/pathlib.rst --- Doc/library/pathlib.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 08ab66b4728c16..a6daca9789a37f 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -596,8 +596,11 @@ When *walk_up* is False (the default), the path must start with *other*. ValueError: '/etc/passwd' is not on the same drive as 'foo' OR one path is relative and the other is absolute. .. warning:: - The *walk_up* option assumes that no symlinks are present in the path; - call :meth:`~Path.resolve` first if necessary to resolve symlinks. + This function is part of :class:`PurePath` and works with strings. + It does not check or access the underlying file structure. + This can impact the *walk_up* option as it assumes that no symlinks + are present in the path; call :meth:`~Path.resolve` first if + necessary to resolve symlinks. .. versionadded:: 3.12 The *walk_up* argument (old behavior is the same as ``walk_up=False``).