Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Lib/pathlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,16 +490,19 @@ def relative_to(self, other, *, walk_up=False):
"""
if not hasattr(other, 'with_segments'):
other = self.with_segments(other)
for step, path in enumerate(chain([other], other.parents)):
parts = []
for path in chain([other], other.parents):
if path == self or path in self.parents:
break
elif not walk_up:
raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
elif path.name == '..':
raise ValueError(f"'..' segment in {str(other)!r} cannot be walked")
else:
parts.append('..')
else:
raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
parts = ['..'] * step + self._tail[len(path._tail):]
parts.extend(self._tail[len(path._tail):])
return self._from_parsed_parts('', '', parts)

def is_relative_to(self, other):
Expand Down
27 changes: 27 additions & 0 deletions Lib/pathlib/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,33 @@ def parents(self):
parent = split(path)[0]
return tuple(parents)

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.
"""
parts = []
for path in (other,) + other.parents:
if self.is_relative_to(path):
break
elif not walk_up:
raise ValueError(f"{self!r} is not in the subpath of {other!r}")
elif path.name == '..':
raise ValueError(f"'..' segment in {other!r} cannot be walked")
else:
parts.append('..')
else:
raise ValueError(f"{self!r} and {other!r} have different anchors")
return self.with_segments(*parts, *self.parts[len(path.parts):])

def is_relative_to(self, other):
"""Return True if the path is relative to another path or False.
"""
return other == self or other in self.parents

def full_match(self, pattern):
"""
Return True if this path matches the given glob-style pattern. The
Expand Down
55 changes: 55 additions & 0 deletions Lib/test/test_pathlib/test_join.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,61 @@ def test_with_suffix(self):
self.assertRaises(ValueError, P('a/b').with_suffix, '.d/.')
self.assertRaises(TypeError, P('a/b').with_suffix, None)

def test_relative_to(self):
P = self.cls
p = P('a/b')
self.assertEqual(p.relative_to(P('')), P('a', 'b'))
self.assertEqual(p.relative_to(P('a')), P('b'))
self.assertEqual(p.relative_to(P('a/b')), P(''))
self.assertEqual(p.relative_to(P(''), walk_up=True), P('a', 'b'))
self.assertEqual(p.relative_to(P('a'), walk_up=True), P('b'))
self.assertEqual(p.relative_to(P('a/b'), walk_up=True), P(''))
self.assertEqual(p.relative_to(P('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(P('c'), walk_up=True), P('..', 'a', 'b'))
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('../a'))
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)
self.assertRaises(ValueError, p.relative_to, P('../a'), walk_up=True)
self.assertRaises(ValueError, p.relative_to, P('a/..'), walk_up=True)
self.assertRaises(ValueError, p.relative_to, P('/a/..'), walk_up=True)
class Q(self.cls):
__eq__ = object.__eq__
__hash__ = object.__hash__
q = Q('a/b')
self.assertTrue(q.relative_to(q))
self.assertRaises(ValueError, q.relative_to, Q(''))
self.assertRaises(ValueError, q.relative_to, Q('a'))
self.assertRaises(ValueError, q.relative_to, Q('a'), walk_up=True)
self.assertRaises(ValueError, q.relative_to, Q('a/b'))
self.assertRaises(ValueError, q.relative_to, Q('c'))

def test_is_relative_to(self):
P = self.cls
p = P('a/b')
self.assertTrue(p.is_relative_to(P('')))
self.assertTrue(p.is_relative_to(P('a')))
self.assertTrue(p.is_relative_to(P('a/b')))
self.assertFalse(p.is_relative_to(P('c')))
self.assertFalse(p.is_relative_to(P('a/b/c')))
self.assertFalse(p.is_relative_to(P('a/c')))
self.assertFalse(p.is_relative_to(P('/a')))
class Q(self.cls):
__eq__ = object.__eq__
__hash__ = object.__hash__
q = Q('a/b')
self.assertTrue(q.is_relative_to(q))
self.assertFalse(q.is_relative_to(Q('')))
self.assertFalse(q.is_relative_to(Q('a')))
self.assertFalse(q.is_relative_to(Q('a/b')))
self.assertFalse(q.is_relative_to(Q('c')))


class LexicalPathJoinTest(JoinTestBase, unittest.TestCase):
cls = LexicalPath
Expand Down
Loading