diff --git a/dvc/cli.py b/dvc/cli.py index 26574d0c7..48598f664 100644 --- a/dvc/cli.py +++ b/dvc/cli.py @@ -324,6 +324,12 @@ def parse_args(argv=None): action='store_true', default=False, help='Show metrics after reproduction') + repro_parser.add_argument( + '--dry', + action='store_true', + default=False, + help='Only print the commands that would be executed ' + 'without actually executing.') repro_parser.set_defaults(func=CmdRepro) # Remove diff --git a/dvc/command/repro.py b/dvc/command/repro.py index aed5390f3..1844e4ddd 100644 --- a/dvc/command/repro.py +++ b/dvc/command/repro.py @@ -17,7 +17,8 @@ class CmdRepro(CmdBase): try: self.project.reproduce(target, recursive=recursive, - force=self.args.force) + force=self.args.force, + dry=self.args.dry) if self.args.metrics: self.project.metrics_show() except ReproductionError as ex: diff --git a/dvc/project.py b/dvc/project.py index 6ad5b3e50..4f07eb926 100644 --- a/dvc/project.py +++ b/dvc/project.py @@ -235,7 +235,7 @@ class Project(object): stage.dump() return stage - def _reproduce_stage(self, stages, node, force): + def _reproduce_stage(self, stages, node, force, dry): stage = stages[node] if stage.locked: @@ -243,13 +243,16 @@ class Project(object): 'going to be reproduced.' self.logger.warn(msg.format(stage.relpath)) - stage = stage.reproduce(force=force) + stage = stage.reproduce(force=force, dry=dry) if not stage: return [] - stage.dump() + + if not dry: + stage.dump() + return [stage] - def reproduce(self, target, recursive=True, force=False): + def reproduce(self, target, recursive=True, force=False, dry=False): import networkx as nx G = self.graph()[1] @@ -259,17 +262,17 @@ class Project(object): raise NotDvcFileError(target) if recursive: - return self._reproduce_stages(G, stages, node, force) + return self._reproduce_stages(G, stages, node, force, dry) - return self._reproduce_stage(stages, node, force) + return self._reproduce_stage(stages, node, force, dry) - def _reproduce_stages(self, G, stages, node, force): + def _reproduce_stages(self, G, stages, node, force, dry): import networkx as nx result = [] for n in nx.dfs_postorder_nodes(G, node): try: - result += self._reproduce_stage(stages, n, force) + result += self._reproduce_stage(stages, n, force, dry) except Exception as ex: raise ReproductionError(stages[n].relpath, ex) return result diff --git a/dvc/stage.py b/dvc/stage.py index 5866e45d8..f3b19816e 100644 --- a/dvc/stage.py +++ b/dvc/stage.py @@ -150,17 +150,17 @@ class Stage(object): self.remove_outs(ignore_remove=True) os.unlink(self.path) - def reproduce(self, force=False): + def reproduce(self, force=False, dry=False): if not self.changed() and not force: return None - if (self.cmd or self.is_import) and not self.locked: + if (self.cmd or self.is_import) and not self.locked and not dry: # Removing outputs only if we actually have command to reproduce self.remove_outs(ignore_remove=False) self.project.logger.info(u'Reproducing \'{}\''.format(self.relpath)) - self.run() + self.run(dry=dry) msg = u'\'{}\' was reproduced'.format(self.relpath) self.project.logger.debug(msg) @@ -297,35 +297,40 @@ class Stage(object): for out in self.outs: out.save() - def run(self): + def run(self, dry=False): if self.locked: msg = u'Verifying outputs in locked stage \'{}\'' self.project.logger.info(msg.format(self.relpath)) - self.check_missing_outputs() + if not dry: + self.check_missing_outputs() elif self.is_import: msg = u'Importing \'{}\' -> \'{}\'' self.project.logger.info(msg.format(self.deps[0].path, self.outs[0].path)) - self.deps[0].download(self.outs[0].path_info) + if not dry: + self.deps[0].download(self.outs[0].path_info) elif self.is_data_source: msg = u'Verifying data sources in \'{}\''.format(self.relpath) self.project.logger.info(msg) - self.check_missing_outputs() + if not dry: + self.check_missing_outputs() else: msg = u'Running command:\n\t{}'.format(self.cmd) self.project.logger.info(msg) - p = subprocess.Popen(self.cmd, - cwd=self.cwd, - shell=True, - env=os.environ, - executable=os.getenv('SHELL')) - p.communicate() - if p.returncode != 0: - raise StageCmdFailedError(self) + if not dry: + p = subprocess.Popen(self.cmd, + cwd=self.cwd, + shell=True, + env=os.environ, + executable=os.getenv('SHELL')) + p.communicate() + if p.returncode != 0: + raise StageCmdFailedError(self) - self.save() + if not dry: + self.save() def check_missing_outputs(self): outs = [out for out in self.outs if not out.exists] diff --git a/tests/test_repro.py b/tests/test_repro.py index a3f2fc3e8..9baced2fc 100644 --- a/tests/test_repro.py +++ b/tests/test_repro.py @@ -125,6 +125,19 @@ class TestReproChangedData(TestRepro): shutil.copyfile(self.BAR, self.FOO) +class TestReproDry(TestReproChangedData): + def test(self): + self.swap_foo_with_bar() + + stages = self.dvc.reproduce(self.file1_stage, dry=True) + + self.assertTrue(len(stages), 2) + self.assertFalse(filecmp.cmp(self.file1, self.BAR)) + + ret = main(['repro', '--dry']) + self.assertFalse(filecmp.cmp(self.file1, self.BAR)) + + class TestReproChangedDeepData(TestReproChangedData): def test(self): file2 = 'file2'