gh-84538: add strict argument to pathlib.PurePath.relative_to (GH-19813)

By default, :meth:`pathlib.PurePath.relative_to` doesn't deal with paths that are not a direct prefix of the other, raising an exception in that instance. This change adds a *walk_up* parameter that can be set to allow for using ``..`` to calculate the relative path.

example:
```
>>> p = PurePosixPath('/etc/passwd')
>>> p.relative_to('/etc')
PurePosixPath('passwd')
>>> p.relative_to('/usr')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
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')
```


https://bugs.python.org/issue40358

Automerge-Triggered-By: GH:brettcannon
This commit is contained in:
domragusa 2022-10-29 01:20:14 +02:00 committed by GitHub
parent 72fa57a8fe
commit e089f23bbb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 150 additions and 21 deletions

View file

@ -564,10 +564,10 @@ Pure paths provide the following methods and properties:
True True
.. method:: PurePath.relative_to(*other) .. method:: PurePath.relative_to(*other, walk_up=False)
Compute a version of this path relative to the path represented by 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 = PurePosixPath('/etc/passwd')
>>> p.relative_to('/') >>> p.relative_to('/')
@ -577,11 +577,33 @@ Pure paths provide the following methods and properties:
>>> p.relative_to('/usr') >>> p.relative_to('/usr')
Traceback (most recent call last): Traceback (most recent call last):
File "<stdin>", line 1, in <module> File "<stdin>", line 1, in <module>
File "pathlib.py", line 694, in relative_to File "pathlib.py", line 941, in relative_to
.format(str(self), str(formatted))) 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 absolute. ValueError: '/etc/passwd' is not in the subpath of '/usr' OR one path is relative and the other is absolute.
NOTE: This function is part of :class:`PurePath` and works with strings. It does not check or access the underlying file structure. 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')
>>> p.relative_to('foo', walk_up=True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
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.
.. warning::
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``).
.. method:: PurePath.with_name(name) .. method:: PurePath.with_name(name)

View file

