diff --git a/dvc/command/pipeline.py b/dvc/command/pipeline.py index fe71ecb8a..99525fd9e 100644 --- a/dvc/command/pipeline.py +++ b/dvc/command/pipeline.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals +from dvc.utils import relpath from dvc.utils.compat import str import argparse -import os import logging from dvc.exceptions import DvcException @@ -21,7 +21,7 @@ class CmdPipelineShow(CmdBase): stage = Stage.load(self.repo, target) G = self.repo.graph()[0] stages = networkx.get_node_attributes(G, "stage") - node = os.path.relpath(stage.path, self.repo.root_dir) + node = relpath(stage.path, self.repo.root_dir) nodes = networkx.dfs_postorder_nodes(G, node) if locked: @@ -41,7 +41,7 @@ class CmdPipelineShow(CmdBase): from dvc.stage import Stage stage = Stage.load(self.repo, target) - node = os.path.relpath(stage.path, self.repo.root_dir) + node = relpath(stage.path, self.repo.root_dir) pipelines = list( filter(lambda g: node in g.nodes(), self.repo.pipelines()) diff --git a/dvc/command/root.py b/dvc/command/root.py index b52364cbb..183d4af38 100644 --- a/dvc/command/root.py +++ b/dvc/command/root.py @@ -1,19 +1,18 @@ from __future__ import unicode_literals import argparse -import os import logging from dvc.repo import Repo from dvc.command.base import CmdBaseNoRepo, append_doc_link - +from dvc.utils import relpath logger = logging.getLogger(__name__) class CmdRoot(CmdBaseNoRepo): def run(self): - logger.info(os.path.relpath(Repo.find_root())) + logger.info(relpath(Repo.find_root())) return 0 diff --git a/dvc/exceptions.py b/dvc/exceptions.py index 98f4a8c56..870dbd542 100644 --- a/dvc/exceptions.py +++ b/dvc/exceptions.py @@ -4,9 +4,10 @@ from __future__ import unicode_literals from dvc.utils.compat import str, builtin_str -import os import traceback +from dvc.utils import relpath + class DvcException(Exception): """Base class for all dvc exceptions. @@ -62,7 +63,7 @@ class OutputNotFoundError(DvcException): def __init__(self, output): super(OutputNotFoundError, self).__init__( "unable to find stage file with output '{path}'".format( - path=os.path.relpath(output) + path=relpath(output) ) ) @@ -198,7 +199,7 @@ class BadMetricError(DvcException): def __init__(self, path): super(BadMetricError, self).__init__( "'{}' does not exist, not a metric or is malformed".format( - os.path.relpath(path) + relpath(path) ) ) @@ -213,7 +214,7 @@ class NoMetricsError(DvcException): class StageFileCorruptedError(DvcException): def __init__(self, path, cause=None): - path = os.path.relpath(path) + path = relpath(path) super(StageFileCorruptedError, self).__init__( "unable to read stage file: {} " "YAML file structure is corrupted".format(path), diff --git a/dvc/ignore.py b/dvc/ignore.py index ce2bcefce..f74815e8f 100644 --- a/dvc/ignore.py +++ b/dvc/ignore.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import os from dulwich.ignore import match_pattern, read_ignore_patterns +from dvc.utils import relpath from dvc.utils.compat import cast_bytes from dvc.utils.fs import get_parent_dirs_up_to @@ -55,15 +56,15 @@ class DvcIgnoreFromFile(DvcIgnore): return dirs, files def get_match(self, abs_path): - rel_path = os.path.relpath(abs_path, self.dirname) + relative_path = relpath(abs_path, self.dirname) if os.name == "nt": - rel_path = rel_path.replace("\\", "/") - rel_path = cast_bytes(rel_path, "utf-8") + relative_path = relative_path.replace("\\", "/") + relative_path = cast_bytes(relative_path, "utf-8") for pattern in self.patterns: if match_pattern( - rel_path, pattern - ) and self._no_negate_pattern_matches(rel_path): + relative_path, pattern + ) and self._no_negate_pattern_matches(relative_path): return (abs_path, pattern, self.ignore_file_path) return None diff --git a/dvc/path_info.py b/dvc/path_info.py index a4c5a60d7..99f6d915c 100644 --- a/dvc/path_info.py +++ b/dvc/path_info.py @@ -6,12 +6,14 @@ import posixpath from funcy import cached_property from dvc.utils.compat import str, builtin_str, basestring, is_py2 -from dvc.utils.compat import pathlib, fspath_py35, urlparse +from dvc.utils.compat import pathlib, urlparse # On Python 2.7/Windows sys.getfilesystemencoding() is set to mbcs, # which is lossy, thus we can't use that, # see https://github.com/mcmtroffaes/pathlib2/issues/56. +from dvc.utils import relpath + if is_py2: fs_encoding = "utf-8" @@ -31,10 +33,7 @@ class PathInfo(pathlib.PurePath): def __str__(self): path = self.__fspath__() - # Windows path on different drive than curdir doesn't have relpath - if os.name == "nt" and not os.path.commonprefix([os.getcwd(), path]): - return path - return os.path.relpath(path) + return relpath(path) def __repr__(self): return builtin_str("{}: '{}'").format(type(self).__name__, self) @@ -51,9 +50,7 @@ class PathInfo(pathlib.PurePath): url = fspath def relpath(self, other): - return self.__class__( - os.path.relpath(fspath_py35(self), fspath_py35(other)) - ) + return self.__class__(relpath(self, other)) def isin(self, other): if isinstance(other, basestring): diff --git a/dvc/remote/base.py b/dvc/remote/base.py index d2bfd44b2..5f2659fc5 100644 --- a/dvc/remote/base.py +++ b/dvc/remote/base.py @@ -16,7 +16,7 @@ import dvc.prompt as prompt from dvc.config import Config from dvc.exceptions import DvcException, ConfirmRemoveError from dvc.progress import progress, ProgressCallback -from dvc.utils import LARGE_DIR_SIZE, tmp_fname, to_chunks, move +from dvc.utils import LARGE_DIR_SIZE, tmp_fname, to_chunks, move, relpath from dvc.state import StateBase from dvc.path_info import PathInfo, URLInfo @@ -148,7 +148,7 @@ class RemoteBASE(object): for fname in files: file_info = self.path_cls(root) / fname - relpath = file_info.relative_to(path_info) + relative_path = file_info.relative_to(path_info) dir_info.append( { # NOTE: this is lossy transformation: @@ -159,7 +159,7 @@ class RemoteBASE(object): # # Yes, this is a BUG, as long as we permit "/" in # filenames on Windows and "\" on Unix - self.PARAM_RELPATH: relpath.as_posix(), + self.PARAM_RELPATH: relative_path.as_posix(), self.PARAM_CHECKSUM: self.get_file_checksum(file_info), } ) @@ -221,13 +221,13 @@ class RemoteBASE(object): if not isinstance(d, list): msg = "dir cache file format error '{}' [skipping the file]" - logger.error(msg.format(os.path.relpath(path))) + logger.error(msg.format(relpath(path))) return [] for info in d: # NOTE: here is a BUG, see comment to .as_posix() below - relpath = self.path_cls.from_posix(info[self.PARAM_RELPATH]) - info[self.PARAM_RELPATH] = relpath.fspath + relative_path = self.path_cls.from_posix(info[self.PARAM_RELPATH]) + info[self.PARAM_RELPATH] = relative_path.fspath return d @@ -699,10 +699,10 @@ class RemoteBASE(object): logger.debug("Linking directory '{}'.".format(path_info)) for entry in dir_info: - relpath = entry[self.PARAM_RELPATH] + relative_path = entry[self.PARAM_RELPATH] entry_checksum = entry[self.PARAM_CHECKSUM] entry_cache_info = self.checksum_to_path_info(entry_checksum) - entry_info = path_info / relpath + entry_info = path_info / relative_path entry_checksum_info = {self.PARAM_CHECKSUM: entry_checksum} if self.changed(entry_info, entry_checksum_info): diff --git a/dvc/remote/config.py b/dvc/remote/config.py index 54b33ed2b..99a841f67 100644 --- a/dvc/remote/config.py +++ b/dvc/remote/config.py @@ -2,6 +2,7 @@ import os import logging import posixpath +from dvc.utils import relpath from dvc.utils.compat import urlparse from dvc.config import Config, ConfigError @@ -85,7 +86,7 @@ class RemoteConfig(object): """ if os.path.isabs(path): return path - return os.path.relpath(path, os.path.dirname(config_file)) + return relpath(path, os.path.dirname(config_file)) def add(self, name, url, default=False, force=False, level=None): from dvc.remote import _get, RemoteLOCAL diff --git a/dvc/remote/local/__init__.py b/dvc/remote/local/__init__.py index 8c37e7a51..b35d326a2 100644 --- a/dvc/remote/local/__init__.py +++ b/dvc/remote/local/__init__.py @@ -28,6 +28,7 @@ from dvc.utils import ( tmp_fname, file_md5, walk_files, + relpath, ) from dvc.config import Config from dvc.exceptions import DvcException @@ -464,11 +465,7 @@ class RemoteLOCAL(RemoteBASE): # would get only the part of file. So, at first, the file should be # copied with the temporary name, and then original file should be # replaced by new. - copyfile( - path, - tmp, - name="Unprotecting '{}'".format(os.path.relpath(path)), - ) + copyfile(path, tmp, name="Unprotecting '{}'".format(relpath(path))) remove(path) os.rename(tmp, path) @@ -536,9 +533,9 @@ class RemoteLOCAL(RemoteBASE): entry_cache_info = self.checksum_to_path_info( entry[self.PARAM_CHECKSUM] ) - relpath = entry[self.PARAM_RELPATH] + relative_path = entry[self.PARAM_RELPATH] self.link( - entry_cache_info, unpacked_dir_info / relpath, "hardlink" + entry_cache_info, unpacked_dir_info / relative_path, "hardlink" ) self.state.save(unpacked_dir_info, checksum) diff --git a/dvc/repo/__init__.py b/dvc/repo/__init__.py index b2e2dfad8..a5f2624e9 100644 --- a/dvc/repo/__init__.py +++ b/dvc/repo/__init__.py @@ -12,6 +12,7 @@ from dvc.exceptions import ( from dvc.ignore import DvcIgnoreFileHandler from dvc.path_info import PathInfo from dvc.utils.compat import open as _open +from dvc.utils import relpath logger = logging.getLogger(__name__) @@ -167,7 +168,7 @@ class Repo(object): if not with_deps: return [stage] - node = os.path.relpath(stage.path, self.root_dir) + node = relpath(stage.path, self.root_dir) G = self._get_pipeline(node) ret = [] @@ -324,7 +325,7 @@ class Repo(object): raise StagePathAsOutputError(stage.wdir, stage.relpath) for stage in stages: - node = os.path.relpath(stage.path, self.root_dir) + node = relpath(stage.path, self.root_dir) G.add_node(node, stage=stage) G_active.add_node(node, stage=stage) @@ -339,7 +340,7 @@ class Repo(object): continue dep_stage = out.stage - dep_node = os.path.relpath(dep_stage.path, self.root_dir) + dep_node = relpath(dep_stage.path, self.root_dir) G.add_node(dep_node, stage=dep_stage) G.add_edge(node, dep_node) if not stage.locked: diff --git a/dvc/repo/init.py b/dvc/repo/init.py index ad03ad0b6..6ef450d5c 100644 --- a/dvc/repo/init.py +++ b/dvc/repo/init.py @@ -8,7 +8,7 @@ from dvc.scm import SCM, NoSCM from dvc.config import Config from dvc.exceptions import InitError from dvc.utils import boxify - +from dvc.utils import relpath logger = logging.getLogger(__name__) @@ -74,7 +74,7 @@ def init(root_dir=os.curdir, no_scm=False, force=False): if not force: raise InitError( "'{repo}' exists. Use '-f' to force.".format( - repo=os.path.relpath(dvc_dir) + repo=relpath(dvc_dir) ) ) diff --git a/dvc/repo/reproduce.py b/dvc/repo/reproduce.py index daa2e40da..bc8b70775 100644 --- a/dvc/repo/reproduce.py +++ b/dvc/repo/reproduce.py @@ -5,7 +5,7 @@ import logging from dvc.exceptions import ReproductionError from dvc.repo.scm_context import scm_context - +from dvc.utils import relpath logger = logging.getLogger(__name__) @@ -67,7 +67,7 @@ def reproduce( elif pipeline or all_pipelines: if pipeline: stage = Stage.load(self, target) - node = os.path.relpath(stage.path, self.root_dir) + node = relpath(stage.path, self.root_dir) pipelines = [self._get_pipeline(node)] else: pipelines = self.pipelines() @@ -115,7 +115,7 @@ def _reproduce( stage = Stage.load(self, target) G = self.graph()[1] stages = nx.get_node_attributes(G, "stage") - node = os.path.relpath(stage.path, self.root_dir) + node = relpath(stage.path, self.root_dir) if single_item: ret = _reproduce_stage( diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py index 38c9022ff..ed70e6e30 100644 --- a/dvc/scm/git/__init__.py +++ b/dvc/scm/git/__init__.py @@ -6,7 +6,7 @@ import os import logging from dvc.utils.compat import str, open -from dvc.utils import fix_env +from dvc.utils import fix_env, relpath from dvc.scm.base import ( Base, SCMError, @@ -92,7 +92,7 @@ class Git(Base): msg.format(self.GITIGNORE, path, ignore_file_dir) ) - entry = os.path.relpath(path, ignore_file_dir).replace(os.sep, "/") + entry = relpath(path, ignore_file_dir).replace(os.sep, "/") # NOTE: using '/' prefix to make path unambiguous if len(entry) > 0 and entry[0] != "/": entry = "/" + entry @@ -125,14 +125,12 @@ class Git(Base): if self._ignored(entry, gitignore): return - msg = "Adding '{}' to '{}'.".format( - os.path.relpath(path), os.path.relpath(gitignore) - ) + msg = "Adding '{}' to '{}'.".format(relpath(path), relpath(gitignore)) logger.info(msg) self._add_entry_to_gitignore(entry, gitignore) - self.track_file(os.path.relpath(gitignore)) + self.track_file(relpath(gitignore)) self.ignored_paths.append(path) @@ -163,7 +161,7 @@ class Git(Base): with open(gitignore, "w") as fobj: fobj.writelines(filtered) - self.track_file(os.path.relpath(gitignore)) + self.track_file(relpath(gitignore)) def add(self, paths): # NOTE: GitPython is not currently able to handle index version >= 3. @@ -202,7 +200,7 @@ class Git(Base): def is_tracked(self, path): # it is equivalent to `bool(self.git.git.ls_files(path))` by # functionality, but ls_files fails on unicode filenames - path = os.path.relpath(path, self.root_dir) + path = relpath(path, self.root_dir) return path in [i[0] for i in self.git.index.entries] def is_dirty(self): diff --git a/dvc/scm/git/tree.py b/dvc/scm/git/tree.py index 2ef8016ab..e00a8b18c 100644 --- a/dvc/scm/git/tree.py +++ b/dvc/scm/git/tree.py @@ -2,6 +2,7 @@ import errno import os from dvc.ignore import DvcIgnoreFilter +from dvc.utils import relpath from dvc.utils.compat import StringIO, BytesIO from dvc.exceptions import DvcException @@ -32,14 +33,14 @@ class GitTree(BaseTree): def open(self, path, binary=False): - relpath = os.path.relpath(path, self.git.working_dir) + relative_path = relpath(path, self.git.working_dir) obj = self.git_object_by_path(path) if obj is None: msg = "No such file in branch '{}'".format(self.rev) - raise IOError(errno.ENOENT, msg, relpath) + raise IOError(errno.ENOENT, msg, relative_path) if obj.mode == GIT_MODE_DIR: - raise IOError(errno.EISDIR, "Is a directory", relpath) + raise IOError(errno.EISDIR, "Is a directory", relative_path) # GitPython's obj.data_stream is a fragile thing, it is better to # read it immediately, also it needs to be to decoded if we follow @@ -98,7 +99,7 @@ class GitTree(BaseTree): def git_object_by_path(self, path): import git - path = os.path.relpath(os.path.realpath(path), self.git.working_dir) + path = relpath(os.path.realpath(path), self.git.working_dir) assert path.split(os.sep, 1)[0] != ".." self._try_fetch_from_remote() diff --git a/dvc/stage.py b/dvc/stage.py index f5feccf0c..93831657e 100644 --- a/dvc/stage.py +++ b/dvc/stage.py @@ -8,6 +8,7 @@ import os import subprocess import logging +from dvc.utils import relpath from dvc.utils.compat import pathlib from dvc.utils.fs import contains_symlink_up_to from schema import Schema, SchemaError, Optional, Or, And @@ -176,7 +177,7 @@ class Stage(object): @property def relpath(self): - return os.path.relpath(self.path) + return relpath(self.path) @property def is_data_source(self): @@ -553,7 +554,7 @@ class Stage(object): raise StageFileBadNameError( "bad stage filename '{}'. Stage files should be named" " 'Dvcfile' or have a '.dvc' suffix (e.g. '{}.dvc').".format( - os.path.relpath(fname), os.path.basename(fname) + relpath(fname), os.path.basename(fname) ) ) @@ -593,7 +594,7 @@ class Stage(object): # looses keys in deps and outs load state = copy.deepcopy(d) - Stage.validate(d, fname=os.path.relpath(fname)) + Stage.validate(d, fname=relpath(fname)) path = os.path.abspath(fname) stage = Stage( @@ -617,7 +618,7 @@ class Stage(object): return stage def dumpd(self): - rel_wdir = os.path.relpath(self.wdir, os.path.dirname(self.path)) + rel_wdir = relpath(self.wdir, os.path.dirname(self.path)) return { key: value for key, value in { @@ -638,15 +639,13 @@ class Stage(object): self._check_dvc_filename(fname) logger.info( - "Saving information to '{file}'.".format( - file=os.path.relpath(fname) - ) + "Saving information to '{file}'.".format(file=relpath(fname)) ) d = self.dumpd() apply_diff(d, self._state) dump_stage_file(fname, self._state) - self.repo.scm.track_file(os.path.relpath(fname)) + self.repo.scm.track_file(relpath(fname)) def _compute_md5(self): from dvc.output.base import OutputBase diff --git a/dvc/state.py b/dvc/state.py index 2999bf7e3..39b77c5e1 100644 --- a/dvc/state.py +++ b/dvc/state.py @@ -7,7 +7,7 @@ import sqlite3 import logging from dvc.config import Config -from dvc.utils import remove, current_timestamp +from dvc.utils import remove, current_timestamp, relpath from dvc.exceptions import DvcException from dvc.utils.fs import get_mtime_and_size, get_inode from dvc.utils.compat import fspath_py35 @@ -416,12 +416,15 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes mtime, _ = get_mtime_and_size(path) inode = get_inode(path) - relpath = os.path.relpath(path, self.root_dir) + relative_path = relpath(path, self.root_dir) cmd = ( "REPLACE INTO {}(path, inode, mtime) " 'VALUES ("{}", {}, "{}")'.format( - self.LINK_STATE_TABLE, relpath, self._to_sqlite(inode), mtime + self.LINK_STATE_TABLE, + relative_path, + self._to_sqlite(inode), + mtime, ) ) self._execute(cmd) diff --git a/dvc/utils/__init__.py b/dvc/utils/__init__.py index 4bf2ab2c6..0c513b88e 100644 --- a/dvc/utils/__init__.py +++ b/dvc/utils/__init__.py @@ -2,7 +2,14 @@ from __future__ import unicode_literals -from dvc.utils.compat import str, builtin_str, open, cast_bytes_py2, StringIO +from dvc.utils.compat import ( + str, + builtin_str, + open, + cast_bytes_py2, + StringIO, + fspath_py35, +) from dvc.utils.compat import fspath import os @@ -46,8 +53,8 @@ def file_md5(fname): if size >= LARGE_FILE_SIZE: bar = True msg = "Computing md5 for a large file {}. This is only done once." - logger.info(msg.format(os.path.relpath(fname))) - name = os.path.relpath(fname) + logger.info(msg.format(relpath(fname))) + name = relpath(fname) total = 0 with open(fname, "rb") as fobj: @@ -170,7 +177,7 @@ def _chmod(func, p, excinfo): def remove(path): - logger.debug("Removing '{}'".format(os.path.relpath(path))) + logger.debug("Removing '{}'".format(relpath(path))) try: if os.path.isdir(path): @@ -369,3 +376,15 @@ def _visual_center(line, width): right_padding = spaces - left_padding return (left_padding * " ") + line + (right_padding * " ") + + +def relpath(path, start=os.curdir): + path = fspath_py35(path) + start = fspath_py35(os.path.abspath(start)) + + # Windows path on different drive than curdir doesn't have relpath + if os.name == "nt" and not os.path.commonprefix( + [start, os.path.abspath(path)] + ): + return path + return os.path.relpath(path, start) diff --git a/tests/basic_env.py b/tests/basic_env.py index 44248a2be..17fc28afd 100644 --- a/tests/basic_env.py +++ b/tests/basic_env.py @@ -83,10 +83,12 @@ class TestDirFixture(object): ) @staticmethod - def mkdtemp(): + def mkdtemp(base_directory=None): prefix = "dvc-test.{}.".format(os.getpid()) suffix = ".{}".format(uuid.uuid4()) - return tempfile.mkdtemp(prefix=prefix, suffix=suffix) + return tempfile.mkdtemp( + prefix=prefix, suffix=suffix, dir=base_directory + ) def setUp(self): self._pushd(self._root_dir) diff --git a/tests/conftest.py b/tests/conftest.py index 522baf3fe..7698fb06e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -126,7 +126,9 @@ def temporary_windows_drive(repo_dir): if set_up_result == 0: raise RuntimeError("Failed to mount windows drive!") - yield new_drive + # NOTE: new_drive has form of `A:` and joining it with some relative + # path might result in non-existing path (A:path\\to) + yield os.path.join(new_drive, os.sep) tear_down_result = windll.kernel32.DefineDosDeviceW( DDD_REMOVE_DEFINITION, new_drive, target_path diff --git a/tests/func/test_add.py b/tests/func/test_add.py index 9cef92c8c..07e243040 100644 --- a/tests/func/test_add.py +++ b/tests/func/test_add.py @@ -16,7 +16,7 @@ from dvc.system import System from mock import patch from dvc.main import main -from dvc.utils import file_md5, LARGE_DIR_SIZE +from dvc.utils import file_md5, LARGE_DIR_SIZE, relpath from dvc.utils.stage import load_stage_file from dvc.utils.compat import range from dvc.stage import Stage @@ -421,7 +421,7 @@ class TestShouldThrowProperExceptionOnCorruptedStageFile(TestDvc): ret = main(["add", self.FOO]) assert 0 == ret - foo_stage = os.path.relpath(self.FOO + Stage.STAGE_FILE_SUFFIX) + foo_stage = relpath(self.FOO + Stage.STAGE_FILE_SUFFIX) # corrupt stage file with open(foo_stage, "a+") as file: diff --git a/tests/func/test_cache.py b/tests/func/test_cache.py index 96297e6f3..beec48991 100644 --- a/tests/func/test_cache.py +++ b/tests/func/test_cache.py @@ -4,6 +4,7 @@ import configobj from dvc.cache import Cache from dvc.main import main +from dvc.utils import relpath from tests.basic_env import TestDvc, TestDir @@ -152,7 +153,7 @@ class TestCmdCacheDir(TestDvc): def test_relative_path(self): tmpdir = self.mkdtemp() - dname = os.path.relpath(tmpdir) + dname = relpath(tmpdir) ret = main(["cache", "dir", dname]) self.assertEqual(ret, 0) diff --git a/tests/func/test_checkout.py b/tests/func/test_checkout.py index 2ac817a9f..437927bc4 100644 --- a/tests/func/test_checkout.py +++ b/tests/func/test_checkout.py @@ -11,7 +11,7 @@ from dvc.main import main from dvc import progress from dvc.repo import Repo as DvcRepo from dvc.system import System -from dvc.utils import walk_files +from dvc.utils import walk_files, relpath from dvc.utils.stage import load_stage_file, dump_stage_file from tests.basic_env import TestDvc from tests.func.test_repro import TestRepro @@ -566,11 +566,11 @@ class TestCheckoutMovedCacheDirWithSymlinks(TestDvc): new_data_link = readlink(self.DATA) self.assertEqual( - os.path.relpath(old_foo_link, old_cache_dir), - os.path.relpath(new_foo_link, new_cache_dir), + relpath(old_foo_link, old_cache_dir), + relpath(new_foo_link, new_cache_dir), ) self.assertEqual( - os.path.relpath(old_data_link, old_cache_dir), - os.path.relpath(new_data_link, new_cache_dir), + relpath(old_data_link, old_cache_dir), + relpath(new_data_link, new_cache_dir), ) diff --git a/tests/func/test_metrics.py b/tests/func/test_metrics.py index 526f7877d..806afcca7 100644 --- a/tests/func/test_metrics.py +++ b/tests/func/test_metrics.py @@ -10,6 +10,7 @@ from dvc.main import main from dvc.exceptions import DvcException, BadMetricError, NoMetricsError from dvc.repo.metrics.show import NO_METRICS_FILE_AT_REFERENCE_WARNING from dvc.stage import Stage +from dvc.utils import relpath from tests.basic_env import TestDvc @@ -342,7 +343,7 @@ class TestMetrics(TestMetricsBase): self.assertMetricsHaveRelativePaths(metrics) def assertMetricsHaveRelativePaths(self, metrics): - root_relpath = os.path.relpath(self.dvc.root_dir) + root_relpath = relpath(self.dvc.root_dir) metric_path = os.path.join(root_relpath, "metric") metric_json_path = os.path.join(root_relpath, "metric_json") metric_tsv_path = os.path.join(root_relpath, "metric_tsv") diff --git a/tests/func/test_repro.py b/tests/func/test_repro.py index b253f3ba4..1f34a7b71 100644 --- a/tests/func/test_repro.py +++ b/tests/func/test_repro.py @@ -18,7 +18,7 @@ import pytest from dvc.main import main from dvc.repo import Repo as DvcRepo -from dvc.utils import file_md5 +from dvc.utils import file_md5, relpath from dvc.utils.stage import load_stage_file, dump_stage_file from dvc.remote.local import RemoteLOCAL from dvc.stage import Stage, StageFileDoesNotExistError @@ -142,7 +142,7 @@ class TestReproWorkingDirectoryAsOutput(TestDvc): os.mkdir(dir2) nested_dir = os.path.join(dir2, "nested") - out_dir = os.path.relpath(nested_dir, dir1) + out_dir = relpath(nested_dir, dir1) nested_stage = self.dvc.run( cwd=dir1, # b diff --git a/tests/unit/utils/test_fs.py b/tests/unit/utils/test_fs.py index dc2f034aa..6924856c1 100644 --- a/tests/unit/utils/test_fs.py +++ b/tests/unit/utils/test_fs.py @@ -4,6 +4,7 @@ from unittest import TestCase import dvc import pytest from dvc.system import System +from dvc.utils import relpath from dvc.utils.compat import str from dvc.utils.fs import ( get_mtime_and_size, @@ -135,3 +136,11 @@ def test_get_parent_dirs_up_to(path1, path2, expected_dirs): result = get_parent_dirs_up_to(path1, path2) assert set(result) == set(expected_dirs) + + +@pytest.mark.skipif(os.name != "nt", reason="Windows specific") +def test_relpath_windows_different_drives(): + path1 = os.path.join("A:", os.sep, "some", "path") + path2 = os.path.join("B:", os.sep, "other", "path") + + assert relpath(path1, path2) == path1