addressing: allow stage name addressing without ":" (#3842)

* addressing: allow stage name addressing without colon

* repo: reorganize imports

* refactor/reorganize collect_granular

* add tests for stage addressing

* fix ds and cc issues

* fix unpack error in test

* tests: stage string representations

* exception: add error message

Also remove `cause` when throwing NoOutputOrStage, as
CheckoutErrorSuggestGit will start chaining all the messages
in cause of the exception.

* rename NoOutputOrStage NoOutputOrStageError

Co-authored-by: Ruslan Kuprieiev <[email protected]>
This commit is contained in:
Saugat Pachhai
2020-05-26 03:51:51 +03:00
committed by GitHub
co-authored by Ruslan Kuprieiev
parent b47998463f
commit 2fecc66cb9
24 changed files with 564 additions and 173 deletions
+2 -4
View File
@@ -10,11 +10,10 @@ logger = logging.getLogger(__name__)
class CmdPipelineShow(CmdBase): class CmdPipelineShow(CmdBase):
def _show(self, target, commands, outs, locked): def _show(self, target, commands, outs, locked):
import networkx import networkx
from dvc import dvcfile
from dvc.utils import parse_target from dvc.utils import parse_target
path, name = parse_target(target) path, name = parse_target(target)
stage = dvcfile.Dvcfile(self.repo, path).stages[name] stage = self.repo.get_stage(path, name)
G = self.repo.graph G = self.repo.graph
stages = networkx.dfs_postorder_nodes(G, stage) stages = networkx.dfs_postorder_nodes(G, stage)
if locked: if locked:
@@ -58,12 +57,11 @@ class CmdPipelineShow(CmdBase):
def _build_graph(self, target, commands=False, outs=False): def _build_graph(self, target, commands=False, outs=False):
import networkx import networkx
from dvc import dvcfile
from dvc.repo.graph import get_pipeline from dvc.repo.graph import get_pipeline
from dvc.utils import parse_target from dvc.utils import parse_target
path, name = parse_target(target) path, name = parse_target(target)
target_stage = dvcfile.Dvcfile(self.repo, path).stages[name] target_stage = self.repo.get_stage(path, name)
G = get_pipeline(self.repo.pipelines, target_stage) G = get_pipeline(self.repo.pipelines, target_stage)
nodes = set() nodes = set()
+12
View File
@@ -322,3 +322,15 @@ class RemoteCacheRequiredError(DvcException):
class IsADirectoryError(DvcException): class IsADirectoryError(DvcException):
"""Raised when a file operation is requested on a directory.""" """Raised when a file operation is requested on a directory."""
class NoOutputOrStageError(DvcException):
"""
Raised when the target is neither an output nor a stage name in dvc.yaml
"""
def __init__(self, target, file):
super().__init__(
f"'{target}' "
f"does not exist as an output or a stage name in '{file}'"
)
+95 -26
View File
@@ -1,3 +1,4 @@
import logging
import os import os
from contextlib import contextmanager from contextlib import contextmanager
from functools import wraps from functools import wraps
@@ -5,17 +6,25 @@ from functools import wraps
from funcy import cached_property, cat, first from funcy import cached_property, cat, first
from dvc.config import Config from dvc.config import Config
from dvc.dvcfile import PIPELINE_FILE, Dvcfile, is_valid_filename
from dvc.exceptions import FileMissingError from dvc.exceptions import FileMissingError
from dvc.exceptions import IsADirectoryError as DvcIsADirectoryError from dvc.exceptions import IsADirectoryError as DvcIsADirectoryError
from dvc.exceptions import NotDvcRepoError, OutputNotFoundError from dvc.exceptions import (
NoOutputOrStageError,
NotDvcRepoError,
OutputNotFoundError,
)
from dvc.ignore import CleanTree from dvc.ignore import CleanTree
from dvc.path_info import PathInfo from dvc.path_info import PathInfo
from dvc.repo.tree import RepoTree from dvc.repo.tree import RepoTree
from dvc.utils.fs import path_isin from dvc.utils.fs import path_isin
from ..stage.exceptions import StageFileDoesNotExistError, StageNotFound
from ..utils import parse_target from ..utils import parse_target
from .graph import check_acyclic, get_pipeline, get_pipelines from .graph import check_acyclic, get_pipeline, get_pipelines
logger = logging.getLogger(__name__)
def locked(f): def locked(f):
@wraps(f) @wraps(f)
@@ -181,6 +190,25 @@ class Repo:
self.scm.ignore_list(flist) self.scm.ignore_list(flist)
def get_stage(self, path=None, name=None):
if not path:
path = PIPELINE_FILE
logger.debug("Assuming '%s' to be a stage inside '%s'", name, path)
dvcfile = Dvcfile(self, path)
return dvcfile.stages[name]
def get_stages(self, path=None, name=None):
if not path:
path = PIPELINE_FILE
logger.debug("Assuming '%s' to be a stage inside '%s'", name, path)
if name:
return [self.get_stage(path, name)]
dvcfile = Dvcfile(self, path)
return list(dvcfile.stages.values())
def check_modified_graph(self, new_stages): def check_modified_graph(self, new_stages):
"""Generate graph including the new stage to check for errors""" """Generate graph including the new stage to check for errors"""
# Building graph might be costly for the ones with many DVC-files, # Building graph might be costly for the ones with many DVC-files,
@@ -204,10 +232,9 @@ class Repo:
stages = nx.dfs_postorder_nodes(graph) stages = nx.dfs_postorder_nodes(graph)
return [stage for stage in stages if path_isin(stage.path, path)] return [stage for stage in stages if path_isin(stage.path, path)]
def collect(self, target, with_deps=False, recursive=False, graph=None): def collect(
import networkx as nx self, target=None, with_deps=False, recursive=False, graph=None
from ..dvcfile import Dvcfile ):
if not target: if not target:
return list(graph) if graph else self.stages return list(graph) if graph else self.stages
@@ -217,36 +244,81 @@ class Repo:
) )
path, name = parse_target(target) path, name = parse_target(target)
dvcfile = Dvcfile(self, path) stages = self.get_stages(path, name)
stages = list(dvcfile.stages.filter(name).values())
if not with_deps: if not with_deps:
return stages return stages
res = set() res = set()
for stage in stages: for stage in stages:
pipeline = get_pipeline(get_pipelines(graph or self.graph), stage) res.update(self._collect_pipeline(stage, graph=graph))
res.update(nx.dfs_postorder_nodes(pipeline, stage))
return res return res
def collect_granular(self, target, *args, **kwargs): def _collect_pipeline(self, stage, graph=None):
from ..dvcfile import Dvcfile, is_valid_filename import networkx as nx
pipeline = get_pipeline(get_pipelines(graph or self.graph), stage)
return nx.dfs_postorder_nodes(pipeline, stage)
def _collect_from_default_dvcfile(self, target):
dvcfile = Dvcfile(self, PIPELINE_FILE)
if dvcfile.exists():
return dvcfile.stages.get(target)
def collect_granular(
self, target=None, with_deps=False, recursive=False, graph=None
):
"""
Priority is in the order of following in case of ambiguity:
- .dvc file or .yaml file
- dir if recursive and directory exists
- stage_name
- output file
"""
if not target: if not target:
return [(stage, None) for stage in self.stages] return [(stage, None) for stage in self.stages]
file, name = parse_target(target) file, name = parse_target(target)
if is_valid_filename(file) and not kwargs.get("with_deps"): stages = []
# Optimization: do not collect the graph for a specific .dvc target
stages = Dvcfile(self, file).stages.filter(name)
return [(stage, None) for stage in stages.values()]
try: # Optimization: do not collect the graph for a specific target
(out,) = self.find_outs_by_path(file, strict=False) if not file:
filter_info = PathInfo(os.path.abspath(file)) # parsing is ambiguous when it does not have a colon
return [(out.stage, filter_info)] # or if it's not a dvcfile, as it can be a stage name
except OutputNotFoundError: # in `dvc.yaml` or, an output in a stage.
stages = self.collect(target, *args, **kwargs) logger.debug(
return [(stage, None) for stage in stages] "Checking if stage '%s' is in '%s'", target, PIPELINE_FILE
)
if not (recursive and os.path.isdir(target)):
stage = self._collect_from_default_dvcfile(target)
if stage:
stages = (
self._collect_pipeline(stage) if with_deps else [stage]
)
elif not with_deps and is_valid_filename(file):
stages = self.get_stages(file, name)
if not stages:
if not (recursive and os.path.isdir(target)):
try:
(out,) = self.find_outs_by_path(target, strict=False)
filter_info = PathInfo(os.path.abspath(target))
return [(out.stage, filter_info)]
except OutputNotFoundError:
pass
try:
stages = self.collect(target, with_deps, recursive, graph)
except StageFileDoesNotExistError as exc:
# collect() might try to use `target` as a stage name
# and throw error that dvc.yaml does not exist, whereas it
# should say that both stage name and file does not exist.
if file and is_valid_filename(file):
raise
raise NoOutputOrStageError(target, exc.file) from exc
except StageNotFound as exc:
raise NoOutputOrStageError(target, exc.file) from exc
return [(stage, None) for stage in stages]
def used_cache( def used_cache(
self, self,
@@ -443,16 +515,13 @@ class Repo:
return PlotTemplates(self.dvc_dir) return PlotTemplates(self.dvc_dir)
def _collect_stages(self): def _collect_stages(self):
from dvc.dvcfile import Dvcfile, is_valid_filename
stages = [] stages = []
outs = set() outs = set()
for root, dirs, files in self.tree.walk(self.root_dir): for root, dirs, files in self.tree.walk(self.root_dir):
for file_name in filter(is_valid_filename, files): for file_name in filter(is_valid_filename, files):
path = os.path.join(root, file_name) path = os.path.join(root, file_name)
stage_loader = Dvcfile(self, path).stages stages.extend(self.get_stages(path))
stages.extend(stage_loader.values())
outs.update( outs.update(
out.fspath out.fspath
for stage in stages for stage in stages
+14 -8
View File
@@ -1,7 +1,11 @@
import logging import logging
import os import os
from dvc.exceptions import CheckoutError, CheckoutErrorSuggestGit from dvc.exceptions import (
CheckoutError,
CheckoutErrorSuggestGit,
NoOutputOrStageError,
)
from dvc.progress import Tqdm from dvc.progress import Tqdm
from dvc.utils import relpath from dvc.utils import relpath
@@ -18,11 +22,11 @@ def _get_unused_links(repo):
return repo.state.get_unused_links(used) return repo.state.get_unused_links(used)
def _fspath_dir(path, root): def _fspath_dir(path):
if not os.path.exists(str(path)): if not os.path.exists(str(path)):
return str(path) return str(path)
path = relpath(path, root) path = relpath(path)
return os.path.join(path, "") if os.path.isdir(path) else path return os.path.join(path, "") if os.path.isdir(path) else path
@@ -56,7 +60,7 @@ def _checkout(
targets = [None] targets = [None]
unused = _get_unused_links(self) unused = _get_unused_links(self)
stats["deleted"] = [_fspath_dir(u, self.root_dir) for u in unused] stats["deleted"] = [_fspath_dir(u) for u in unused]
self.state.remove_links(unused) self.state.remove_links(unused)
if isinstance(targets, str): if isinstance(targets, str):
@@ -70,7 +74,11 @@ def _checkout(
target, with_deps=with_deps, recursive=recursive target, with_deps=with_deps, recursive=recursive
) )
) )
except (StageFileDoesNotExistError, StageFileBadNameError) as exc: except (
StageFileDoesNotExistError,
StageFileBadNameError,
NoOutputOrStageError,
) as exc:
if not target: if not target:
raise raise
raise CheckoutErrorSuggestGit(target) from exc raise CheckoutErrorSuggestGit(target) from exc
@@ -87,9 +95,7 @@ def _checkout(
filter_info=filter_info, filter_info=filter_info,
) )
for key, items in result.items(): for key, items in result.items():
stats[key].extend( stats[key].extend(_fspath_dir(path) for path in items)
_fspath_dir(path, self.root_dir) for path in items
)
if stats.get("failed"): if stats.get("failed"):
raise CheckoutError(stats["failed"], stats) raise CheckoutError(stats["failed"], stats)
+1
View File
@@ -46,3 +46,4 @@ def commit(self, target, with_deps=False, recursive=False, force=False):
stage.commit() stage.commit()
Dvcfile(self, stage.path).dump(stage) Dvcfile(self, stage.path).dump(stage)
return stages
+2 -4
View File
@@ -3,13 +3,11 @@ from . import locked
@locked @locked
def lock(self, target, unlock=False): def lock(self, target, unlock=False):
from .. import dvcfile
from dvc.utils import parse_target from dvc.utils import parse_target
path, name = parse_target(target) path, name = parse_target(target)
dvcfile = dvcfile.Dvcfile(self, path) stage = self.get_stage(path, name)
stage = dvcfile.stages[name]
stage.locked = False if unlock else True stage.locked = False if unlock else True
dvcfile.dump(stage, update_pipeline=True) stage.dvcfile.dump(stage, update_pipeline=True)
return stage return stage
+4 -5
View File
@@ -8,15 +8,14 @@ logger = logging.getLogger(__name__)
@locked @locked
def remove(self, target, dvc_only=False): def remove(self, target, dvc_only=False):
from ..dvcfile import Dvcfile from ..dvcfile import Dvcfile, is_valid_filename
path, name = parse_target(target) path, name = parse_target(target)
dvcfile = Dvcfile(self, path) stages = self.get_stages(path, name)
stages = list(dvcfile.stages.filter(name).values())
for stage in stages: for stage in stages:
stage.remove_outs(force=True) stage.remove_outs(force=True)
if not dvc_only: if path and is_valid_filename(path) and not dvc_only:
dvcfile.remove() Dvcfile(self, path).remove()
return stages return stages
+1 -3
View File
@@ -61,7 +61,6 @@ def reproduce(
all_pipelines=False, all_pipelines=False,
**kwargs **kwargs
): ):
from ..dvcfile import Dvcfile
from dvc.utils import parse_target from dvc.utils import parse_target
if not target and not all_pipelines: if not target and not all_pipelines:
@@ -81,8 +80,7 @@ def reproduce(
if all_pipelines: if all_pipelines:
pipelines = active_pipelines pipelines = active_pipelines
else: else:
dvcfile = Dvcfile(self, path) stage = self.get_stage(path, name)
stage = dvcfile.stages[name]
pipelines = [get_pipeline(active_pipelines, stage)] pipelines = [get_pipeline(active_pipelines, stage)]
targets = [] targets = []
+8 -18
View File
@@ -144,14 +144,10 @@ class Stage(params.StageParams):
self._dvcfile = dvcfile self._dvcfile = dvcfile
def __repr__(self): def __repr__(self):
return "Stage: '{path}'".format( return f"Stage: '{self.addressing}'"
path=self.path_in_repo if self.path else "No path"
)
def __str__(self): def __str__(self):
return "stage: '{path}'".format( return f"stage: '{self.addressing}'"
path=self.relpath if self.path else "No path"
)
@property @property
def addressing(self): def addressing(self):
@@ -159,7 +155,7 @@ class Stage(params.StageParams):
Useful for alternative presentations where we don't need Useful for alternative presentations where we don't need
`Stage:` prefix. `Stage:` prefix.
""" """
return self.relpath return self.relpath if self.path else "No path"
def __hash__(self): def __hash__(self):
return hash(self.path_in_repo) return hash(self.path_in_repo)
@@ -530,19 +526,13 @@ class PipelineStage(Stage):
def __hash__(self): def __hash__(self):
return hash((self.path_in_repo, self.name)) return hash((self.path_in_repo, self.name))
def __repr__(self):
return "Stage: '{path}:{name}'".format(
path=self.relpath if self.path else "No path", name=self.name
)
def __str__(self):
return "stage: '{path}:{name}'".format(
path=self.relpath if self.path else "No path", name=self.name
)
@property @property
def addressing(self): def addressing(self):
return super().addressing + ":" + self.name from dvc.dvcfile import PIPELINE_FILE
if self.path and self.relpath == PIPELINE_FILE:
return self.name
return f"{super().addressing}:{self.name}"
def reload(self): def reload(self):
return self.dvcfile.stages[self.name] return self.dvcfile.stages[self.name]
+5 -10
View File
@@ -15,15 +15,8 @@ class StageFileFormatError(DvcException):
class StageFileDoesNotExistError(DvcException): class StageFileDoesNotExistError(DvcException):
def __init__(self, fname): def __init__(self, fname):
from dvc.dvcfile import DVC_FILE_SUFFIX, is_dvc_file self.file = fname
super().__init__(f"'{self.file}' does not exist.")
msg = f"'{fname}' does not exist."
sname = fname + DVC_FILE_SUFFIX
if is_dvc_file(sname):
msg += f" Do you mean '{sname}'?"
super().__init__(msg)
class StageFileAlreadyExistsError(DvcException): class StageFileAlreadyExistsError(DvcException):
@@ -87,8 +80,10 @@ class MissingDataSource(DvcException):
class StageNotFound(KeyError, DvcException): class StageNotFound(KeyError, DvcException):
def __init__(self, file, name): def __init__(self, file, name):
self.file = file.relpath
self.name = name
super().__init__( super().__init__(
f"Stage '{name}' not found inside '{file.relpath}' file" f"Stage '{self.name}' not found inside '{self.file}' file"
) )
-13
View File
@@ -30,16 +30,6 @@ class StageLoader(Mapping):
self.stages_data = stages_data or {} self.stages_data = stages_data or {}
self.lockfile_data = lockfile_data or {} self.lockfile_data = lockfile_data or {}
def filter(self, item=None):
if not item:
return self
if item not in self:
raise StageNotFound(self.dvcfile, item)
data = {item: self.stages_data[item]} if item in self else {}
return self.__class__(self.dvcfile, data, self.lockfile_data)
@staticmethod @staticmethod
def fill_from_lock(stage, lock_data): def fill_from_lock(stage, lock_data):
from .params import StageParams from .params import StageParams
@@ -224,9 +214,6 @@ class SingleStageLoader(Mapping):
self.stage_data = stage_data or {} self.stage_data = stage_data or {}
self.stage_text = stage_text self.stage_text = stage_text
def filter(self, item=None):
return self
def __getitem__(self, item): def __getitem__(self, item):
if item: if item:
logger.warning( logger.warning(
+12 -6
View File
@@ -395,7 +395,7 @@ def format_link(link):
def parse_target(target, default=None): def parse_target(target, default=None):
from dvc.dvcfile import PIPELINE_FILE, PIPELINE_LOCK from dvc.dvcfile import PIPELINE_FILE, PIPELINE_LOCK, is_valid_filename
from dvc.exceptions import DvcException from dvc.exceptions import DvcException
if not target: if not target:
@@ -409,13 +409,19 @@ def parse_target(target, default=None):
match.group("path"), match.group("path"),
match.group("name"), match.group("name"),
) )
if path:
if os.path.basename(path) == PIPELINE_LOCK:
raise DvcException(
"Did you mean: `{}`?".format(
target.replace(".lock", ".yaml", 1)
)
)
if not name:
ret = (target, None)
return ret if is_valid_filename(target) else ret[::-1]
if not path: if not path:
path = default or PIPELINE_FILE path = default or PIPELINE_FILE
logger.debug("Assuming file to be '%s'", path) logger.debug("Assuming file to be '%s'", path)
if os.path.basename(path) == PIPELINE_LOCK:
raise DvcException(
"Did you mean: `{}`?".format(target.replace(".lock", ".yaml", 1))
)
return path, name return path, name
+60 -11
View File
@@ -7,22 +7,20 @@ import shutil
import pytest import pytest
from mock import patch from mock import patch
from dvc.dvcfile import DVC_FILE_SUFFIX, Dvcfile from dvc.dvcfile import DVC_FILE_SUFFIX, PIPELINE_FILE, Dvcfile
from dvc.exceptions import ( from dvc.exceptions import (
CheckoutError, CheckoutError,
CheckoutErrorSuggestGit, CheckoutErrorSuggestGit,
ConfirmRemoveError, ConfirmRemoveError,
DvcException, DvcException,
NoOutputOrStageError,
) )
from dvc.main import main from dvc.main import main
from dvc.remote import S3Remote from dvc.remote import S3Remote
from dvc.remote.local import LocalRemote from dvc.remote.local import LocalRemote
from dvc.repo import Repo as DvcRepo from dvc.repo import Repo as DvcRepo
from dvc.stage import Stage from dvc.stage import Stage
from dvc.stage.exceptions import ( from dvc.stage.exceptions import StageFileDoesNotExistError
StageFileBadNameError,
StageFileDoesNotExistError,
)
from dvc.system import System from dvc.system import System
from dvc.utils import relpath from dvc.utils import relpath
from dvc.utils.fs import walk_files from dvc.utils.fs import walk_files
@@ -403,17 +401,26 @@ class TestCheckoutSuggestGit(TestRepro):
def test(self): def test(self):
try: try:
self.dvc.checkout(targets=["gitbranch"]) self.dvc.checkout(targets="gitbranch")
except DvcException as exc: except DvcException as exc:
self.assertIsInstance(exc, CheckoutErrorSuggestGit) self.assertIsInstance(exc, CheckoutErrorSuggestGit)
self.assertIsInstance(exc.__cause__, StageFileDoesNotExistError) self.assertIsInstance(exc.__cause__, NoOutputOrStageError)
self.assertIsInstance(
exc.__cause__.__cause__, StageFileDoesNotExistError
)
try:
self.dvc.checkout(targets=self.FOO)
except DvcException as exc:
self.assertIsInstance(exc, CheckoutErrorSuggestGit)
self.assertIsInstance(exc.__cause__, NoOutputOrStageError)
self.assertIsNone(exc.__cause__.__cause__) self.assertIsNone(exc.__cause__.__cause__)
try: try:
self.dvc.checkout(targets=[self.FOO]) self.dvc.checkout(targets="looks-like-dvcfile.dvc")
except DvcException as exc: except DvcException as exc:
self.assertIsInstance(exc, CheckoutErrorSuggestGit) self.assertIsInstance(exc, CheckoutErrorSuggestGit)
self.assertIsInstance(exc.__cause__, StageFileBadNameError) self.assertIsInstance(exc.__cause__, StageFileDoesNotExistError)
self.assertIsNone(exc.__cause__.__cause__) self.assertIsNone(exc.__cause__.__cause__)
@@ -770,9 +777,51 @@ def test_checkout_for_external_outputs(tmp_dir, dvc):
assert stats == {**empty_checkout, "modified": [str(file_path)]} assert stats == {**empty_checkout, "modified": [str(file_path)]}
def test_checkouts_for_pipeline_tracked_outs(tmp_dir, dvc, scm, run_copy): def test_checkouts_with_different_addressing(tmp_dir, dvc, run_copy):
from dvc.dvcfile import PIPELINE_FILE tmp_dir.gen({"foo": "foo", "lorem": "lorem"})
run_copy("foo", "bar", name="copy-foo-bar")
run_copy("lorem", "ipsum", name="copy-lorem-ipsum")
(tmp_dir / "bar").unlink()
(tmp_dir / "ipsum").unlink()
assert set(dvc.checkout(PIPELINE_FILE)["added"]) == {"bar", "ipsum"}
(tmp_dir / "bar").unlink()
(tmp_dir / "ipsum").unlink()
assert set(dvc.checkout(":")["added"]) == {"bar", "ipsum"}
(tmp_dir / "bar").unlink()
assert dvc.checkout("copy-foo-bar")["added"] == ["bar"]
(tmp_dir / "bar").unlink()
assert dvc.checkout("dvc.yaml:copy-foo-bar")["added"] == ["bar"]
(tmp_dir / "bar").unlink()
assert dvc.checkout(":copy-foo-bar")["added"] == ["bar"]
(tmp_dir / "bar").unlink()
(tmp_dir / "data").mkdir()
with (tmp_dir / "data").chdir():
assert dvc.checkout(relpath(tmp_dir / "dvc.yaml") + ":copy-foo-bar")[
"added"
] == [relpath(tmp_dir / "bar")]
(tmp_dir / "bar").unlink()
assert dvc.checkout("bar")["added"] == ["bar"]
def test_checkouts_on_same_stage_name_and_output_name(tmp_dir, dvc, run_copy):
tmp_dir.gen("foo", "foo")
run_copy("foo", "bar", name="copy-foo-bar")
run_copy("foo", "copy-foo-bar", name="make_collision")
(tmp_dir / "bar").unlink()
(tmp_dir / "copy-foo-bar").unlink()
assert dvc.checkout("copy-foo-bar")["added"] == ["bar"]
assert dvc.checkout("./copy-foo-bar")["added"] == ["copy-foo-bar"]
def test_checkouts_for_pipeline_tracked_outs(tmp_dir, dvc, scm, run_copy):
tmp_dir.gen("foo", "foo") tmp_dir.gen("foo", "foo")
stage1 = run_copy("foo", "bar", name="copy-foo-bar") stage1 = run_copy("foo", "bar", name="copy-foo-bar")
tmp_dir.gen("lorem", "lorem") tmp_dir.gen("lorem", "lorem")
+14
View File
@@ -1,5 +1,6 @@
import pytest import pytest
from dvc.dvcfile import PIPELINE_FILE
from dvc.stage.exceptions import StageCommitError from dvc.stage.exceptions import StageCommitError
from dvc.utils.stage import dump_stage_file, load_stage_file from dvc.utils.stage import dump_stage_file, load_stage_file
@@ -81,3 +82,16 @@ def test_commit_no_exec(tmp_dir, dvc):
assert dvc.status(stage.path) assert dvc.status(stage.path)
dvc.commit(stage.path, force=True) dvc.commit(stage.path, force=True)
assert dvc.status(stage.path) == {} assert dvc.status(stage.path) == {}
def test_commit_pipeline_stage(tmp_dir, dvc, run_copy):
tmp_dir.gen("foo", "foo")
stage = run_copy("foo", "bar", no_commit=True, name="copy-foo-bar")
assert dvc.status(stage.addressing)
assert dvc.commit(stage.addressing, force=True) == [stage]
assert not dvc.status(stage.addressing)
# just to confirm different variants work
assert dvc.commit(f":{stage.addressing}") == [stage]
assert dvc.commit(f"{PIPELINE_FILE}:{stage.addressing}") == [stage]
assert dvc.commit(PIPELINE_FILE) == [stage]
+21
View File
@@ -932,3 +932,24 @@ def test_push_pull_all(tmp_dir, scm, dvc, setup_remote, key, expected):
clean(["foo", "bar", "baz"], dvc) clean(["foo", "bar", "baz"], dvc)
assert dvc.pull(**{key: True})["fetched"] == expected assert dvc.pull(**{key: True})["fetched"] == expected
def test_push_pull_fetch_pipeline_stages(tmp_dir, dvc, run_copy, setup_remote):
remote_path = setup_remote(dvc)
tmp_dir.dvc_gen("foo", "foo")
run_copy("foo", "bar", no_commit=True, name="copy-foo-bar")
dvc.push("copy-foo-bar")
assert len(recurse_list_dir(remote_path)) == 1
# pushing everything so as we can check pull/fetch only downloads
# from specified targets
dvc.push()
clean(["foo", "bar"], dvc)
dvc.pull("copy-foo-bar")
assert (tmp_dir / "bar").exists()
assert len(recurse_list_dir(dvc.cache.local.cache_dir)) == 1
clean(["bar"], dvc)
dvc.fetch("copy-foo-bar")
assert len(recurse_list_dir(dvc.cache.local.cache_dir)) == 1
+1 -38
View File
@@ -1,10 +1,7 @@
import pytest import pytest
from dvc.dvcfile import PIPELINE_FILE, Dvcfile from dvc.dvcfile import PIPELINE_FILE, Dvcfile
from dvc.stage.exceptions import ( from dvc.stage.exceptions import StageFileDoesNotExistError
StageFileDoesNotExistError,
StageNameUnspecified,
)
from dvc.stage.loader import StageNotFound from dvc.stage.loader import StageNotFound
@@ -163,37 +160,3 @@ def test_stage_collection(tmp_dir, dvc):
single_stage=True, single_stage=True,
) )
assert {s for s in dvc.stages} == {stage1, stage3, stage2} assert {s for s in dvc.stages} == {stage1, stage3, stage2}
def test_stage_filter(tmp_dir, dvc, run_copy):
tmp_dir.gen("foo", "foo")
stage1 = run_copy("foo", "bar", name="copy-foo-bar")
stage2 = run_copy("bar", "bax", name="copy-bar-bax")
stage3 = run_copy("bax", "baz", name="copy-bax-baz")
stages = Dvcfile(dvc, PIPELINE_FILE).stages
assert set(stages.filter(None).values()) == {
stage1,
stage2,
stage3,
}
assert set(stages.filter("copy-bar-bax").values()) == {stage2}
assert stages.filter("copy-bar-bax").get("copy-bar-bax") == stage2
with pytest.raises(StageNameUnspecified):
stages.get(None)
with pytest.raises(StageNotFound):
assert stages["unknown"]
with pytest.raises(StageNotFound):
assert stages.filter("unknown")
def test_stage_filter_in_singlestage_file(tmp_dir, dvc, run_copy):
tmp_dir.gen("foo", "foo")
stage = run_copy("foo", "bar", single_stage=True)
dvcfile = Dvcfile(dvc, stage.path)
assert set(dvcfile.stages.filter(None).values()) == {stage}
assert dvcfile.stages.filter(None).get(None) == stage
assert dvcfile.stages.filter("copy-bar-bax").get("copy-bar-bax") == stage
assert dvcfile.stages.filter("copy-bar-bax").get(None) == stage
assert set(dvcfile.stages.filter("unknown").values()) == {stage}
+8 -8
View File
@@ -296,20 +296,20 @@ def test_pipeline_list_show_multistage(tmp_dir, dvc, run_copy, caplog):
with caplog.at_level(logging.INFO, "dvc"): with caplog.at_level(logging.INFO, "dvc"):
command._show("foobar.dvc", False, False, False) command._show("foobar.dvc", False, False, False)
output = caplog.text.splitlines() output = caplog.text.splitlines()
assert "dvc.yaml:copy-foo-bar" in output[0] assert "copy-foo-bar" in output[0]
assert "foobar.dvc" in output[1] assert "foobar.dvc" in output[1]
caplog.clear() caplog.clear()
with caplog.at_level(logging.INFO, "dvc"): with caplog.at_level(logging.INFO, "dvc"):
command._show("dvc.yaml:copy-foo-bar", False, False, False) command._show("copy-foo-bar", False, False, False)
assert "dvc.yaml:copy-foo-bar" in caplog.text assert "copy-foo-bar" in caplog.text
assert "foobar.dvc" not in caplog.text assert "foobar.dvc" not in caplog.text
command = CmdPipelineList([]) command = CmdPipelineList([])
caplog.clear() caplog.clear()
with caplog.at_level(logging.INFO, "dvc"): with caplog.at_level(logging.INFO, "dvc"):
command.run() command.run()
assert "dvc.yaml:copy-foo-bar" in caplog.text assert "copy-foo-bar" in caplog.text
assert "foobar.dvc" in caplog.text assert "foobar.dvc" in caplog.text
assert "1 pipelines in total" assert "1 pipelines in total"
@@ -320,13 +320,13 @@ def test_pipeline_ascii_multistage(tmp_dir, dvc, run_copy):
run_copy("bar", "foobar", single_stage=True) run_copy("bar", "foobar", single_stage=True)
command = CmdPipelineShow([]) command = CmdPipelineShow([])
nodes, edges, _ = command._build_graph("foobar.dvc") nodes, edges, _ = command._build_graph("foobar.dvc")
assert set(nodes) == {"dvc.yaml:copy-foo-bar", "foobar.dvc"} assert set(nodes) == {"copy-foo-bar", "foobar.dvc"}
assert set(edges) == { assert set(edges) == {
("foobar.dvc", "dvc.yaml:copy-foo-bar"), ("foobar.dvc", "copy-foo-bar"),
} }
nodes, *_ = command._build_graph("dvc.yaml:copy-foo-bar") nodes, *_ = command._build_graph("copy-foo-bar")
assert set(nodes) == {"dvc.yaml:copy-foo-bar"} assert set(nodes) == {"copy-foo-bar"}
def test_pipeline_multi_outputs_stages(dvc): def test_pipeline_multi_outputs_stages(dvc):
+251 -2
View File
@@ -1,13 +1,24 @@
import os import os
from operator import itemgetter
import pytest
from dvc.cache import Cache from dvc.cache import Cache
from dvc.dvcfile import PIPELINE_FILE, PIPELINE_LOCK
from dvc.exceptions import NoOutputOrStageError
from dvc.path_info import PathInfo
from dvc.repo import Repo from dvc.repo import Repo
from dvc.stage.exceptions import (
StageFileDoesNotExistError,
StageNameUnspecified,
StageNotFound,
)
from dvc.system import System from dvc.system import System
from dvc.utils import relpath
from dvc.utils.fs import remove
def test_destroy(tmp_dir, dvc, run_copy): def test_destroy(tmp_dir, dvc, run_copy):
from dvc.dvcfile import PIPELINE_FILE, PIPELINE_LOCK
dvc.config["cache"]["type"] = ["symlink"] dvc.config["cache"]["type"] = ["symlink"]
dvc.cache = Cache(dvc) dvc.cache = Cache(dvc)
@@ -88,6 +99,12 @@ def test_collect(tmp_dir, scm, dvc, run_copy):
assert collect_outs("dvc.yaml:copy-foo-foobar", recursive=True) == { assert collect_outs("dvc.yaml:copy-foo-foobar", recursive=True) == {
"foobar" "foobar"
} }
assert collect_outs("copy-foo-foobar") == {"foobar"}
assert collect_outs("copy-foo-foobar", with_deps=True) == {
"foobar",
"foo",
}
assert collect_outs("copy-foo-foobar", recursive=True) == {"foobar"}
run_copy("foobar", "baz", name="copy-foobar-baz") run_copy("foobar", "baz", name="copy-foobar-baz")
assert collect_outs("dvc.yaml") == {"foobar", "baz"} assert collect_outs("dvc.yaml") == {"foobar", "baz"}
@@ -98,6 +115,26 @@ def test_collect(tmp_dir, scm, dvc, run_copy):
} }
def test_collect_dir_recursive(tmp_dir, dvc, run_head):
tmp_dir.gen({"dir": {"foo": "foo"}})
(stage1,) = dvc.add("dir", recursive=True)
with (tmp_dir / "dir").chdir():
stage2 = run_head("foo", name="copy-foo-bar")
stage3 = run_head("foo-1", single_stage=True)
assert set(dvc.collect("dir", recursive=True)) == {stage1, stage2, stage3}
def test_collect_with_not_existing_output_or_stage_name(
tmp_dir, dvc, run_copy
):
with pytest.raises(StageFileDoesNotExistError):
dvc.collect("some_file")
tmp_dir.dvc_gen("foo", "foo")
run_copy("foo", "bar", name="copy-foo-bar")
with pytest.raises(StageNotFound):
dvc.collect("some_file")
def test_stages(tmp_dir, dvc): def test_stages(tmp_dir, dvc):
def stages(): def stages():
return {stage.relpath for stage in Repo(os.fspath(tmp_dir)).stages} return {stage.relpath for stage in Repo(os.fspath(tmp_dir)).stages}
@@ -113,3 +150,215 @@ def test_stages(tmp_dir, dvc):
tmp_dir.gen(".dvcignore", "dir") tmp_dir.gen(".dvcignore", "dir")
assert stages() == {"file.dvc"} assert stages() == {"file.dvc"}
@pytest.fixture
def stages(tmp_dir, run_copy):
stage1, stage2 = tmp_dir.dvc_gen({"foo": "foo", "lorem": "lorem"})
return {
"foo-generate": stage1,
"lorem-generate": stage2,
"copy-foo-bar": run_copy("foo", "bar", single_stage=True),
"copy-bar-foobar": run_copy("bar", "foobar", name="copy-bar-foobar"),
"copy-lorem-ipsum": run_copy("lorem", "ipsum", name="lorem-ipsum"),
}
def test_collect_granular_with_no_target(tmp_dir, dvc, stages):
assert set(map(itemgetter(0), dvc.collect_granular())) == set(
stages.values()
)
assert list(map(itemgetter(1), dvc.collect_granular())) == [None] * len(
stages
)
def test_collect_granular_with_target(tmp_dir, dvc, stages):
assert dvc.collect_granular("bar.dvc") == [(stages["copy-foo-bar"], None)]
assert dvc.collect_granular(PIPELINE_FILE) == [
(stages["copy-bar-foobar"], None),
(stages["copy-lorem-ipsum"], None),
]
assert dvc.collect_granular(":") == [
(stages["copy-bar-foobar"], None),
(stages["copy-lorem-ipsum"], None),
]
assert dvc.collect_granular("copy-bar-foobar") == [
(stages["copy-bar-foobar"], None)
]
assert dvc.collect_granular(":copy-bar-foobar") == [
(stages["copy-bar-foobar"], None)
]
assert dvc.collect_granular("dvc.yaml:copy-bar-foobar") == [
(stages["copy-bar-foobar"], None)
]
with (tmp_dir / dvc.DVC_DIR).chdir():
assert dvc.collect_granular(
relpath(tmp_dir / PIPELINE_FILE) + ":copy-bar-foobar"
) == [(stages["copy-bar-foobar"], None)]
assert dvc.collect_granular("foobar") == [
(stages["copy-bar-foobar"], PathInfo(tmp_dir / "foobar"))
]
@pytest.mark.parametrize(
"target",
[
"not_existing.dvc",
"not_existing.dvc:stage_name",
"not_existing/dvc.yaml",
"not_existing/dvc.yaml:stage_name",
],
)
def test_collect_with_not_existing_dvcfile(tmp_dir, dvc, target):
with pytest.raises(StageFileDoesNotExistError):
dvc.collect_granular(target)
with pytest.raises(StageFileDoesNotExistError):
dvc.collect(target)
def test_collect_granular_with_not_existing_output_or_stage_name(tmp_dir, dvc):
with pytest.raises(NoOutputOrStageError):
dvc.collect_granular("some_file")
with pytest.raises(NoOutputOrStageError):
dvc.collect_granular("some_file", recursive=True)
def test_collect_granular_with_deps(tmp_dir, dvc, stages):
assert set(
map(itemgetter(0), dvc.collect_granular("bar.dvc", with_deps=True))
) == {stages["copy-foo-bar"], stages["foo-generate"]}
assert set(
map(
itemgetter(0),
dvc.collect_granular("copy-bar-foobar", with_deps=True),
)
) == {
stages["copy-bar-foobar"],
stages["copy-foo-bar"],
stages["foo-generate"],
}
assert set(
map(
itemgetter(0), dvc.collect_granular(PIPELINE_FILE, with_deps=True),
)
) == set(stages.values())
def test_collect_granular_same_output_name_stage_name(tmp_dir, dvc, run_copy):
(stage1,) = tmp_dir.dvc_gen("foo", "foo")
(stage2,) = tmp_dir.dvc_gen("copy-foo-bar", "copy-foo-bar")
stage3 = run_copy("foo", "bar", name="copy-foo-bar")
assert dvc.collect_granular("copy-foo-bar") == [(stage3, None)]
coll = dvc.collect_granular("copy-foo-bar", with_deps=True)
assert set(map(itemgetter(0), coll)) == {stage3, stage1}
assert list(map(itemgetter(1), coll)) == [None] * 2
assert dvc.collect_granular("./copy-foo-bar") == [
(stage2, PathInfo(tmp_dir / "copy-foo-bar"))
]
assert dvc.collect_granular("./copy-foo-bar", with_deps=True) == [
(stage2, PathInfo(tmp_dir / "copy-foo-bar"))
]
def test_collect_granular_priority_on_collision(tmp_dir, dvc, run_copy):
tmp_dir.gen({"dir": {"foo": "foo"}, "foo": "foo"})
(stage1,) = dvc.add("dir", recursive=True)
stage2 = run_copy("foo", "bar", name="dir")
assert dvc.collect_granular("dir") == [(stage2, None)]
assert dvc.collect_granular("dir", recursive=True) == [(stage1, None)]
remove(tmp_dir / "dir")
assert dvc.collect_granular("dir") == [(stage2, None)]
assert dvc.collect_granular("dir", recursive=True) == [(stage2, None)]
def test_collect_granular_collision_output_dir_stage_name(
tmp_dir, dvc, run_copy
):
stage1, *_ = tmp_dir.dvc_gen({"dir": {"foo": "foo"}, "foo": "foo"})
stage3 = run_copy("foo", "bar", name="dir")
assert dvc.collect_granular("dir") == [(stage3, None)]
assert not dvc.collect_granular("dir", recursive=True)
assert dvc.collect_granular("./dir") == [
(stage1, PathInfo(tmp_dir / "dir"))
]
def test_collect_granular_not_existing_stage_name(tmp_dir, dvc, run_copy):
tmp_dir.dvc_gen("foo", "foo")
(stage,) = tmp_dir.dvc_gen("copy-foo-bar", "copy-foo-bar")
run_copy("foo", "bar", name="copy-foo-bar")
assert dvc.collect_granular("copy-foo-bar.dvc:stage_name_not_needed") == [
(stage, None)
]
with pytest.raises(StageNotFound):
dvc.collect_granular("dvc.yaml:does-not-exist")
def test_get_stages(tmp_dir, dvc, run_copy):
with pytest.raises(StageFileDoesNotExistError):
dvc.get_stages()
tmp_dir.gen("foo", "foo")
stage1 = run_copy("foo", "bar", name="copy-foo-bar")
stage2 = run_copy("bar", "foobar", name="copy-bar-foobar")
assert set(dvc.get_stages()) == {stage1, stage2}
assert set(dvc.get_stages(path=PIPELINE_FILE)) == {stage1, stage2}
assert set(dvc.get_stages(name="copy-bar-foobar")) == {stage2}
assert set(dvc.get_stages(path=PIPELINE_FILE, name="copy-bar-foobar")) == {
stage2
}
with pytest.raises(StageFileDoesNotExistError):
dvc.get_stages(path=relpath(tmp_dir / ".." / PIPELINE_FILE))
with pytest.raises(StageNotFound):
dvc.get_stages(path=PIPELINE_FILE, name="copy")
def test_get_stages_old_dvcfile(tmp_dir, dvc):
(stage1,) = tmp_dir.dvc_gen("foo", "foo")
assert set(dvc.get_stages("foo.dvc")) == {stage1}
assert set(dvc.get_stages("foo.dvc", name="foo-generate")) == {stage1}
with pytest.raises(StageFileDoesNotExistError):
dvc.get_stages(path=relpath(tmp_dir / ".." / "foo.dvc"))
def test_get_stage(tmp_dir, dvc, run_copy):
tmp_dir.gen("foo", "foo")
stage1 = run_copy("foo", "bar", name="copy-foo-bar")
with pytest.raises(StageNameUnspecified):
dvc.get_stage()
with pytest.raises(StageNameUnspecified):
dvc.get_stage(path=PIPELINE_FILE)
assert dvc.get_stage(path=PIPELINE_FILE, name="copy-foo-bar") == stage1
assert dvc.get_stage(name="copy-foo-bar") == stage1
with pytest.raises(StageFileDoesNotExistError):
dvc.get_stage(path="something.yaml", name="name")
with pytest.raises(StageNotFound):
dvc.get_stage(name="random_name")
def test_get_stage_single_stage_dvcfile(tmp_dir, dvc):
(stage1,) = tmp_dir.dvc_gen("foo", "foo")
assert dvc.get_stage("foo.dvc") == stage1
assert dvc.get_stage("foo.dvc", name="jpt") == stage1
with pytest.raises(StageFileDoesNotExistError):
dvc.get_stage(path="bar.dvc", name="name")
+1 -1
View File
@@ -56,7 +56,7 @@ class SingleStageRun:
@staticmethod @staticmethod
def _get_stage_target(stage): def _get_stage_target(stage):
return stage.path return stage.addressing
class TestRepro(SingleStageRun, TestDvc): class TestRepro(SingleStageRun, TestDvc):
+3 -5
View File
@@ -32,7 +32,7 @@ class MultiStageRun:
@staticmethod @staticmethod
def _get_stage_target(stage): def _get_stage_target(stage):
return stage.path + ":" + stage.name return stage.addressing
class TestReproFailMultiStage(MultiStageRun, test_repro.TestReproFail): class TestReproFailMultiStage(MultiStageRun, test_repro.TestReproFail):
@@ -312,15 +312,13 @@ def test_repro_when_cmd_changes(tmp_dir, dvc, run_copy):
tmp_dir.gen("foo", "foo") tmp_dir.gen("foo", "foo")
stage = run_copy("foo", "bar", name="copy-file") stage = run_copy("foo", "bar", name="copy-file")
target = ":copy-file" target = "copy-file"
assert not dvc.reproduce(target) assert not dvc.reproduce(target)
stage.cmd = " ".join(stage.cmd.split()) # change cmd spacing by two stage.cmd = " ".join(stage.cmd.split()) # change cmd spacing by two
PipelineFile(dvc, PIPELINE_FILE)._dump_pipeline_file(stage) PipelineFile(dvc, PIPELINE_FILE)._dump_pipeline_file(stage)
assert dvc.status([target]) == { assert dvc.status([target]) == {target: ["changed command"]}
PIPELINE_FILE + target: ["changed command"]
}
assert dvc.reproduce(target)[0] == stage assert dvc.reproduce(target)[0] == stage
+28 -8
View File
@@ -8,7 +8,7 @@ from dvc.main import main
from dvc.output.local import LocalOutput from dvc.output.local import LocalOutput
from dvc.remote.local import LocalRemote from dvc.remote.local import LocalRemote
from dvc.repo import Repo from dvc.repo import Repo
from dvc.stage import Stage from dvc.stage import PipelineStage, Stage
from dvc.stage.exceptions import StageFileFormatError from dvc.stage.exceptions import StageFileFormatError
from dvc.utils.stage import dump_stage_file, load_stage_file from dvc.utils.stage import dump_stage_file, load_stage_file
from tests.basic_env import TestDvc from tests.basic_env import TestDvc
@@ -196,19 +196,39 @@ def test_parent_repo_collect_stages(tmp_dir, scm, dvc):
assert subrepo_stages != [] assert subrepo_stages != []
def test_stage_addressing(tmp_dir, dvc, run_copy): def test_stage_strings_representation(tmp_dir, dvc, run_copy):
tmp_dir.dvc_gen("foo", "foo") tmp_dir.dvc_gen("foo", "foo")
stage1 = run_copy("foo", "bar", single_stage=True) stage1 = run_copy("foo", "bar", single_stage=True)
assert stage1.addressing == "bar.dvc" assert stage1.addressing == "bar.dvc"
assert repr(stage1) == "Stage: 'bar.dvc'"
assert str(stage1) == "stage: 'bar.dvc'"
stage2 = run_copy("bar", "baz", name="copy-bar-baz") stage2 = run_copy("bar", "baz", name="copy-bar-baz")
assert stage2.addressing == "dvc.yaml:copy-bar-baz" assert stage2.addressing == "copy-bar-baz"
assert repr(stage2) == "Stage: 'copy-bar-baz'"
assert str(stage2) == "stage: 'copy-bar-baz'"
folder = tmp_dir / "dir" folder = tmp_dir / "dir"
folder.mkdir() folder.mkdir()
with folder.chdir(): with folder.chdir():
assert stage1.addressing == os.path.relpath(stage1.path) rel_path = os.path.relpath(stage1.path)
assert ( assert stage1.addressing == rel_path
stage2.addressing assert repr(stage1) == f"Stage: '{rel_path}'"
== os.path.relpath(stage2.path) + ":" + stage2.name assert str(stage1) == f"stage: '{rel_path}'"
)
rel_path = os.path.relpath(stage2.path)
assert stage2.addressing == f"{rel_path}:{stage2.name}"
assert repr(stage2) == f"Stage: '{rel_path}:{stage2.name}'"
assert str(stage2) == f"stage: '{rel_path}:{stage2.name}'"
def test_stage_on_no_path_string_repr(tmp_dir, dvc):
s = Stage(dvc)
assert s.addressing == "No path"
assert repr(s) == "Stage: 'No path'"
assert str(s) == "stage: 'No path'"
p = PipelineStage(dvc, name="stage_name")
assert p.addressing == "No path:stage_name"
assert repr(p) == "Stage: 'No path:stage_name'"
assert str(p) == "stage: 'No path:stage_name'"
+3 -3
View File
@@ -73,12 +73,12 @@ def test_status_on_pipeline_stages(tmp_dir, dvc, run_copy):
stage.cmd = " ".join(stage.cmd.split()) stage.cmd = " ".join(stage.cmd.split())
stage.dvcfile._dump_pipeline_file(stage) stage.dvcfile._dump_pipeline_file(stage)
assert dvc.status() == {"dvc.yaml:copy-foo-bar": ["changed command"]} assert dvc.status("copy-foo-bar") == {"copy-foo-bar": ["changed command"]}
# delete outputs # delete outputs
(tmp_dir / "bar").unlink() (tmp_dir / "bar").unlink()
assert dvc.status() == { assert dvc.status() == {
"dvc.yaml:copy-foo-bar": [ "copy-foo-bar": [
{"changed outs": {"bar": "deleted"}}, {"changed outs": {"bar": "deleted"}},
"changed command", "changed command",
] ]
@@ -86,7 +86,7 @@ def test_status_on_pipeline_stages(tmp_dir, dvc, run_copy):
(tmp_dir / "foo").unlink() (tmp_dir / "foo").unlink()
assert dvc.status() == { assert dvc.status() == {
"foo.dvc": [{"changed outs": {"foo": "deleted"}}], "foo.dvc": [{"changed outs": {"foo": "deleted"}}],
"dvc.yaml:copy-foo-bar": [ "copy-foo-bar": [
{"changed deps": {"foo": "deleted"}}, {"changed deps": {"foo": "deleted"}},
{"changed outs": {"bar": "deleted"}}, {"changed outs": {"bar": "deleted"}},
"changed command", "changed command",
+15
View File
@@ -87,6 +87,21 @@ def test_collect_optimization(tmp_dir, dvc, mocker):
dvc.collect_granular(stage.path) dvc.collect_granular(stage.path)
def test_collect_optimization_on_stage_name(tmp_dir, dvc, mocker, run_copy):
tmp_dir.dvc_gen("foo", "foo")
stage = run_copy("foo", "bar", name="copy-foo-bar")
# Forget cached stages and graph and error out on collection
dvc._reset()
mocker.patch(
"dvc.repo.Repo.stages",
property(raiser(Exception("Should not collect"))),
)
# Should read stage directly instead of collecting the whole graph
assert dvc.collect("copy-foo-bar") == [stage]
assert dvc.collect_granular("copy-foo-bar") == [(stage, None)]
def test_skip_graph_checks(tmp_dir, dvc, mocker, run_copy): def test_skip_graph_checks(tmp_dir, dvc, mocker, run_copy):
# See https://github.com/iterative/dvc/issues/2671 for more info # See https://github.com/iterative/dvc/issues/2671 for more info
mock_collect_graph = mocker.patch("dvc.repo.Repo._collect_graph") mock_collect_graph = mocker.patch("dvc.repo.Repo._collect_graph")
+3
View File
@@ -146,6 +146,9 @@ def test_resolve_output(inp, out, is_dir, expected, mocker):
["../models/stage.dvc", ("../models/stage.dvc", None), "def"], ["../models/stage.dvc", ("../models/stage.dvc", None), "def"],
[":name", ("default", "name"), "default"], [":name", ("default", "name"), "default"],
[":name", ("default", "name"), "default"], [":name", ("default", "name"), "default"],
["something.dvc:name", ("something.dvc", "name"), None],
["../something.dvc:name", ("../something.dvc", "name"), None],
["file", (None, "file"), None],
], ],
) )
def test_parse_target(inp, out, default): def test_parse_target(inp, out, default):