@ -149,6 +149,11 @@ pathlib
all file or directory names within them, similar to :func:`os.walk`. all file or directory names within them, similar to :func:`os.walk`.
(Contributed by Stanislav Zmiev in :gh:`90385`.) (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 dis
--- ---

View file

@ -626,10 +626,13 @@ class PurePath(object):
return self._from_parsed_parts(self._drv, self._root, return self._from_parsed_parts(self._drv, self._root,
self._parts[:-1] + [name]) self._parts[:-1] + [name])
def relative_to(self, *other): def relative_to(self, *other, walk_up=False):
"""Return the relative path to another path identified by the passed """Return the relative path to another path identified by the passed
arguments. If the operation is not possible (because this is not 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.
The *walk_up* parameter controls whether `..` may be used to resolve
the path.
""" """
# For the purpose of this method, drive and root are considered # For the purpose of this method, drive and root are considered
# separate parts, i.e.: # separate parts, i.e.:
@ -644,20 +647,35 @@ class PurePath(object):
abs_parts = [drv, root] + parts[1:] abs_parts = [drv, root] + parts[1:]
else: else:
abs_parts = parts abs_parts = parts
to_drv, to_root, to_parts = self._parse_args(other) other_drv, other_root, other_parts = self._parse_args(other)
if to_root: if other_root:
to_abs_parts = [to_drv, to_root] + to_parts[1:] other_abs_parts = [other_drv, other_root] + other_parts[1:]
else: else:
to_abs_parts = to_parts other_abs_parts = other_parts
n = len(to_abs_parts) num_parts = len(other_abs_parts)
cf = self._flavour.casefold_parts casefold = self._flavour.casefold_parts
if (root or drv) if n == 0 else cf(abs_parts[:n]) != cf(to_abs_parts): num_common_parts = 0
formatted = self._format_parsed_parts(to_drv, to_root, to_parts) for part, other_part in zip(casefold(abs_parts), casefold(other_abs_parts)):
raise ValueError("{!r} is not in the subpath of {!r}" if part != other_part:
" OR one path is relative and the other is absolute." break
.format(str(self), str(formatted))) num_common_parts += 1
return self._from_parsed_parts('', root if n == 1 else '', if walk_up:
abs_parts[n:]) 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)
def is_relative_to(self, *other): def is_relative_to(self, *other):
"""Return True if the path is relative to another path or False. """Return True if the path is relative to another path or False.

View file

@ -640,13 +640,29 @@ class _BasePurePathTest(object):
self.assertEqual(p.relative_to('a/'), P('b')) self.assertEqual(p.relative_to('a/'), P('b'))
self.assertEqual(p.relative_to(P('a/b')), P()) self.assertEqual(p.relative_to(P('a/b')), P())
self.assertEqual(p.relative_to('a/b'), P()) self.assertEqual(p.relative_to('a/b'), P())
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. # With several args.
self.assertEqual(p.relative_to('a', 'b'), P()) self.assertEqual(p.relative_to('a', 'b'), P())
self.assertEqual(p.relative_to('a', 'b', walk_up=True), P())
# Unrelated paths. # Unrelated paths.
self.assertRaises(ValueError, p.relative_to, P('c')) 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/b/c'))
self.assertRaises(ValueError, p.relative_to, P('a/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('/a'))
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') p = P('/a/b')
self.assertEqual(p.relative_to(P('/')), P('a/b')) self.assertEqual(p.relative_to(P('/')), P('a/b'))
self.assertEqual(p.relative_to('/'), P('a/b')) self.assertEqual(p.relative_to('/'), P('a/b'))
@ -655,6 +671,19 @@ class _BasePurePathTest(object):
self.assertEqual(p.relative_to('/a/'), P('b')) self.assertEqual(p.relative_to('/a/'), P('b'))
self.assertEqual(p.relative_to(P('/a/b')), P()) self.assertEqual(p.relative_to(P('/a/b')), P())
self.assertEqual(p.relative_to('/a/b'), P()) self.assertEqual(p.relative_to('/a/b'), P())
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. # Unrelated paths.
self.assertRaises(ValueError, p.relative_to, P('/c')) 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/b/c'))
@ -662,6 +691,8 @@ class _BasePurePathTest(object):
self.assertRaises(ValueError, p.relative_to, P()) self.assertRaises(ValueError, p.relative_to, P())
self.assertRaises(ValueError, p.relative_to, '') self.assertRaises(ValueError, p.relative_to, '')
self.assertRaises(ValueError, p.relative_to, P('a')) self.assertRaises(ValueError, p.relative_to, P('a'))
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): def test_is_relative_to_common(self):
P = self.cls P = self.cls
@ -1124,6 +1155,16 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase):
self.assertEqual(p.relative_to('c:foO/'), P('Bar')) self.assertEqual(p.relative_to('c:foO/'), P('Bar'))
self.assertEqual(p.relative_to(P('c:foO/baR')), P()) self.assertEqual(p.relative_to(P('c:foO/baR')), P())
self.assertEqual(p.relative_to('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(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. # Unrelated paths.
self.assertRaises(ValueError, p.relative_to, P()) self.assertRaises(ValueError, p.relative_to, P())
self.assertRaises(ValueError, p.relative_to, '') self.assertRaises(ValueError, p.relative_to, '')
@ -1134,6 +1175,13 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase):
self.assertRaises(ValueError, p.relative_to, P('C:/Foo')) 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/Bar/Baz'))
self.assertRaises(ValueError, p.relative_to, P('C:Foo/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:Foo/Baz'))
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') p = P('C:/Foo/Bar')
self.assertEqual(p.relative_to(P('c:')), P('/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('c:'), P('/Foo/Bar'))
@ -1146,6 +1194,20 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase):
self.assertEqual(p.relative_to('c:/foO/'), P('Bar')) self.assertEqual(p.relative_to('c:/foO/'), P('Bar'))
self.assertEqual(p.relative_to(P('c:/foO/baR')), P()) self.assertEqual(p.relative_to(P('c:/foO/baR')), P())
self.assertEqual(p.relative_to('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'))
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. # Unrelated paths.
self.assertRaises(ValueError, p.relative_to, P('C:/Baz')) 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/Bar/Baz'))
@ -1156,6 +1218,12 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase):
self.assertRaises(ValueError, p.relative_to, P('/')) self.assertRaises(ValueError, p.relative_to, P('/'))
self.assertRaises(ValueError, p.relative_to, P('/Foo')) 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'))
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. # UNC paths.
p = P('//Server/Share/Foo/Bar') p = P('//Server/Share/Foo/Bar')
self.assertEqual(p.relative_to(P('//sErver/sHare')), P('Foo/Bar')) self.assertEqual(p.relative_to(P('//sErver/sHare')), P('Foo/Bar'))
@ -1166,11 +1234,25 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase):
self.assertEqual(p.relative_to('//sErver/sHare/Foo/'), P('Bar')) 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(P('//sErver/sHare/Foo/Bar')), P())
self.assertEqual(p.relative_to('//sErver/sHare/Foo/Bar'), P()) self.assertEqual(p.relative_to('//sErver/sHare/Foo/Bar'), P())
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. # Unrelated paths.
self.assertRaises(ValueError, p.relative_to, P('/Server/Share/Foo')) 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('c:/Server/Share/Foo'))
self.assertRaises(ValueError, p.relative_to, P('//z/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/z/Foo'))
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): def test_is_relative_to(self):
P = self.cls P = self.cls

View file

@ -1440,6 +1440,7 @@ Pierre Quentel
Brian Quinlan Brian Quinlan
Anders Qvist Anders Qvist
Thomas Rachel Thomas Rachel
Domenico Ragusa
Ram Rachum Ram Rachum
Jeffrey Rackauckas Jeffrey Rackauckas
Jérôme Radix Jérôme Radix

View file

@ -0,0 +1 @@
Add walk_up argument in :meth:`pathlib.PurePath.relative_to`.