diff --git a/dvc/config.py b/dvc/config.py index 0ea2408ab..77f086c14 100644 --- a/dvc/config.py +++ b/dvc/config.py @@ -103,7 +103,7 @@ class Config(dict): self.fs = fs or self.wfs if dvc_dir: - self.dvc_dir = self.fs.path.abspath(self.fs.path.realpath(dvc_dir)) + self.dvc_dir = self.fs.path.abspath(dvc_dir) self.load( validate=validate, config=config, remote=remote, remote_config=remote_config diff --git a/dvc/exceptions.py b/dvc/exceptions.py index 8192365d9..5c6b2844d 100644 --- a/dvc/exceptions.py +++ b/dvc/exceptions.py @@ -71,6 +71,10 @@ class OutputNotFoundError(DvcException): ) +class StageNotFoundError(DvcException): + pass + + class StagePathAsOutputError(DvcException): """Thrown if directory that stage is going to be saved in is specified as an output of another stage. diff --git a/dvc/repo/__init__.py b/dvc/repo/__init__.py index c3ee34512..69475d8b9 100644 --- a/dvc/repo/__init__.py +++ b/dvc/repo/__init__.py @@ -421,7 +421,7 @@ class Repo: fs = fs or localfs root = root or os.curdir - root_dir = fs.path.realpath(root) + root_dir = fs.path.abspath(root) if not fs.isdir(root_dir): raise NotDvcRepoError(f"directory '{root}' does not exist") diff --git a/dvc/repo/experiments/executor/base.py b/dvc/repo/experiments/executor/base.py index bbabd56a8..a9f95c5c4 100644 --- a/dvc/repo/experiments/executor/base.py +++ b/dvc/repo/experiments/executor/base.py @@ -607,7 +607,7 @@ class BaseExecutor(ABC): old_cwd = os.getcwd() for path in copy_paths or []: - cls._copy_path(os.path.realpath(path), os.path.join(dvc.root_dir, path)) + cls._copy_path(os.path.abspath(path), os.path.join(dvc.root_dir, path)) if info.wdir: os.chdir(os.path.join(dvc.scm.root_dir, info.wdir)) diff --git a/dvc/repo/graph.py b/dvc/repo/graph.py index 56ea1e0ed..3ee0d4e3f 100644 --- a/dvc/repo/graph.py +++ b/dvc/repo/graph.py @@ -31,6 +31,9 @@ def check_acyclic(graph: "DiGraph") -> None: def get_pipeline(pipelines, node): found = [i for i in pipelines if i.has_node(node)] + if not found: + return None + assert len(found) == 1 return found[0] @@ -60,6 +63,9 @@ def collect_pipeline(stage: "Stage", graph: "DiGraph") -> Iterator["Stage"]: import networkx as nx pipeline = get_pipeline(get_pipelines(graph), stage) + if not pipeline: + return iter([]) + return nx.dfs_postorder_nodes(pipeline, stage) diff --git a/dvc/repo/init.py b/dvc/repo/init.py index 4a68a2bb2..086813982 100644 --- a/dvc/repo/init.py +++ b/dvc/repo/init.py @@ -37,7 +37,7 @@ def init(root_dir=os.curdir, no_scm=False, force=False, subdir=False): # noqa: "Cannot initialize repo with `--no-scm` and `--subdir`" ) - root_dir = os.path.realpath(root_dir) + root_dir = os.path.abspath(root_dir) dvc_dir = os.path.join(root_dir, Repo.DVC_DIR) try: diff --git a/dvc/repo/stage.py b/dvc/repo/stage.py index 321101934..a62ed845b 100644 --- a/dvc/repo/stage.py +++ b/dvc/repo/stage.py @@ -37,11 +37,19 @@ StageSet = Set["Stage"] def _collect_with_deps(stages: StageList, graph: "DiGraph") -> StageSet: + from dvc.exceptions import StageNotFoundError from dvc.repo.graph import collect_pipeline res: StageSet = set() for stage in stages: - res.update(collect_pipeline(stage, graph=graph)) + pl = list(collect_pipeline(stage, graph=graph)) + if not pl: + raise StageNotFoundError( + f"Stage {stage} is not found in the project. " + "Check that there are no symlinks in the parents " + "leading up to it within the project." + ) + res.update(pl) return res @@ -211,7 +219,7 @@ class StageLoad: self, path: Optional[str] = None, name: Optional[str] = None ) -> str: if path: - return self.repo.fs.path.realpath(path) + return self.repo.fs.path.abspath(path) path = PROJECT_FILE logger.debug("Assuming '%s' to be a stage inside '%s'", name, path) diff --git a/dvc/stage/utils.py b/dvc/stage/utils.py index 29cd35f48..3a104750d 100644 --- a/dvc/stage/utils.py +++ b/dvc/stage/utils.py @@ -32,14 +32,14 @@ def check_stage_path(repo, path, is_wdir=False): wdir_or_path="stage working dir" if is_wdir else "file path", path=path ) - real_path = os.path.realpath(path) + real_path = os.path.abspath(path) if not os.path.exists(real_path): raise StagePathNotFoundError(error_msg.format("does not exist")) if not os.path.isdir(real_path): raise StagePathNotDirectoryError(error_msg.format("is not directory")) - proj_dir = os.path.realpath(repo.root_dir) + proj_dir = os.path.abspath(repo.root_dir) if real_path != proj_dir and not path_isin(real_path, proj_dir): raise StagePathOutsideError(error_msg.format("is outside of DVC repo")) diff --git a/dvc/utils/__init__.py b/dvc/utils/__init__.py index 94c41b8e5..4022691d4 100644 --- a/dvc/utils/__init__.py +++ b/dvc/utils/__init__.py @@ -218,23 +218,13 @@ def _visual_center(line, width): def relpath(path, start=os.curdir): - path = os.fspath(path) + path = os.path.abspath(os.fspath(path)) start = os.path.abspath(os.fspath(start)) # Windows path on different drive than curdir doesn't have relpath - if os.name == "nt": - # Since python 3.8 os.realpath resolves network shares to their UNC - # path. So, to be certain that relative paths correctly captured, - # we need to resolve to UNC path first. We resolve only the drive - # name so that we don't follow any 'real' symlinks on the path - def resolve_network_drive_windows(path_to_resolve): - drive, tail = os.path.splitdrive(path_to_resolve) - return os.path.join(os.path.realpath(drive), tail) + if os.name == "nt" and not os.path.commonprefix([start, path]): + return path - path = resolve_network_drive_windows(os.path.abspath(path)) - start = resolve_network_drive_windows(start) - if not os.path.commonprefix([start, path]): - return path return os.path.relpath(path, start) diff --git a/tests/func/test_stage.py b/tests/func/test_stage.py index 76ffc1500..51814fcfc 100644 --- a/tests/func/test_stage.py +++ b/tests/func/test_stage.py @@ -230,17 +230,31 @@ def test_parent_repo_collect_stages(tmp_dir, scm, dvc): @pytest.mark.parametrize("with_deps", (False, True)) def test_collect_symlink(tmp_dir, dvc, with_deps): + from dvc.exceptions import StageNotFoundError + tmp_dir.gen({"data": {"foo": "foo contents"}}) foo_path = os.path.join("data", "foo") dvc.add(foo_path) data_link = tmp_dir / "data_link" data_link.symlink_to("data") - stage = list( - dvc.stage.collect(target=str(data_link / "foo.dvc"), with_deps=with_deps) - )[0] - assert stage.addressing == f"{foo_path}.dvc" + if with_deps: + # NOTE: with_deps means that we'll need to collect and use dvcfiles in the repo + # and we currently don't follow symlinks when collecting those, so it will not + # be able to find the target stage. + with pytest.raises(StageNotFoundError): + dvc.stage.collect(target=str(data_link / "foo.dvc"), with_deps=with_deps) + else: + stage = list( + dvc.stage.collect(target=str(data_link / "foo.dvc"), with_deps=with_deps) + )[0] + + assert stage.addressing == os.path.join("data_link", "foo.dvc") + + stage = list(dvc.stage.collect(target=f"{foo_path}.dvc", with_deps=with_deps))[0] + + assert stage.addressing == os.path.join("data", "foo.dvc") def test_stage_strings_representation(tmp_dir, dvc, run_copy): diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 0e159d86e..3db70ff0a 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -40,19 +40,8 @@ def test_fix_env_pyenv(path, orig): @pytest.mark.skipif(os.name != "nt", reason="Windows specific") -def test_relpath_windows(monkeypatch): - """test that relpath correctly generated when run on a - windows network share. The drive mapped path is mapped - to a UNC path by os.path.realpath""" - - def dummy_realpath(path): - return path.replace("x:", "\\\\server\\share") - - monkeypatch.setattr(os.path, "realpath", dummy_realpath) - assert ( - relpath("x:\\dir1\\dir2\\file.txt", "\\\\server\\share\\dir1") - == "dir2\\file.txt" - ) +def test_relpath_windows(): + assert relpath("x:\\dir1\\dir2\\file.txt", "x:\\dir1") == "dir2\\file.txt" assert ( relpath("y:\\dir1\\dir2\\file.txt", "\\\\server\\share\\dir1")