diff --git a/dvc/exceptions.py b/dvc/exceptions.py index 6d52b33bf..0232c612c 100644 --- a/dvc/exceptions.py +++ b/dvc/exceptions.py @@ -14,6 +14,17 @@ class DvcException(Exception): class UnsupportedRemoteError(DvcException): - def __init__(self, config, cause=None): + def __init__(self, config): msg = "Remote '{}' is not supported.".format(config) - super(UnsupportedRemoteError, self).__init__(msg, cause=cause) + super(UnsupportedRemoteError, self).__init__(msg) + + +class OutputDuplicationError(DvcException): + def __init__(self, output, stages): + assert isinstance(output, str) + assert isinstance(stages, list) + assert all(isinstance(stage, str) for stage in stages) + msg = "File '{}' is specified as an output in more than one stage: {}" + super(OutputDuplicationError, self).__init__(msg.format(output, + str(stages))) + diff --git a/dvc/project.py b/dvc/project.py index e9ccd2169..0819fcfcf 100644 --- a/dvc/project.py +++ b/dvc/project.py @@ -455,6 +455,21 @@ class Project(object): return ret + + def _find_output_by_path(self, path, outs=None): + from dvc.exceptions import OutputDuplicationError + + if not outs: + outs = [out for stage in self.active_stages() for out in stage.outs] + + abs_path = os.path.abspath(path) + matched = [out for out in outs if out.path == abs_path] + stages = [out.stage.path for out in matched] + if len(stages) > 1: + raise OutputDuplicationError(path, stages) + + return matched[0] if matched else None + def metrics_show(self, path=None, json_path=None, tsv_path=None, htsv_path=None, @@ -464,9 +479,20 @@ class Project(object): res = {} for branch in self.scm.brancher(all_branches=all_branches): outs = [out for stage in self.active_stages() for out in stage.outs] - metrics = filter(lambda o: o.metric, outs) - fnames = [path] if path else map(lambda o: o.path, metrics) + + if path: + out = self._find_output_by_path(path, outs=outs) + stage = out.stage.path if out else None + fnames = [path] + else: + metrics = filter(lambda o: o.metric, outs) + stage = None + fnames = map(lambda o: o.path, metrics) + for fname in fnames: + if stage: + self.checkout(stage) + rel = os.path.relpath(fname) metric = self._read_metric(fname, json_path=json_path, @@ -491,7 +517,6 @@ class Project(object): if res: return res - if path: msg = 'File \'{}\' does not exist'.format(path) else: @@ -500,32 +525,25 @@ class Project(object): raise DvcException(msg) def _metrics_modify(self, path, val): - apath = os.path.abspath(path) found = False - for stage in self.stages(): - for out in stage.outs: - if apath != out.path: - continue - - found = True - - if out.path_info['scheme'] != 'local': - msg = 'Output \'{}\' scheme \'{}\' is not supported for metrics' - raise DvcException(msg.format(out.path, out.path_info['scheme'])) - - if out.use_cache: - msg = 'Cached output \'{}\' is not supported for metrics' - raise DvcException(msg.format(out.rel_path)) - - out.metric = val - out._verify_metric() - - stage.dump() - - if not found: + out = self._find_output_by_path(path) + if not out: msg = 'Unable to find file \'{}\' in the pipeline'.format(path) raise DvcException(msg) + if out.path_info['scheme'] != 'local': + msg = 'Output \'{}\' scheme \'{}\' is not supported for metrics' + raise DvcException(msg.format(out.path, out.path_info['scheme'])) + + if out.use_cache: + msg = 'Cached output \'{}\' is not supported for metrics' + raise DvcException(msg.format(out.rel_path)) + + out.metric = val + out._verify_metric() + + out.stage.dump() + def metrics_add(self, path): self._metrics_modify(path, True) diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 46064ff25..059b68c6f 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -1,6 +1,7 @@ import os import json +from dvc.project import Project from dvc.main import main from dvc.exceptions import DvcException from tests.basic_env import TestDvc @@ -159,3 +160,38 @@ class TestNoMetrics(TestDvc): def test(self): ret = main(['metrics', 'show']) self.assertNotEqual(ret, 0) + + +class TestCachedMetrics(TestDvc): + def _do_write(self, branch): + self.dvc.scm.checkout(branch) + self.dvc.checkout() + + with open('metrics.json', 'w+') as fd: + json.dump({'metrics': branch}, fd) + + stage = self.dvc.add('metrics.json') + self.assertNotEqual(stage, None) + + self.dvc.scm.add(['.gitignore', 'metrics.json.dvc']) + self.dvc.scm.commit(branch) + + def test(self): + self.dvc.scm.commit('init') + + self.dvc.scm.branch('one') + self.dvc.scm.branch('two') + + self._do_write('master') + self._do_write('one') + self._do_write('two') + + self.dvc = Project('.') + + res = self.dvc.metrics_show('metrics.json', + all_branches=True, + json_path='metrics') + + self.assertEqual(res, {"master": {"metrics.json": ["master"]}, + "one": {"metrics.json": ["one"]}, + "two": {"metrics.json": ["two"]}})