diff --git a/dvc/path_info.py b/dvc/path_info.py index 18ffaf04a..c170086bd 100644 --- a/dvc/path_info.py +++ b/dvc/path_info.py @@ -75,6 +75,17 @@ class PathInfo(pathlib.PurePath, _BasePath): n = len(other._cparts) return len(self._cparts) > n and self._cparts[:n] == other._cparts + def relative_to(self, other): # pylint: disable=arguments-differ + # pathlib relative_to raises exception when one path is not a direct + # descendant of the other when os.path.relpath would return abspath. + # For DVC PathInfo we only need the relpath behavior. + # See: https://bugs.python.org/issue40358 + try: + path = super().relative_to(other) + except ValueError: + path = relpath(self, other) + return self.__class__(path) + class WindowsPathInfo(PathInfo, pathlib.PureWindowsPath): pass diff --git a/tests/unit/test_path_info.py b/tests/unit/test_path_info.py index 9f4ba3e1b..c4f6bc40e 100644 --- a/tests/unit/test_path_info.py +++ b/tests/unit/test_path_info.py @@ -1,4 +1,5 @@ import copy +import os import pathlib import pytest @@ -102,3 +103,17 @@ def test_url_replace_path(): assert u.params == new_u.params assert u.query == new_u.query assert u.fragment == new_u.fragment + + +@pytest.mark.parametrize( + "p1, p2", + [ + ("/foo/bar", "/baz"), + ("/baz", "/foo/bar"), + ("/foo/bar", "../baz"), + ("../baz", "/foo/bar"), + ], +) +def test_relative_to_different_subpath(p1, p2): + expected = PathInfo(os.path.relpath(p1, p2)) + assert PathInfo(p1).relative_to(PathInfo(p2)) == expected