Skip to content

Commit 5e1f215

Browse files
committed
Sync from upstream
1 parent 3ab34ab commit 5e1f215

File tree

4 files changed

+98
-1
lines changed

4 files changed

+98
-1
lines changed

CHANGES.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Version History
44
Unreleased
55
----------
66

7-
- Nothing yet
7+
- Add ``JoinablePath.relative_to()`` and ``JoinablePath.is_relative_to()``.
88

99
v0.5.1
1010
------

docs/api.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ This package offers the following abstract base classes:
164164
:meth:`~JoinablePath.__truediv__`
165165
:meth:`~JoinablePath.__rtruediv__`
166166

167+
:meth:`~JoinablePath.relative_to`
168+
:meth:`~JoinablePath.is_relative_to`
169+
167170
:meth:`~JoinablePath.full_match`
168171

169172
- * :class:`ReadablePath`
@@ -291,6 +294,18 @@ This package offers the following abstract base classes:
291294

292295
Return a new path with the given path segment joined on the beginning.
293296

297+
.. method:: relative_to(other, *, walk_up=False)
298+
299+
Return a new relative path from *other* to this path. The default
300+
implementation compares this path and the parents of *other*;
301+
``__eq__()`` must be implemented for this to work correctly.
302+
303+
.. method:: is_relative_to(other)
304+
305+
Returns ``True`` is this path is relative to *other*, ``False``
306+
otherwise. The default implementation compares this path and the parents
307+
of *other*; ``__eq__()`` must be implemented for this to work correctly.
308+
294309
.. method:: full_match(pattern)
295310

296311
Return true if the path matches the given glob-style pattern, false

pathlib_abc/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,33 @@ def parents(self):
241241
parent = split(path)[0]
242242
return tuple(parents)
243243

244+
def relative_to(self, other, *, walk_up=False):
245+
"""Return the relative path to another path identified by the passed
246+
arguments. If the operation is not possible (because this is not
247+
related to the other path), raise ValueError.
248+
249+
The *walk_up* parameter controls whether `..` may be used to resolve
250+
the path.
251+
"""
252+
parts = []
253+
for path in (other,) + other.parents:
254+
if self.is_relative_to(path):
255+
break
256+
elif not walk_up:
257+
raise ValueError(f"{self!r} is not in the subpath of {other!r}")
258+
elif path.name == '..':
259+
raise ValueError(f"'..' segment in {other!r} cannot be walked")
260+
else:
261+
parts.append('..')
262+
else:
263+
raise ValueError(f"{self!r} and {other!r} have different anchors")
264+
return self.with_segments(*parts, *self.parts[len(path.parts):])
265+
266+
def is_relative_to(self, other):
267+
"""Return True if the path is relative to another path or False.
268+
"""
269+
return other == self or other in self.parents
270+
244271
def full_match(self, pattern):
245272
"""
246273
Return True if this path matches the given glob-style pattern. The

tests/test_join.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,61 @@ def test_with_suffix(self):
354354
self.assertRaises(ValueError, P('a/b').with_suffix, '.d/.')
355355
self.assertRaises(TypeError, P('a/b').with_suffix, None)
356356

357+
def test_relative_to(self):
358+
P = self.cls
359+
p = P('a/b')
360+
self.assertEqual(p.relative_to(P('')), P('a/b'))
361+
self.assertEqual(p.relative_to(P('a')), P('b'))
362+
self.assertEqual(p.relative_to(P('a/b')), P(''))
363+
self.assertEqual(p.relative_to(P(''), walk_up=True), P('a/b'))
364+
self.assertEqual(p.relative_to(P('a'), walk_up=True), P('b'))
365+
self.assertEqual(p.relative_to(P('a/b'), walk_up=True), P(''))
366+
self.assertEqual(p.relative_to(P('a/c'), walk_up=True), P('../b'))
367+
self.assertEqual(p.relative_to(P('a/b/c'), walk_up=True), P('..'))
368+
self.assertEqual(p.relative_to(P('c'), walk_up=True), P('../a/b'))
369+
self.assertRaises(ValueError, p.relative_to, P('c'))
370+
self.assertRaises(ValueError, p.relative_to, P('a/b/c'))
371+
self.assertRaises(ValueError, p.relative_to, P('a/c'))
372+
self.assertRaises(ValueError, p.relative_to, P('/a'))
373+
self.assertRaises(ValueError, p.relative_to, P('../a'))
374+
self.assertRaises(ValueError, p.relative_to, P('a/..'))
375+
self.assertRaises(ValueError, p.relative_to, P('/a/..'))
376+
self.assertRaises(ValueError, p.relative_to, P('/'), walk_up=True)
377+
self.assertRaises(ValueError, p.relative_to, P('/a'), walk_up=True)
378+
self.assertRaises(ValueError, p.relative_to, P('../a'), walk_up=True)
379+
self.assertRaises(ValueError, p.relative_to, P('a/..'), walk_up=True)
380+
self.assertRaises(ValueError, p.relative_to, P('/a/..'), walk_up=True)
381+
class Q(self.cls):
382+
__eq__ = object.__eq__
383+
__hash__ = object.__hash__
384+
q = Q('a/b')
385+
self.assertTrue(q.relative_to(q))
386+
self.assertRaises(ValueError, q.relative_to, Q(''))
387+
self.assertRaises(ValueError, q.relative_to, Q('a'))
388+
self.assertRaises(ValueError, q.relative_to, Q('a'), walk_up=True)
389+
self.assertRaises(ValueError, q.relative_to, Q('a/b'))
390+
self.assertRaises(ValueError, q.relative_to, Q('c'))
391+
392+
def test_is_relative_to(self):
393+
P = self.cls
394+
p = P('a/b')
395+
self.assertTrue(p.is_relative_to(P('')))
396+
self.assertTrue(p.is_relative_to(P('a')))
397+
self.assertTrue(p.is_relative_to(P('a/b')))
398+
self.assertFalse(p.is_relative_to(P('c')))
399+
self.assertFalse(p.is_relative_to(P('a/b/c')))
400+
self.assertFalse(p.is_relative_to(P('a/c')))
401+
self.assertFalse(p.is_relative_to(P('/a')))
402+
class Q(self.cls):
403+
__eq__ = object.__eq__
404+
__hash__ = object.__hash__
405+
q = Q('a/b')
406+
self.assertTrue(q.is_relative_to(q))
407+
self.assertFalse(q.is_relative_to(Q('')))
408+
self.assertFalse(q.is_relative_to(Q('a')))
409+
self.assertFalse(q.is_relative_to(Q('a/b')))
410+
self.assertFalse(q.is_relative_to(Q('c')))
411+
357412

358413
class LexicalPathJoinTest(JoinTestBase, unittest.TestCase):
359414
cls = LexicalPath

0 commit comments

Comments
 (0)