implement removing stage (#3881)

This should allow us to be able to remove a stage from a dvcfile,
(if it's a single stage file, it removes it), and make us able to
delete the overwrite the stage on run as well.

This commit:
* changes `run` `--overwrite-dvcfile` flag to `--overwrite`
* removes prompt in `run` for overwrite and fails hard
* changes `remove`: now it by default removes stage entry and unprotect outs
* `remove` has `--outs` to remove outputs instead of unprotecting them
This commit is contained in:
Saugat Pachhai
2020-05-31 17:41:19 +03:00
committed by GitHub
parent 9ee62ef8f0
commit edfa1741db
14 changed files with 292 additions and 190 deletions
+4 -41
View File
@@ -1,7 +1,6 @@
import argparse
import logging
import dvc.prompt as prompt
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
@@ -9,30 +8,10 @@ logger = logging.getLogger(__name__)
class CmdRemove(CmdBase):
def _is_dvc_only(self, target):
if not self.args.purge:
return True
if self.args.force:
return False
msg = "Are you sure you want to remove '{}' with its outputs?".format(
target
)
if prompt.confirm(msg):
return False
raise DvcException(
"Cannot purge without a confirmation from the user."
" Use `-f` to force."
)
def run(self):
for target in self.args.targets:
try:
dvc_only = self._is_dvc_only(target)
self.repo.remove(target, dvc_only=dvc_only)
self.repo.remove(target, outs=self.args.outs)
except DvcException:
logger.exception(f"failed to remove '{target}'")
return 1
@@ -40,7 +19,7 @@ class CmdRemove(CmdBase):
def add_parser(subparsers, parent_parser):
REMOVE_HELP = "Remove DVC-tracked files or directories."
REMOVE_HELP = "Remove stage entry and unprotect outputs"
remove_parser = subparsers.add_parser(
"remove",
parents=[parent_parser],
@@ -48,27 +27,11 @@ def add_parser(subparsers, parent_parser):
help=REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remove_parser_group = remove_parser.add_mutually_exclusive_group()
remove_parser_group.add_argument(
"-o",
remove_parser.add_argument(
"--outs",
action="store_true",
default=True,
help="Only remove DVC-file outputs. (Default)",
)
remove_parser_group.add_argument(
"-p",
"--purge",
action="store_true",
default=False,
help="Remove DVC-file and all its outputs.",
)
remove_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Force purge.",
help="Remove outputs as well.",
)
remove_parser.add_argument(
"targets", nargs="+", help="DVC-files to remove."
+3 -3
View File
@@ -45,7 +45,7 @@ class CmdRun(CmdBase):
fname=self.args.file,
wdir=self.args.wdir,
no_exec=self.args.no_exec,
overwrite=self.args.overwrite_dvcfile,
overwrite=self.args.overwrite,
run_cache=not self.args.no_run_cache,
no_commit=self.args.no_commit,
outs_persist=self.args.outs_persist,
@@ -177,10 +177,10 @@ def add_parser(subparsers, parent_parser):
help="Only create stage file without actually running it.",
)
run_parser.add_argument(
"--overwrite-dvcfile",
"--overwrite",
action="store_true",
default=False,
help="Overwrite existing DVC-file without asking for confirmation.",
help="Overwrite existing stage",
)
run_parser.add_argument(
"--no-run-cache",
+37 -2
View File
@@ -150,7 +150,7 @@ class SingleStageFile(FileMixin):
"Saving information to '{file}'.".format(file=relpath(self.path))
)
dump_stage_file(self.path, serialize.to_single_stage_file(stage))
self.repo.scm.track_file(relpath(self.path))
self.repo.scm.track_file(self.relpath)
def remove_with_prompt(self, force=False):
if not self.exists():
@@ -165,6 +165,9 @@ class SingleStageFile(FileMixin):
self.remove()
def remove_stage(self, stage):
self.remove()
class PipelineFile(FileMixin):
"""Abstraction for pipelines file, .yaml + .lock combined."""
@@ -212,7 +215,7 @@ class PipelineFile(FileMixin):
"Adding stage '%s' to '%s'", stage.name, self.relpath,
)
dump_stage_file(self.path, data)
self.repo.scm.track_file(relpath(self.path))
self.repo.scm.track_file(self.relpath)
@property
def stage(self):
@@ -234,6 +237,22 @@ class PipelineFile(FileMixin):
super().remove()
self._lockfile.remove()
def remove_stage(self, stage):
self._lockfile.remove_stage(stage)
if not self.exists():
return
with open(self.path, "r") as f:
d = parse_stage_for_update(f.read(), self.path)
self.validate(d, self.path)
if stage.name not in d.get("stages", {}):
return
logger.debug("Removing '%s' from '%s'", stage.name, self.path)
del d["stages"][stage.name]
dump_stage_file(self.path, d)
class Lockfile(FileMixin):
from dvc.schema import COMPILED_LOCKFILE_SCHEMA as SCHEMA
@@ -271,6 +290,22 @@ class Lockfile(FileMixin):
if modified:
self.repo.scm.track_file(self.relpath)
def remove_stage(self, stage):
if not self.exists():
return
with open(self.path) as f:
d = parse_stage_for_update(f.read(), self.path)
self.validate(d, self.path)
if stage.name not in d:
return
logger.debug("Removing '%s' from '%s'", stage.name, self.path)
del d[stage.name]
dump_stage_file(self.path, d)
class Dvcfile:
def __new__(cls, repo, path, **kwargs):
+3 -7
View File
@@ -7,15 +7,11 @@ logger = logging.getLogger(__name__)
@locked
def remove(self, target, dvc_only=False):
from ..dvcfile import Dvcfile, is_valid_filename
def remove(self, target, outs=False):
path, name = parse_target(target)
stages = self.get_stages(path, name)
for stage in stages:
stage.remove_outs(force=True)
if path and is_valid_filename(path) and not dvc_only:
Dvcfile(self, path).remove()
for stage in stages:
stage.remove(remove_outs=outs, force=outs)
return stages
+10 -4
View File
@@ -3,7 +3,11 @@ import os
from funcy import concat, first
from dvc.exceptions import InvalidArgumentError
from dvc.stage.exceptions import DuplicateStageName, InvalidStageName
from dvc.stage.exceptions import (
DuplicateStageName,
InvalidStageName,
StageFileAlreadyExistsError,
)
from ..exceptions import OutputDuplicationError
from . import locked
@@ -74,10 +78,12 @@ def run(self, fname=None, no_exec=False, single_stage=False, **kwargs):
dvcfile = Dvcfile(self, stage.path)
if dvcfile.exists():
if stage_name and stage_name in dvcfile.stages:
if kwargs.get("overwrite", True):
dvcfile.remove_stage(stage)
elif stage_cls != PipelineStage:
raise StageFileAlreadyExistsError(dvcfile.relpath)
elif stage_name and stage_name in dvcfile.stages:
raise DuplicateStageName(stage_name, dvcfile)
if stage_cls != PipelineStage:
dvcfile.remove_with_prompt(force=kwargs.get("overwrite", True))
try:
self.check_modified_graph([stage])
+3 -2
View File
@@ -282,12 +282,13 @@ class Stage(params.StageParams):
out.unprotect()
@rwlocked(write=["outs"])
def remove(self, force=False, remove_outs=True):
def remove(self, force=False, remove_outs=True, purge=True):
if remove_outs:
self.remove_outs(ignore_remove=True, force=force)
else:
self.unprotect_outs()
self.dvcfile.remove()
if purge:
self.dvcfile.remove_stage(self)
@rwlocked(read=["deps"], write=["outs"])
def reproduce(self, interactive=False, **kwargs):
+132 -2
View File
@@ -1,8 +1,14 @@
import textwrap
import pytest
from dvc.dvcfile import PIPELINE_FILE, Dvcfile
from dvc.stage.exceptions import StageFileDoesNotExistError
from dvc.dvcfile import PIPELINE_FILE, PIPELINE_LOCK, Dvcfile
from dvc.stage.exceptions import (
StageFileDoesNotExistError,
StageFileFormatError,
)
from dvc.stage.loader import StageNotFound
from dvc.utils.stage import dump_stage_file
def test_run_load_one_for_multistage(tmp_dir, dvc):
@@ -160,3 +166,127 @@ def test_stage_collection(tmp_dir, dvc):
single_stage=True,
)
assert {s for s in dvc.stages} == {stage1, stage3, stage2}
def test_remove_stage(tmp_dir, dvc, run_copy):
tmp_dir.gen("foo", "foo")
stage = run_copy("foo", "bar", name="copy-foo-bar")
stage2 = run_copy("bar", "foobar", name="copy-bar-foobar")
dvc_file = Dvcfile(dvc, PIPELINE_FILE)
assert dvc_file.exists()
assert {"copy-bar-foobar", "copy-foo-bar"} == set(
dvc_file._load()[0]["stages"].keys()
)
dvc_file.remove_stage(stage)
assert ["copy-bar-foobar"] == list(dvc_file._load()[0]["stages"].keys())
# sanity check
stage2.reload()
# re-check to see if it fails if there's no stage entry
dvc_file.remove_stage(stage)
dvc_file.remove(force=True)
# should not fail when there's no file at all.
dvc_file.remove_stage(stage)
def test_remove_stage_lockfile(tmp_dir, dvc, run_copy):
tmp_dir.gen("foo", "foo")
stage = run_copy("foo", "bar", name="copy-foo-bar")
stage2 = run_copy("bar", "foobar", name="copy-bar-foobar")
dvc_file = Dvcfile(dvc, PIPELINE_FILE)
lock_file = dvc_file._lockfile
assert dvc_file.exists()
assert lock_file.exists()
assert {"copy-bar-foobar", "copy-foo-bar"} == set(lock_file.load().keys())
lock_file.remove_stage(stage)
assert ["copy-bar-foobar"] == list(lock_file.load().keys())
# sanity check
stage2.reload()
# re-check to see if it fails if there's no stage entry
lock_file.remove_stage(stage)
lock_file.remove()
# should not fail when there's no file at all.
lock_file.remove_stage(stage)
def test_remove_stage_dvcfiles(tmp_dir, dvc, run_copy):
tmp_dir.gen("foo", "foo")
stage = run_copy("foo", "bar", single_stage=True)
dvc_file = Dvcfile(dvc, stage.path)
assert dvc_file.exists()
dvc_file.remove_stage(stage)
assert not dvc_file.exists()
# re-check to see if it fails if there's no stage entry
dvc_file.remove_stage(stage)
dvc_file.remove(force=True)
# should not fail when there's no file at all.
dvc_file.remove_stage(stage)
def test_remove_stage_on_lockfile_format_error(tmp_dir, dvc, run_copy):
tmp_dir.gen("foo", "foo")
stage = run_copy("foo", "bar", name="copy-foo-bar")
dvc_file = Dvcfile(dvc, stage.path)
lock_file = dvc_file._lockfile
data = dvc_file._load()[0]
lock_data = lock_file.load()
lock_data["gibberish"] = True
data["gibberish"] = True
dump_stage_file(lock_file.relpath, lock_data)
with pytest.raises(StageFileFormatError):
dvc_file.remove_stage(stage)
lock_file.remove()
dvc_file.dump(stage)
dump_stage_file(dvc_file.relpath, data)
with pytest.raises(StageFileFormatError):
dvc_file.remove_stage(stage)
def test_remove_stage_preserves_comment(tmp_dir, dvc, run_copy):
tmp_dir.gen(
"dvc.yaml",
textwrap.dedent(
"""\
stages:
generate-foo:
cmd: "echo foo > foo"
# This copies 'foo' text to 'foo' file.
outs:
- foo
copy-foo-bar:
cmd: "python copy.py foo bar"
deps:
- foo
outs:
- bar"""
),
)
dvc.reproduce(PIPELINE_FILE)
dvc_file = Dvcfile(dvc, PIPELINE_FILE)
assert dvc_file.exists()
assert (tmp_dir / PIPELINE_LOCK).exists()
assert (tmp_dir / "foo").exists()
assert (tmp_dir / "bar").exists()
dvc_file.remove_stage(dvc_file.stages["copy-foo-bar"])
assert (
"# This copies 'foo' text to 'foo' file."
in (tmp_dir / PIPELINE_FILE).read_text()
)
+3 -3
View File
@@ -68,7 +68,7 @@ class TestGCBranchesTags(TestDvcGit):
self.dvc.scm.tag("v1.0")
self.dvc.scm.checkout("test", create_new=True)
self.dvc.remove(stages[0].relpath, dvc_only=True)
self.dvc.remove(stages[0].relpath)
with open(fname, "w+") as fobj:
fobj.write("test")
stages = self.dvc.add(fname)
@@ -77,7 +77,7 @@ class TestGCBranchesTags(TestDvcGit):
self.dvc.scm.commit("test")
self.dvc.scm.checkout("master")
self.dvc.remove(stages[0].relpath, dvc_only=True)
self.dvc.remove(stages[0].relpath)
with open(fname, "w+") as fobj:
fobj.write("trash")
stages = self.dvc.add(fname)
@@ -85,7 +85,7 @@ class TestGCBranchesTags(TestDvcGit):
self.dvc.scm.add([".gitignore", stages[0].relpath])
self.dvc.scm.commit("trash")
self.dvc.remove(stages[0].relpath, dvc_only=True)
self.dvc.remove(stages[0].relpath)
with open(fname, "w+") as fobj:
fobj.write("master")
stages = self.dvc.add(fname)
+42 -107
View File
@@ -1,129 +1,64 @@
import os
from mock import patch
import pytest
from dvc.exceptions import DvcException
from dvc.main import main
from dvc.stage import Stage
from dvc.stage.exceptions import StageFileDoesNotExistError
from dvc.system import System
from dvc.utils.fs import remove
from tests.basic_env import TestDvc
class TestRemove(TestDvc):
def test(self):
stages = self.dvc.add(self.FOO)
self.assertEqual(len(stages), 1)
stage = stages[0]
self.assertTrue(stage is not None)
(stage_removed,) = self.dvc.remove(stage.path, dvc_only=True)
@pytest.mark.parametrize("remove_outs", [True, False])
def test_remove(tmp_dir, dvc, run_copy, remove_outs):
(stage1,) = tmp_dir.dvc_gen("foo", "foo")
stage2 = run_copy("foo", "bar", single_stage=True)
stage3 = run_copy("bar", "foobar", name="copy-bar-foobar")
self.assertIsInstance(stage_removed, Stage)
self.assertEqual(stage.path, stage_removed.path)
self.assertFalse(os.path.isfile(self.FOO))
(stage_removed,) = self.dvc.remove(stage.path)
self.assertIsInstance(stage_removed, Stage)
self.assertEqual(stage.path, stage_removed.path)
self.assertFalse(os.path.isfile(self.FOO))
self.assertFalse(os.path.exists(stage.path))
for stage in [stage1, stage2, stage3]:
dvc.remove(stage.addressing, outs=remove_outs)
out_exists = (out.exists for out in stage.outs)
assert stage not in dvc._collect_stages()
if remove_outs:
assert not any(out_exists)
else:
assert all(out_exists)
class TestRemoveNonExistentFile(TestDvc):
def test(self):
with self.assertRaises(StageFileDoesNotExistError):
self.dvc.remove("non_existent_dvc_file")
def test_remove_non_existent_file(tmp_dir, dvc):
with pytest.raises(StageFileDoesNotExistError):
dvc.remove("non_existent_dvc_file.dvc")
with pytest.raises(StageFileDoesNotExistError):
dvc.remove("non_existent_stage_name")
class TestRemoveBrokenSymlink(TestDvc):
def test(self):
ret = main(["config", "cache.type", "symlink"])
self.assertEqual(ret, 0)
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
remove(self.dvc.cache.local.cache_dir)
self.assertTrue(System.is_symlink(self.FOO))
ret = main(["remove", self.FOO + ".dvc"])
self.assertEqual(ret, 0)
self.assertFalse(os.path.lexists(self.FOO))
class TestRemoveDirectory(TestDvc):
def test(self):
stages = self.dvc.add(self.DATA_DIR)
self.assertEqual(len(stages), 1)
stage_add = stages[0]
self.assertTrue(stage_add is not None)
(stage_removed,) = self.dvc.remove(stage_add.path)
self.assertEqual(stage_add.path, stage_removed.path)
self.assertFalse(os.path.exists(self.DATA_DIR))
self.assertFalse(os.path.exists(stage_removed.path))
class TestCmdRemove(TestDvc):
def test(self):
stages = self.dvc.add(self.FOO)
self.assertEqual(len(stages), 1)
stage = stages[0]
self.assertTrue(stage is not None)
ret = main(["remove", stage.path])
self.assertEqual(ret, 0)
ret = main(["remove", "non-existing-dvc-file"])
self.assertNotEqual(ret, 0)
class TestRemovePurge(TestDvc):
def test(self):
dvcfile = self.dvc.add(self.FOO)[0].path
ret = main(["remove", "--purge", "--force", dvcfile])
self.assertEqual(ret, 0)
self.assertFalse(os.path.exists(self.FOO))
self.assertFalse(os.path.exists(dvcfile))
@patch("dvc.prompt.confirm", return_value=False)
def test_force(self, mock_prompt):
dvcfile = self.dvc.add(self.FOO)[0].path
ret = main(["remove", "--purge", dvcfile])
mock_prompt.assert_called()
self.assertEqual(ret, 1)
self.assertRaises(DvcException)
def test_remove_pipeline_outs(tmp_dir, dvc, run_copy):
from dvc.dvcfile import PIPELINE_FILE
def test_remove_broken_symlink(tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
stage = run_copy("foo", "bar", name="copy-foo-bar")
dvc.cache.local.cache_types = ["symlink"]
assert main(["remove", stage.path]) == 0
assert not (tmp_dir / "bar").exists()
assert (tmp_dir / PIPELINE_FILE).exists()
(stage,) = dvc.add("foo")
remove(dvc.cache.local.cache_dir)
assert System.is_symlink("foo")
stage = run_copy("foo", "foobar", name="copy-foo-foobar")
with pytest.raises(DvcException):
dvc.remove(stage.addressing)
assert os.path.lexists("foo")
assert (tmp_dir / stage.relpath).exists()
dvc.remove(stage.addressing, outs=True)
assert not os.path.lexists("foo")
assert not (tmp_dir / stage.relpath).exists()
def test_cmd_remove(tmp_dir, dvc):
assert main(["remove", "non-existing-dvc-file"]) == 1
(stage,) = tmp_dir.dvc_gen("foo", "foo")
assert main(["remove", stage.addressing]) == 0
assert not (tmp_dir / "foobar").exists()
assert not (tmp_dir / stage.relpath).exists()
assert (tmp_dir / "foo").exists()
stage = run_copy("foo", "baz", name="copy-foo-baz")
assert main(["remove", "--purge", "-f", stage.addressing]) == 0
assert not (tmp_dir / "baz").exists()
assert (tmp_dir / "foo").exists()
assert (tmp_dir / PIPELINE_FILE).exists()
dvc.reproduce(PIPELINE_FILE)
assert main(["remove", PIPELINE_FILE]) == 0
for file in ["bar", "foobar", "baz"]:
assert not (tmp_dir / file).exists()
assert (tmp_dir / "foo").exists()
assert (tmp_dir / PIPELINE_FILE).exists()
(stage,) = tmp_dir.dvc_gen("foo", "foo")
assert main(["remove", stage.addressing, "--outs"]) == 0
assert not (tmp_dir / stage.relpath).exists()
assert not (tmp_dir / "foo").exists()
+9 -4
View File
@@ -983,7 +983,8 @@ class TestReproExternalBase(SingleStageRun, TestDvc):
self.dvc.gc(workspace=True)
self.assertEqual(self.dvc.status(), {})
self.dvc.remove(cmd_stage.path, dvc_only=True)
with self.dvc.lock:
cmd_stage.remove_outs(force=True)
self.assertNotEqual(self.dvc.status([cmd_stage.addressing]), {})
self.dvc.checkout([cmd_stage.path], force=True)
@@ -1203,12 +1204,13 @@ class TestReproExternalHTTP(TestReproExternalBase):
self.assertTrue(os.path.exists(import_output))
self.assertTrue(filecmp.cmp(import_output, self.FOO, shallow=False))
self.dvc.remove("imported_file.dvc")
self.dvc.remove("imported_file.dvc", outs=True)
with StaticFileServer(handler_class=ContentMD5Handler) as httpd:
import_url = urljoin(self.get_remote(httpd.server_port), self.FOO)
import_output = "imported_file"
import_stage = self.dvc.imp_url(import_url, import_output)
assert import_stage.repo == self.dvc
self.assertTrue(os.path.exists(import_output))
self.assertTrue(filecmp.cmp(import_output, self.FOO, shallow=False))
@@ -1225,7 +1227,7 @@ class TestReproExternalHTTP(TestReproExternalBase):
self.assertEqual(ret1, 0)
self.assertEqual(ret2, 0)
self.dvc = DvcRepo(".")
self.dvc = import_stage.repo = DvcRepo(".")
run_dependency = urljoin(remote, self.BAR)
run_output = "remote_file"
@@ -1245,7 +1247,10 @@ class TestReproExternalHTTP(TestReproExternalBase):
self.assertTrue(os.path.exists(run_output))
# Pull
self.dvc.remove(import_stage.path, dvc_only=True)
with self.dvc.lock:
self.assertEqual(import_stage.repo.lock.is_locked, True)
self.assertEqual(self.dvc.lock.is_locked, True)
import_stage.remove_outs(force=True)
self.assertFalse(os.path.exists(import_output))
shutil.move(self.local_cache, cache_id)
+2 -1
View File
@@ -218,7 +218,8 @@ def test_run_already_exists(tmp_dir, dvc, run_copy):
tmp_dir.dvc_gen("foo", "foo")
run_copy("foo", "bar", name="copy")
with pytest.raises(DuplicateStageName):
run_copy("bar", "foobar", name="copy")
run_copy("bar", "foobar", name="copy", overwrite=False)
run_copy("bar", "foobar", name="copy", overwrite=True)
supported_params = {
+14 -13
View File
@@ -350,7 +350,7 @@ class TestRunUnprotectOutsCopy(TestDvc):
ret = main(
[
"run",
"--overwrite-dvcfile",
"--overwrite",
"--no-run-cache",
"--single-stage",
"-d",
@@ -408,7 +408,7 @@ class TestRunUnprotectOutsSymlink(TestDvc):
ret = main(
[
"run",
"--overwrite-dvcfile",
"--overwrite",
"--no-run-cache",
"--single-stage",
"-d",
@@ -467,7 +467,7 @@ class TestRunUnprotectOutsHardlink(TestDvc):
ret = main(
[
"run",
"--overwrite-dvcfile",
"--overwrite",
"--no-run-cache",
"--single-stage",
"-d",
@@ -552,7 +552,7 @@ class TestCmdRunOverwrite(TestDvc):
self.FOO,
"-d",
self.CODE,
"--overwrite-dvcfile",
"--overwrite",
"--no-run-cache",
"--single-stage",
"-o",
@@ -576,7 +576,7 @@ class TestCmdRunOverwrite(TestDvc):
ret = main(
[
"run",
"--overwrite-dvcfile",
"--overwrite",
"--single-stage",
"-f",
"out.dvc",
@@ -676,19 +676,19 @@ def test_rerun_deterministic_ignore_cache(tmp_dir, run_copy):
def test_rerun_callback(dvc):
def run_callback():
def run_callback(overwrite=False):
return dvc.run(
cmd=("echo content > out"),
cmd="echo content > out",
outs=["out"],
deps=[],
overwrite=False,
overwrite=overwrite,
single_stage=True,
)
assert run_callback() is not None
with mock.patch("dvc.prompt.confirm", return_value=True):
with pytest.raises(StageFileAlreadyExistsError):
assert run_callback() is not None
assert run_callback(overwrite=True) is not None
def test_rerun_changed_dep(tmp_dir, run_copy):
@@ -698,6 +698,7 @@ def test_rerun_changed_dep(tmp_dir, run_copy):
tmp_dir.gen("foo", "changed content")
with pytest.raises(StageFileAlreadyExistsError):
run_copy("foo", "out", overwrite=False, single_stage=True)
assert run_copy("foo", "out", overwrite=True, single_stage=True)
def test_rerun_changed_stage(tmp_dir, run_copy):
@@ -788,7 +789,7 @@ class TestRunPersist(TestDvc):
self.assertEqual(2, len(lines))
def should_remove_persistent_outs(self, file, stage_file):
ret = main(["remove", stage_file])
ret = main(["remove", stage_file, "--outs"])
self.assertEqual(0, ret)
self.assertFalse(os.path.exists(file))
@@ -869,7 +870,7 @@ class TestRerunWithSameOutputs(TestDvc):
"run",
self._outs_command,
self.FOO,
"--overwrite-dvcfile",
"--overwrite",
"--single-stage",
f"echo {self.BAR_CONTENTS} >> {self.FOO}",
]
@@ -945,7 +946,7 @@ class TestPersistentOutput(TestDvc):
cmd = [
"run",
"--overwrite-dvcfile",
"--overwrite",
"--single-stage",
"--deps",
"immutable",
+29
View File
@@ -241,3 +241,32 @@ def test_stage_on_no_path_string_repr(tmp_dir, dvc):
assert p.addressing == "No path:stage_name"
assert repr(p) == "Stage: 'No path:stage_name'"
assert str(p) == "stage: 'No path:stage_name'"
def test_stage_remove_pipeline_stage(tmp_dir, dvc, run_copy):
tmp_dir.gen("foo", "foo")
stage = run_copy("foo", "bar", name="copy-foo-bar")
run_copy("bar", "foobar", name="copy-bar-foobar")
dvc_file = stage.dvcfile
with dvc.lock:
stage.remove(purge=False)
assert stage.name in dvc_file.stages
with dvc.lock:
stage.remove()
assert stage.name not in dvc_file.stages
assert "copy-bar-foobar" in dvc_file.stages
def test_stage_remove_pointer_stage(tmp_dir, dvc, run_copy):
(stage,) = tmp_dir.dvc_gen("foo", "foo")
with dvc.lock:
stage.remove(purge=False)
assert not (tmp_dir / "foo").exists()
assert (tmp_dir / stage.relpath).exists()
with dvc.lock:
stage.remove()
assert not (tmp_dir / stage.relpath).exists()
+1 -1
View File
@@ -27,7 +27,7 @@ def test_run(mocker, dvc):
"--wdir",
"wdir",
"--no-exec",
"--overwrite-dvcfile",
"--overwrite",
"--no-run-cache",
"--no-commit",
"--outs-persist",