diff --git a/dvc/command/remove.py b/dvc/command/remove.py index ab3a963a9b..4409abf1df 100644 --- a/dvc/command/remove.py +++ b/dvc/command/remove.py @@ -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 @@ 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 @@ def run(self): 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", - "--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", + "--outs", action="store_true", default=False, - help="Force purge.", + help="Remove outputs as well.", ) remove_parser.add_argument( "targets", nargs="+", help="DVC-files to remove." diff --git a/dvc/command/run.py b/dvc/command/run.py index 77f6538308..6f56e754e9 100644 --- a/dvc/command/run.py +++ b/dvc/command/run.py @@ -45,7 +45,7 @@ def run(self): 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", diff --git a/dvc/dvcfile.py b/dvc/dvcfile.py index 43e6cbffb7..aab3e4ed81 100644 --- a/dvc/dvcfile.py +++ b/dvc/dvcfile.py @@ -150,7 +150,7 @@ def dump(self, stage, **kwargs): "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 @@ def remove_with_prompt(self, force=False): self.remove() + def remove_stage(self, stage): + self.remove() + class PipelineFile(FileMixin): """Abstraction for pipelines file, .yaml + .lock combined.""" @@ -212,7 +215,7 @@ def _dump_pipeline_file(self, stage): "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 @@ def remove(self, force=False): 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 @@ def dump(self, stage, **kwargs): 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): diff --git a/dvc/repo/remove.py b/dvc/repo/remove.py index d00f330099..0aa80adbfd 100644 --- a/dvc/repo/remove.py +++ b/dvc/repo/remove.py @@ -7,15 +7,11 @@ @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 diff --git a/dvc/repo/run.py b/dvc/repo/run.py index 374068eac6..313d00feaa 100644 --- a/dvc/repo/run.py +++ b/dvc/repo/run.py @@ -3,7 +3,11 @@ 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]) diff --git a/dvc/stage/__init__.py b/dvc/stage/__init__.py index ac429ad0ff..725cb1e0e6 100644 --- a/dvc/stage/__init__.py +++ b/dvc/stage/__init__.py @@ -282,12 +282,13 @@ def unprotect_outs(self): 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): diff --git a/tests/func/test_dvcfile.py b/tests/func/test_dvcfile.py index 4ba82e5bd5..4f62c082d3 100644 --- a/tests/func/test_dvcfile.py +++ b/tests/func/test_dvcfile.py @@ -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() + ) diff --git a/tests/func/test_gc.py b/tests/func/test_gc.py index 0ddb6e025a..a12f4b6e85 100644 --- a/tests/func/test_gc.py +++ b/tests/func/test_gc.py @@ -68,7 +68,7 @@ def test(self): 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 @@ def test(self): 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 @@ def test(self): 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) diff --git a/tests/func/test_remove.py b/tests/func/test_remove.py index a6944609f7..ac6057e65e 100644 --- a/tests/func/test_remove.py +++ b/tests/func/test_remove.py @@ -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)) + 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) - (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)) +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 TestRemoveNonExistentFile(TestDvc): - def test(self): - with self.assertRaises(StageFileDoesNotExistError): - self.dvc.remove("non_existent_dvc_file") +def test_remove_broken_symlink(tmp_dir, dvc): + tmp_dir.gen("foo", "foo") + dvc.cache.local.cache_types = ["symlink"] -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) + (stage,) = dvc.add("foo") + remove(dvc.cache.local.cache_dir) + assert System.is_symlink("foo") + with pytest.raises(DvcException): + dvc.remove(stage.addressing) + assert os.path.lexists("foo") + assert (tmp_dir / stage.relpath).exists() -def test_remove_pipeline_outs(tmp_dir, dvc, run_copy): - from dvc.dvcfile import PIPELINE_FILE + dvc.remove(stage.addressing, outs=True) + assert not os.path.lexists("foo") + assert not (tmp_dir / stage.relpath).exists() - tmp_dir.gen("foo", "foo") - stage = run_copy("foo", "bar", name="copy-foo-bar") - assert main(["remove", stage.path]) == 0 - assert not (tmp_dir / "bar").exists() - assert (tmp_dir / PIPELINE_FILE).exists() - - stage = run_copy("foo", "foobar", name="copy-foo-foobar") +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 (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 not (tmp_dir / stage.relpath).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() diff --git a/tests/func/test_repro.py b/tests/func/test_repro.py index ab9720dee6..fe8f707baa 100644 --- a/tests/func/test_repro.py +++ b/tests/func/test_repro.py @@ -983,7 +983,8 @@ def test(self, mock_prompt): 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 @@ def test(self): 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 @@ def test(self): 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 @@ def test(self): 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) diff --git a/tests/func/test_run_multistage.py b/tests/func/test_run_multistage.py index 71862411a5..50377423d0 100644 --- a/tests/func/test_run_multistage.py +++ b/tests/func/test_run_multistage.py @@ -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 = { diff --git a/tests/func/test_run_single_stage.py b/tests/func/test_run_single_stage.py index 362e218e1a..6afa23748e 100644 --- a/tests/func/test_run_single_stage.py +++ b/tests/func/test_run_single_stage.py @@ -350,7 +350,7 @@ def test(self): ret = main( [ "run", - "--overwrite-dvcfile", + "--overwrite", "--no-run-cache", "--single-stage", "-d", @@ -408,7 +408,7 @@ def test(self): ret = main( [ "run", - "--overwrite-dvcfile", + "--overwrite", "--no-run-cache", "--single-stage", "-d", @@ -467,7 +467,7 @@ def test(self): ret = main( [ "run", - "--overwrite-dvcfile", + "--overwrite", "--no-run-cache", "--single-stage", "-d", @@ -552,7 +552,7 @@ def test(self): self.FOO, "-d", self.CODE, - "--overwrite-dvcfile", + "--overwrite", "--no-run-cache", "--single-stage", "-o", @@ -576,7 +576,7 @@ def test(self): 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 @@ def should_append_upon_repro(self, file, stage_file): 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 @@ def _run_twice_with_same_outputs(self): "run", self._outs_command, self.FOO, - "--overwrite-dvcfile", + "--overwrite", "--single-stage", f"echo {self.BAR_CONTENTS} >> {self.FOO}", ] @@ -945,7 +946,7 @@ def test_ignore_run_cache(self): cmd = [ "run", - "--overwrite-dvcfile", + "--overwrite", "--single-stage", "--deps", "immutable", diff --git a/tests/func/test_stage.py b/tests/func/test_stage.py index 599d99134d..dbad827242 100644 --- a/tests/func/test_stage.py +++ b/tests/func/test_stage.py @@ -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() diff --git a/tests/unit/command/test_run.py b/tests/unit/command/test_run.py index 87695291a1..ad6e9d921f 100644 --- a/tests/unit/command/test_run.py +++ b/tests/unit/command/test_run.py @@ -27,7 +27,7 @@ def test_run(mocker, dvc): "--wdir", "wdir", "--no-exec", - "--overwrite-dvcfile", + "--overwrite", "--no-run-cache", "--no-commit", "--outs-persist",