cli: remove dvc run (#9508)

Note that `Repo.run()` API still exists, which will be removed in
successive PRs. A lot of tests depend on it that it'll take time
before removing it.
This commit is contained in:
skshetry
2023-05-25 16:27:14 +05:45
committed by GitHub
parent 8acd943d52
commit 0e38525d8d
9 changed files with 116 additions and 721 deletions
-2
View File
@@ -41,7 +41,6 @@ from dvc.commands import (
remove,
repro,
root,
run,
stage,
unprotect,
update,
@@ -62,7 +61,6 @@ COMMANDS = [
remove,
move,
unprotect,
run,
repro,
data_sync,
gc,
-95
View File
@@ -1,95 +0,0 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.commands.stage import parse_cmd
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
class CmdRun(CmdBase):
def run(self):
if not any(
[
self.args.deps,
self.args.outs,
self.args.outs_no_cache,
self.args.metrics,
self.args.metrics_no_cache,
self.args.plots,
self.args.plots_no_cache,
self.args.outs_persist,
self.args.outs_persist_no_cache,
self.args.params,
self.args.command,
]
): # pragma: no cover
logger.error(
"too few arguments. Specify at least one: `-d`, `-o`, `-O`, "
"`-m`, `-M`, `-p`, `--plots`, `--plots-no-cache`, "
"`--outs-persist`, `--outs-persist-no-cache`, `command`."
)
return 1
kwargs = vars(self.args)
kwargs.update(
{
"cmd": parse_cmd(self.args.command),
"fname": kwargs.pop("file"),
"no_exec": self.args.no_exec,
"run_cache": not kwargs.pop("no_run_cache"),
}
)
try:
self.repo.run(**kwargs)
except DvcException:
logger.exception("")
return 1
return 0
def add_parser(subparsers, parent_parser):
from dvc.commands.stage import _add_common_args
RUN_HELP = "Generate a dvc.yaml file from a command and execute the command."
run_parser = subparsers.add_parser(
"run",
parents=[parent_parser],
description=append_doc_link(RUN_HELP, "run"),
help=RUN_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
run_parser.add_argument("-n", "--name", help="Stage name.")
run_parser.add_argument("--file", metavar="<filename>", help=argparse.SUPPRESS)
run_parser.add_argument(
"--single-stage",
action="store_true",
default=False,
help=argparse.SUPPRESS,
)
_add_common_args(run_parser)
run_parser.add_argument(
"--no-exec",
action="store_true",
default=False,
help="Only create dvc.yaml without actually running it.",
)
run_parser.add_argument(
"--no-commit",
action="store_true",
default=False,
help="Don't put files/directories into cache.",
)
run_parser.add_argument(
"--no-run-cache",
action="store_true",
default=False,
help=(
"Execute the command even if this stage has already been run "
"with the same command/dependencies/outputs/etc before."
),
)
run_parser.set_defaults(func=CmdRun)
+2 -11
View File
@@ -362,10 +362,6 @@ def test_should_update_state_entry_for_file_after_add(mocker, dvc, tmp_dir):
assert ret == 0
assert file_md5_counter.mock.call_count == 1
ret = main(["run", "--single-stage", "-d", "foo", "echo foo"])
assert ret == 0
assert file_md5_counter.mock.call_count == 1
os.rename("foo", "foo.back")
ret = main(["checkout"])
assert ret == 0
@@ -392,19 +388,14 @@ def test_should_update_state_entry_for_directory_after_add(mocker, dvc, tmp_dir)
assert ret == 0
assert file_md5_counter.mock.call_count == 5
ls = "dir" if os.name == "nt" else "ls"
ret = main(["run", "--single-stage", "-d", "data", "{} {}".format(ls, "data")])
assert ret == 0
assert file_md5_counter.mock.call_count == 7
os.rename("data", "data.back")
ret = main(["checkout"])
assert ret == 0
assert file_md5_counter.mock.call_count == 7
assert file_md5_counter.mock.call_count == 5
ret = main(["status"])
assert ret == 0
assert file_md5_counter.mock.call_count == 9
assert file_md5_counter.mock.call_count == 6
def test_add_commit(tmp_dir, dvc):
-49
View File
@@ -11,7 +11,6 @@ from dvc.commands.data_sync import CmdDataPull, CmdDataPush
from dvc.commands.init import CmdInit
from dvc.commands.remove import CmdRemove
from dvc.commands.repro import CmdRepro
from dvc.commands.run import CmdRun
from dvc.commands.status import CmdDataStatus
from dvc.exceptions import NotDvcRepoError
@@ -21,54 +20,6 @@ def test_argparse(dvc):
assert isinstance(args.func(args), CmdInit)
def test_run(dvc):
dep1 = "dep1"
dep2 = "dep2"
out1 = "out1"
out2 = "out2"
out_no_cache1 = "out_no_cache1"
out_no_cache2 = "out_no_cache2"
fname = "dvc.dvc"
cmd = "cmd"
arg1 = "arg1"
arg2 = "arg2"
args = parse_args(
[
"run",
"-d",
dep1,
"--deps",
dep2,
"-o",
out1,
"--outs",
out2,
"-O",
out_no_cache1,
"--outs-no-cache",
out_no_cache2,
"--file",
fname,
cmd,
arg1,
arg2,
]
)
cmd_cls = args.func(args)
assert isinstance(cmd_cls, CmdRun)
assert args.deps == [dep1, dep2]
assert args.outs == [out1, out2]
assert args.outs_no_cache == [out_no_cache1, out_no_cache2]
assert args.file == fname
assert args.command == [cmd, arg1, arg2]
cmd_cls.repo.close()
def test_pull(dvc):
args = parse_args(["pull"])
cmd = args.func(args)
-2
View File
@@ -147,8 +147,6 @@ def test_hash_recalculation(mocker, dvc, tmp_dir, local_remote):
assert ret == 0
ret = main(["push"])
assert ret == 0
ret = main(["run", "--single-stage", "-d", "foo", "echo foo"])
assert ret == 0
assert test_file_md5.mock.call_count == 1
+51 -27
View File
@@ -335,9 +335,10 @@ def test_repro_dry_no_exec(tmp_dir, dvc):
ret = main(
[
"run",
"--no-exec",
"--single-stage",
"stage",
"add",
"-n",
f"copy-{idir}-{odir}",
"-d",
idir,
"-o",
@@ -351,18 +352,17 @@ def test_repro_dry_no_exec(tmp_dir, dvc):
ret = main(
[
"run",
"--no-exec",
"--single-stage",
"--file",
DVC_FILE,
"stage",
"add",
"-n",
"ls",
*deps,
"ls {}".format(" ".join(dep for i, dep in enumerate(deps) if i % 2)),
]
)
assert ret == 0
ret = main(["repro", "--dry", DVC_FILE])
ret = main(["repro", "--dry", "ls"])
assert ret == 0
@@ -1114,14 +1114,27 @@ def test_downstream(dvc):
# \ /
# A
#
assert main(["run", "--single-stage", "-o", "A", "echo A>A"]) == 0
assert main(["run", "--single-stage", "-d", "A", "-o", "B", "echo B>B"]) == 0
assert main(["run", "--single-stage", "-d", "A", "-o", "C", "echo C>C"]) == 0
assert main(["stage", "add", "-n", "stage-A", "--run", "-o", "A", "echo A>A"]) == 0
assert (
main(
["stage", "add", "-n", "stage-B", "--run", "-d", "A", "-o", "B", "echo B>B"]
)
== 0
)
assert (
main(
["stage", "add", "-n", "stage-C", "--run", "-d", "A", "-o", "C", "echo C>C"]
)
== 0
)
assert (
main(
[
"run",
"--single-stage",
"stage",
"add",
"-n",
"stage-D",
"--run",
"-d",
"B",
"-d",
@@ -1133,13 +1146,21 @@ def test_downstream(dvc):
)
== 0
)
assert main(["run", "--single-stage", "-o", "G", "echo G>G"]) == 0
assert main(["run", "--single-stage", "-d", "G", "-o", "F", "echo F>F"]) == 0
assert main(["stage", "add", "-n", "stage-G", "--run", "-o", "G", "echo G>G"]) == 0
assert (
main(
["stage", "add", "-n", "stage-F", "--run", "-d", "G", "-o", "F", "echo F>F"]
)
== 0
)
assert (
main(
[
"run",
"--single-stage",
"stage",
"add",
"-n",
"stage-E",
"--run",
"-d",
"D",
"-d",
@@ -1160,22 +1181,25 @@ def test_downstream(dvc):
# /
# B
#
evaluation = dvc.reproduce("B.dvc", downstream=True, force=True)
evaluation = dvc.reproduce("stage-B", downstream=True, force=True)
assert len(evaluation) == 3
assert evaluation[0].relpath == "B.dvc"
assert evaluation[1].relpath == "D.dvc"
assert evaluation[2].relpath == "E.dvc"
assert evaluation[0].addressing == "stage-B"
assert evaluation[1].addressing == "stage-D"
assert evaluation[2].addressing == "stage-E"
# B, C should be run (in any order) before D
# See https://github.com/iterative/dvc/issues/3602
evaluation = dvc.reproduce("A.dvc", downstream=True, force=True)
evaluation = dvc.reproduce("stage-A", downstream=True, force=True)
assert len(evaluation) == 5
assert evaluation[0].relpath == "A.dvc"
assert {evaluation[1].relpath, evaluation[2].relpath} == {"B.dvc", "C.dvc"}
assert evaluation[3].relpath == "D.dvc"
assert evaluation[4].relpath == "E.dvc"
assert evaluation[0].addressing == "stage-A"
assert {evaluation[1].addressing, evaluation[2].addressing} == {
"stage-B",
"stage-C",
}
assert evaluation[3].addressing == "stage-D"
assert evaluation[4].addressing == "stage-E"
def test_repro_when_cmd_changes(tmp_dir, dvc, run_copy, mocker):
+62 -38
View File
@@ -36,7 +36,7 @@ def test_repro_frozen(tmp_dir, dvc, run_copy):
assert stages == [data_stage, stage0]
def test_downstream(tmp_dir, dvc):
def test_downstream(M, tmp_dir, dvc):
# The dependency graph should look like this:
#
# E
@@ -47,19 +47,60 @@ def test_downstream(tmp_dir, dvc):
# \ /
# A
#
assert main(["run", "-n", "A-gen", "-o", "A", "echo A>A"]) == 0
assert main(["run", "-n", "B-gen", "-d", "A", "-o", "B", "echo B>B"]) == 0
assert main(["run", "--single-stage", "-d", "A", "-o", "C", "echo C>C"]) == 0
assert main(["stage", "add", "--run", "-n", "A-gen", "-o", "A", "echo A>A"]) == 0
assert (
main(["run", "-n", "D-gen", "-d", "B", "-d", "C", "-o", "D", "echo D>D"]) == 0
main(["stage", "add", "--run", "-n", "B-gen", "-d", "A", "-o", "B", "echo B>B"])
== 0
)
assert main(["run", "--single-stage", "-o", "G", "echo G>G"]) == 0
assert main(["run", "-n", "F-gen", "-d", "G", "-o", "F", "echo F>F"]) == 0
assert (
main(
[
"run",
"--single-stage",
"stage",
"add",
"--run",
"-n",
"C-gen",
"-d",
"A",
"-o",
"C",
"echo C>C",
]
)
== 0
)
assert (
main(
[
"stage",
"add",
"--run",
"-n",
"D-gen",
"-d",
"B",
"-d",
"C",
"-o",
"D",
"echo D>D",
]
)
== 0
)
assert main(["stage", "add", "--run", "-n", "G-gen", "-o", "G", "echo G>G"]) == 0
assert (
main(["stage", "add", "--run", "-n", "F-gen", "-d", "G", "-o", "F", "echo F>F"])
== 0
)
assert (
main(
[
"stage",
"add",
"--run",
"-n",
"E-gen",
"-d",
"D",
"-d",
@@ -83,41 +124,24 @@ def test_downstream(tmp_dir, dvc):
evaluation = dvc.reproduce(PROJECT_FILE + ":B-gen", downstream=True, force=True)
assert len(evaluation) == 3
assert isinstance(evaluation[0], PipelineStage)
assert evaluation[0].relpath == PROJECT_FILE
assert evaluation[0].name == "B-gen"
assert isinstance(evaluation[1], PipelineStage)
assert evaluation[1].relpath == PROJECT_FILE
assert evaluation[1].name == "D-gen"
assert not isinstance(evaluation[2], PipelineStage)
assert evaluation[2].relpath == "E.dvc"
assert all(isinstance(stage, PipelineStage) for stage in evaluation)
assert all(stage.relpath == PROJECT_FILE for stage in evaluation)
assert [stage.name for stage in evaluation] == ["B-gen", "D-gen", "E-gen"]
# B, C should be run (in any order) before D
# See https://github.com/iterative/dvc/issues/3602
evaluation = dvc.reproduce(PROJECT_FILE + ":A-gen", downstream=True, force=True)
assert len(evaluation) == 5
assert isinstance(evaluation[0], PipelineStage)
assert evaluation[0].relpath == PROJECT_FILE
assert evaluation[0].name == "A-gen"
names = set()
for stage in evaluation[1:3]:
if isinstance(stage, PipelineStage):
assert stage.relpath == PROJECT_FILE
names.add(stage.name)
else:
names.add(stage.relpath)
assert names == {"B-gen", "C.dvc"}
assert isinstance(evaluation[3], PipelineStage)
assert evaluation[3].relpath == PROJECT_FILE
assert evaluation[3].name == "D-gen"
assert not isinstance(evaluation[4], PipelineStage)
assert evaluation[4].relpath == "E.dvc"
assert all(isinstance(stage, PipelineStage) for stage in evaluation)
assert all(stage.relpath == PROJECT_FILE for stage in evaluation)
assert [stage.name for stage in evaluation] == [
"A-gen",
M.any_of("B-gen", "C-gen"),
M.any_of("B-gen", "C-gen"),
"D-gen",
"E-gen",
]
def test_repro_when_cmd_changes(tmp_dir, dvc, run_copy, mocker):
+1 -353
View File
@@ -18,8 +18,7 @@ from dvc.exceptions import (
OverlappingOutputPathsError,
StagePathAsOutputError,
)
from dvc.fs import system
from dvc.output import Output, OutputIsStageFileError
from dvc.output import OutputIsStageFileError
from dvc.stage import Stage
from dvc.stage.exceptions import (
StageFileAlreadyExistsError,
@@ -307,262 +306,6 @@ def test_run_remove_outs(tmp_dir, dvc, append_foo_script):
)
def test_run_unprotect_outs_copy(tmp_dir, dvc, append_foo_script):
ret = main(["config", "cache.type", "copy"])
assert ret == 0
ret = main(
[
"run",
"-d",
"append_foo.py",
"-o",
"foo",
"--single-stage",
"python",
"append_foo.py",
"foo",
]
)
assert ret == 0
assert os.access("foo", os.W_OK)
with open("foo", encoding="utf-8") as fd:
assert fd.read() == "foo"
ret = main(
[
"run",
"--force",
"--no-run-cache",
"--single-stage",
"-d",
"append_foo.py",
"-o",
"foo",
"python",
"append_foo.py",
"foo",
]
)
assert ret == 0
assert os.access("foo", os.W_OK)
with open("foo", encoding="utf-8") as fd:
assert fd.read() == "foo"
def test_run_unprotect_outs_symlink(tmp_dir, dvc, append_foo_script):
ret = main(["config", "cache.type", "symlink"])
assert ret == 0
assert ret == 0
ret = main(
[
"run",
"-d",
"append_foo.py",
"-o",
"foo",
"--single-stage",
"python",
"append_foo.py",
"foo",
]
)
assert ret == 0
if os.name == "nt":
# NOTE: Windows symlink perms don't propagate to the target
assert os.access("foo", os.W_OK)
else:
assert not os.access("foo", os.W_OK)
assert system.is_symlink("foo")
with open("foo", encoding="utf-8") as fd:
assert fd.read() == "foo"
ret = main(
[
"run",
"--force",
"--no-run-cache",
"--single-stage",
"-d",
"append_foo.py",
"-o",
"foo",
"python",
"append_foo.py",
"foo",
]
)
assert ret == 0
if os.name == "nt":
# NOTE: Windows symlink perms don't propagate to the target
assert os.access("foo", os.W_OK)
else:
assert not os.access("foo", os.W_OK)
assert system.is_symlink("foo")
with open("foo", encoding="utf-8") as fd:
assert fd.read() == "foo"
def test_run_unprotect_outs_hardlink(tmp_dir, dvc, append_foo_script):
ret = main(["config", "cache.type", "hardlink"])
assert ret == 0
assert ret == 0
ret = main(
[
"run",
"-d",
"append_foo.py",
"-o",
"foo",
"--single-stage",
"python",
"append_foo.py",
"foo",
]
)
assert ret == 0
assert not os.access("foo", os.W_OK)
assert system.is_hardlink("foo")
with open("foo", encoding="utf-8") as fd:
assert fd.read() == "foo"
ret = main(
[
"run",
"--force",
"--no-run-cache",
"--single-stage",
"-d",
"append_foo.py",
"-o",
"foo",
"python",
"append_foo.py",
"foo",
]
)
assert ret == 0
assert not os.access("foo", os.W_OK)
assert system.is_hardlink("foo")
with open("foo", encoding="utf-8") as fd:
assert fd.read() == "foo"
def test_cmd_run_overwrite(tmp_dir, dvc, copy_script):
# NOTE: using sleep() is a workaround for filesystems
# with low mtime resolution. We have to use mtime since
# comparing mtime's is the only way to check that the stage
# file didn't change(size and inode in the first test down
# below don't change).
tmp_dir.gen({"foo": "foo", "bar": "bar"})
import time
ret = main(
[
"run",
"-d",
"foo",
"-d",
"copy.py",
"-o",
"out",
"--file",
"out.dvc",
"--single-stage",
"python",
"copy.py",
"foo",
"out",
]
)
assert ret == 0
stage_mtime = os.path.getmtime("out.dvc")
time.sleep(1)
ret = main(
[
"run",
"-d",
"foo",
"-d",
"copy.py",
"--force",
"--no-run-cache",
"--single-stage",
"-o",
"out",
"--file",
"out.dvc",
"python",
"copy.py",
"foo",
"out",
]
)
assert ret == 0
# NOTE: check that dvcfile was overwritten
assert stage_mtime != os.path.getmtime("out.dvc")
stage_mtime = os.path.getmtime("out.dvc")
time.sleep(1)
ret = main(
[
"run",
"--force",
"--single-stage",
"--file",
"out.dvc",
"-d",
"bar",
"cat bar",
]
)
assert ret == 0
# NOTE: check that dvcfile was overwritten
assert stage_mtime != os.path.getmtime("out.dvc")
class TestCmdRunCliMetrics:
def test_cached(self, dvc):
ret = main(
[
"run",
"-m",
"metrics.txt",
"--single-stage",
"echo test > metrics.txt",
]
)
assert ret == 0
with open("metrics.txt", encoding="utf-8") as fd:
assert fd.read().rstrip() == "test"
def test_not_cached(self, dvc):
ret = main(
[
"run",
"-M",
"metrics.txt",
"--single-stage",
"echo test > metrics.txt",
]
)
assert ret == 0
with open("metrics.txt", encoding="utf-8") as fd:
assert fd.read().rstrip() == "test"
class TestCmdRunWorkingDirectory:
def test_default_wdir_is_not_written(self, tmp_dir, dvc):
stage = dvc.run(
@@ -665,62 +408,6 @@ def test_rerun_changed_out(tmp_dir, run_copy):
run_copy("foo", "out", force=False, single_stage=True)
def test_run_commit(dvc):
fname = "test"
ret = main(
[
"run",
"-o",
fname,
"--no-commit",
"--single-stage",
"echo",
"test",
">",
fname,
]
)
assert ret == 0
assert os.path.isfile(fname)
assert not os.path.exists(dvc.cache.local.path)
ret = main(["commit", fname + ".dvc"])
assert ret == 0
assert os.path.isfile(fname)
assert len(list(dvc.cache.local.all())) == 1
@pytest.mark.parametrize(
"outs_command",
[
"--outs-persist",
"--outs-persist-no-cache",
],
)
def test_run_persist(tmp_dir, dvc, outs_command):
assert (
main(
[
"run",
"--single-stage",
"--always-changed",
outs_command,
"file",
"echo content>>file",
]
)
== 0
)
d = (tmp_dir / "file.dvc").load_yaml()
assert d["outs"][0][Output.PARAM_PERSIST]
assert main(["repro", "file.dvc"]) == 0
assert (tmp_dir / "file").read_text() == "content\ncontent\n"
assert main(["remove", "file.dvc", "--outs"]) == 0
assert not (tmp_dir / "file").exists()
def test_should_raise_on_overlapping_output_paths(tmp_dir, dvc, append_foo_script):
tmp_dir.gen("data", {"foo": "foo", "bar": "bar"})
ret = main(["add", "data"])
@@ -746,45 +433,6 @@ def test_should_raise_on_overlapping_output_paths(tmp_dir, dvc, append_foo_scrip
)
@pytest.mark.parametrize(
"outs_command, first_expected, second_expected",
[
("--outs", "foo\n", "bar\n"),
("--outs-persist", "foo\n", "foo\nbar\n"),
],
)
def test_rerun_with_same_outputs(
tmp_dir, dvc, outs_command, first_expected, second_expected
):
assert (
main(
[
"run",
"--single-stage",
"--outs",
"foo",
"echo foo>foo",
]
)
== 0
)
assert (tmp_dir / "foo").read_text() == first_expected
assert (
main(
[
"run",
outs_command,
"foo",
"--force",
"--single-stage",
"echo bar>>foo",
]
)
== 0
)
assert (tmp_dir / "foo").read_text() == second_expected
def test_should_not_checkout_upon_corrupted_local_hardlink_cache(
mocker, tmp_dir, dvc, copy_script
):
-144
View File
@@ -1,144 +0,0 @@
from dvc.cli import parse_args
from dvc.commands.run import CmdRun
from tests.utils.asserts import called_once_with_subset
def test_run(mocker, dvc):
cli_args = parse_args(
[
"run",
"--name",
"name",
"--deps",
"deps",
"--outs",
"outs",
"--outs-no-cache",
"outs-no-cache",
"--metrics",
"metrics",
"--metrics-no-cache",
"metrics-no-cache",
"--plots",
"plots",
"--plots-no-cache",
"plots-no-cache",
"--file",
"file",
"--wdir",
"wdir",
"--no-exec",
"--force",
"--no-run-cache",
"--no-commit",
"--outs-persist",
"outs-persist",
"--outs-persist-no-cache",
"outs-persist-no-cache",
"--always-changed",
"--params",
"file:param1,param2",
"--params",
"param3",
"--external",
"--desc",
"description",
"command",
]
)
assert cli_args.func == CmdRun
cmd = cli_args.func(cli_args)
m = mocker.patch.object(cmd.repo, "run", autospec=True)
assert cmd.run() == 0
assert called_once_with_subset(
m,
deps=["deps"],
outs=["outs"],
outs_no_cache=["outs-no-cache"],
metrics=["metrics"],
metrics_no_cache=["metrics-no-cache"],
plots=["plots"],
plots_no_cache=["plots-no-cache"],
outs_persist=["outs-persist"],
outs_persist_no_cache=["outs-persist-no-cache"],
params=["file:param1,param2", "param3"],
fname="file",
wdir="wdir",
no_exec=True,
force=True,
run_cache=False,
no_commit=True,
always_changed=True,
cmd="command",
name="name",
single_stage=False,
external=True,
desc="description",
)
def test_run_args_from_cli(mocker, dvc):
args = parse_args(["run", "echo", "foo"])
cmd = args.func(args)
m = mocker.patch.object(cmd.repo, "run", autospec=True)
assert cmd.run() == 0
assert called_once_with_subset(
m,
deps=[],
outs=[],
outs_no_cache=[],
metrics=[],
metrics_no_cache=[],
plots=[],
plots_no_cache=[],
outs_persist=[],
outs_persist_no_cache=[],
params=[],
fname=None,
wdir=None,
no_exec=False,
force=False,
run_cache=True,
no_commit=False,
always_changed=False,
cmd="echo foo",
name=None,
single_stage=False,
external=False,
desc=None,
)
def test_run_args_with_spaces(mocker, dvc):
args = parse_args(["run", "echo", "foo bar"])
cmd = args.func(args)
m = mocker.patch.object(cmd.repo, "run", autospec=True)
assert cmd.run() == 0
assert called_once_with_subset(
m,
deps=[],
outs=[],
outs_no_cache=[],
metrics=[],
metrics_no_cache=[],
plots=[],
plots_no_cache=[],
outs_persist=[],
outs_persist_no_cache=[],
params=[],
fname=None,
wdir=None,
no_exec=False,
force=False,
run_cache=True,
no_commit=False,
always_changed=False,
cmd='echo "foo bar"',
name=None,
single_stage=False,
external=False,
desc=None,
)