Skip to content

Commit 357ce82

Browse files
committed
run: introduce --always-changed
This is an extension of our callback stages (no deps, so always considered as changed) for stages with dependencies, so that these stages could be used properly in the middle of the DAG. See attached issue for more info. Related to #2378
1 parent b3e4ff7 commit 357ce82

File tree

4 files changed

+31
-1
lines changed

4 files changed

+31
-1
lines changed

dvc/command/run.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def run(self):
5151
no_commit=self.args.no_commit,
5252
outs_persist=self.args.outs_persist,
5353
outs_persist_no_cache=self.args.outs_persist_no_cache,
54+
always_changed=self.args.always_changed,
5455
)
5556
except DvcException:
5657
logger.exception("failed to run command")
@@ -189,6 +190,12 @@ def add_parser(subparsers, parent_parser):
189190
help="Declare output file or directory that will not be "
190191
"removed upon repro (do not put into DVC cache).",
191192
)
193+
run_parser.add_argument(
194+
"--always-changed",
195+
action="store_true",
196+
default=False,
197+
help="Always consider this DVC-file as changed.",
198+
)
192199
run_parser.add_argument(
193200
"command", nargs=argparse.REMAINDER, help="Command to execute."
194201
)

dvc/stage.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ class Stage(object):
139139
PARAM_OUTS = "outs"
140140
PARAM_LOCKED = "locked"
141141
PARAM_META = "meta"
142+
PARAM_ALWAYS_CHANGED = "always_changed"
142143

143144
SCHEMA = {
144145
Optional(PARAM_MD5): Or(str, None),
@@ -148,6 +149,7 @@ class Stage(object):
148149
Optional(PARAM_OUTS): Or(And(list, Schema([output.SCHEMA])), None),
149150
Optional(PARAM_LOCKED): bool,
150151
Optional(PARAM_META): object,
152+
Optional(PARAM_ALWAYS_CHANGED): bool,
151153
}
152154

153155
TAG_REGEX = r"^(?P<path>.*)@(?P<tag>[^\\/@:]*)$"
@@ -164,6 +166,7 @@ def __init__(
164166
locked=False,
165167
tag=None,
166168
state=None,
169+
always_changed=False,
167170
):
168171
if deps is None:
169172
deps = []
@@ -179,6 +182,7 @@ def __init__(
179182
self.md5 = md5
180183
self.locked = locked
181184
self.tag = tag
185+
self.always_changed = always_changed
182186
self._state = state or {}
183187

184188
def __repr__(self):
@@ -244,6 +248,9 @@ def _changed_deps(self):
244248
)
245249
return True
246250

251+
if self.always_changed:
252+
return True
253+
247254
for dep in self.deps:
248255
status = dep.status()
249256
if status:
@@ -457,6 +464,7 @@ def create(repo, **kwargs):
457464
wdir=wdir,
458465
cmd=kwargs.get("cmd", None),
459466
locked=kwargs.get("locked", False),
467+
always_changed=kwargs.get("always_changed", False),
460468
)
461469

462470
Stage._fill_stage_outputs(stage, **kwargs)
@@ -515,6 +523,7 @@ def create(repo, **kwargs):
515523
not ignore_build_cache
516524
and stage.is_cached
517525
and not stage.is_callback
526+
and not stage.always_changed
518527
):
519528
logger.info("Stage is cached, skipping.")
520529
return None
@@ -619,6 +628,7 @@ def load(repo, fname):
619628
md5=d.get(Stage.PARAM_MD5),
620629
locked=d.get(Stage.PARAM_LOCKED, False),
621630
tag=tag,
631+
always_changed=d.get(Stage.PARAM_ALWAYS_CHANGED, False),
622632
state=state,
623633
)
624634

@@ -639,6 +649,7 @@ def dumpd(self):
639649
Stage.PARAM_DEPS: [d.dumpd() for d in self.deps],
640650
Stage.PARAM_OUTS: [o.dumpd() for o in self.outs],
641651
Stage.PARAM_META: self._state.get("meta"),
652+
Stage.PARAM_ALWAYS_CHANGED: self.always_changed,
642653
}.items()
643654
if value
644655
}
@@ -839,6 +850,7 @@ def run(self, dry=False, no_commit=False, force=False):
839850
if (
840851
not force
841852
and not self.is_callback
853+
and not self.always_changed
842854
and self._already_cached()
843855
):
844856
self.checkout()
@@ -885,7 +897,7 @@ def status(self):
885897
if self.changed_md5():
886898
ret.append("changed checksum")
887899

888-
if self.is_callback:
900+
if self.is_callback or self.always_changed:
889901
ret.append("always changed")
890902

891903
if ret:

tests/unit/command/test_run.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def test_run(mocker, dvc_repo):
3232
"outs-persist",
3333
"--outs-persist-no-cache",
3434
"outs-persist-no-cache",
35+
"--always-changed",
3536
"command",
3637
]
3738
)
@@ -58,6 +59,7 @@ def test_run(mocker, dvc_repo):
5859
ignore_build_cache=True,
5960
remove_outs=True,
6061
no_commit=True,
62+
always_changed=True,
6163
cmd="command",
6264
)
6365

@@ -83,6 +85,7 @@ def test_run_args_from_cli(mocker, dvc_repo):
8385
ignore_build_cache=False,
8486
remove_outs=False,
8587
no_commit=False,
88+
always_changed=False,
8689
cmd="echo foo",
8790
)
8891

@@ -108,5 +111,6 @@ def test_run_args_with_spaces(mocker, dvc_repo):
108111
ignore_build_cache=False,
109112
remove_outs=False,
110113
no_commit=False,
114+
always_changed=False,
111115
cmd='echo "foo bar"',
112116
)

tests/unit/test_stage.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,10 @@ def test_stage_run_ignore_sigint(mocker):
107107
assert communicate.called_once_with()
108108
signal_mock.assert_any_call(signal.SIGINT, signal.SIG_IGN)
109109
assert signal.getsignal(signal.SIGINT) == signal.default_int_handler
110+
111+
112+
def test_always_changed():
113+
stage = Stage(None, "path", always_changed=True)
114+
stage.save()
115+
assert stage.changed()
116+
assert stage.status()["path"] == ["always changed"]

0 commit comments

Comments
 (0)