diff --git a/dvc/command/metrics.py b/dvc/command/metrics.py index 39c53f5b8..748b3bff7 100644 --- a/dvc/command/metrics.py +++ b/dvc/command/metrics.py @@ -35,13 +35,11 @@ def show_metrics(metrics, all_branches=False, all_tags=False): class CmdMetricsShow(CmdBase): def run(self): - typ = self.args.type - xpath = self.args.xpath try: metrics = self.repo.metrics.show( - self.args.path, - typ=typ, - xpath=xpath, + self.args.targets, + typ=self.args.type, + xpath=self.args.xpath, all_branches=self.args.all_branches, all_tags=self.args.all_tags, recursive=self.args.recursive, @@ -121,7 +119,10 @@ def add_parser(subparsers, parent_parser): formatter_class=argparse.RawDescriptionHelpFormatter, ) metrics_show_parser.add_argument( - "path", nargs="?", help="Path to a metric file or a directory." + "targets", + nargs="*", + help="Metric files or directories (see -R) to show " + "(leave empty to display all)", ) metrics_show_parser.add_argument( "-t", @@ -155,8 +156,8 @@ def add_parser(subparsers, parent_parser): action="store_true", default=False, help=( - "If path is a directory, recursively search and process metric " - "files in path." + "If any target is a directory, recursively search and process " + "metric files." ), ) metrics_show_parser.set_defaults(func=CmdMetricsShow) diff --git a/dvc/exceptions.py b/dvc/exceptions.py index da96fd0f9..508b1fb05 100644 --- a/dvc/exceptions.py +++ b/dvc/exceptions.py @@ -196,10 +196,11 @@ class ReproductionError(DvcException): class BadMetricError(DvcException): - def __init__(self, path): + def __init__(self, paths): super(BadMetricError, self).__init__( - "'{}' does not exist, not a metric or is malformed".format( - relpath(path) + "the following outputs do not exist, are not a metric file " + "or are malformed: {paths}".format( + paths=", ".join(relpath(path) for path in paths) ) ) diff --git a/dvc/repo/metrics/show.py b/dvc/repo/metrics/show.py index 42ceab3d0..e7f61024d 100644 --- a/dvc/repo/metrics/show.py +++ b/dvc/repo/metrics/show.py @@ -152,15 +152,16 @@ def _read_metric(fd, typ=None, xpath=None, fname=None, branch=None): return None -def _collect_metrics(repo, path, recursive, typ, xpath, branch): +def _collect_metrics(repo, targets, recursive, typ, xpath, branch): """Gather all the metric outputs. Args: - path (str): Path to a metric file or a directory. + targets (list): List of paths to metric files or directories. + If there's no target, collect all metrics. recursive (bool): If path is a directory, do a recursive search for metrics on the given path. - typ (str): The type of metric to search for, could be one of the - following (raw|json|tsv|htsv|csv|hcsv). + typ (str): The type that will be used to interpret the metric file, + one of the followings - (raw|json|tsv|htsv|csv|hcsv). xpath (str): Path to search for. branch (str): Branch to look up for metrics. @@ -171,20 +172,21 @@ def _collect_metrics(repo, path, recursive, typ, xpath, branch): - xpath: """ outs = [out for stage in repo.stages() for out in stage.outs] + found = [] - if path: - try: - outs = repo.find_outs_by_path(path, outs=outs, recursive=recursive) - except OutputNotFoundError: - logger.debug( - "DVC-file not for found for '{}' in branch '{}'".format( - path, branch + if targets: + for target in targets: + try: + found += repo.find_outs_by_path( + target, outs=outs, recursive=recursive ) - ) - return [] + except OutputNotFoundError: + pass + else: + found = outs res = [] - for o in outs: + for o in found: if not o.metric: continue @@ -259,7 +261,7 @@ def _read_metrics(repo, metrics, branch): def show( repo, - path=None, + targets=None, typ=None, xpath=None, all_branches=False, @@ -269,14 +271,16 @@ def show( res = {} for branch in repo.brancher(all_branches=all_branches, all_tags=all_tags): - entries = _collect_metrics(repo, path, recursive, typ, xpath, branch) + entries = _collect_metrics( + repo, targets, recursive, typ, xpath, branch + ) metrics = _read_metrics(repo, entries, branch) if metrics: res[branch] = metrics if not res: - if path: - raise BadMetricError(path) + if targets: + raise BadMetricError(targets) raise NoMetricsError() return res diff --git a/tests/func/test_metrics.py b/tests/func/test_metrics.py index e7892f506..a452dabe9 100644 --- a/tests/func/test_metrics.py +++ b/tests/func/test_metrics.py @@ -85,14 +85,14 @@ class TestMetricsBase(TestDvcGit): class TestMetrics(TestMetricsBase): def test_show(self): - ret = self.dvc.metrics.show("metric", all_branches=True) + ret = self.dvc.metrics.show(["metric"], all_branches=True) self.assertEqual(len(ret), 3) self.assertEqual(ret["foo"]["metric"], "foo") self.assertEqual(ret["bar"]["metric"], "bar") self.assertEqual(ret["baz"]["metric"], "baz") ret = self.dvc.metrics.show( - "metric_json", typ="json", xpath="branch", all_branches=True + ["metric_json"], typ="json", xpath="branch", all_branches=True ) self.assertEqual(len(ret), 3) self.assertSequenceEqual(ret["foo"]["metric_json"], ["foo"]) @@ -100,7 +100,7 @@ class TestMetrics(TestMetricsBase): self.assertSequenceEqual(ret["baz"]["metric_json"], ["baz"]) ret = self.dvc.metrics.show( - "metric_tsv", typ="tsv", xpath="0,0", all_branches=True + ["metric_tsv"], typ="tsv", xpath="0,0", all_branches=True ) self.assertEqual(len(ret), 3) self.assertSequenceEqual(ret["foo"]["metric_tsv"], ["foo"]) @@ -108,7 +108,7 @@ class TestMetrics(TestMetricsBase): self.assertSequenceEqual(ret["baz"]["metric_tsv"], ["baz"]) ret = self.dvc.metrics.show( - "metric_htsv", typ="htsv", xpath="0,branch", all_branches=True + ["metric_htsv"], typ="htsv", xpath="0,branch", all_branches=True ) self.assertEqual(len(ret), 3) self.assertSequenceEqual(ret["foo"]["metric_htsv"], ["foo"]) @@ -116,7 +116,7 @@ class TestMetrics(TestMetricsBase): self.assertSequenceEqual(ret["baz"]["metric_htsv"], ["baz"]) ret = self.dvc.metrics.show( - "metric_csv", typ="csv", xpath="0,0", all_branches=True + ["metric_csv"], typ="csv", xpath="0,0", all_branches=True ) self.assertEqual(len(ret), 3) self.assertSequenceEqual(ret["foo"]["metric_csv"], ["foo"]) @@ -124,7 +124,7 @@ class TestMetrics(TestMetricsBase): self.assertSequenceEqual(ret["baz"]["metric_csv"], ["baz"]) ret = self.dvc.metrics.show( - "metric_hcsv", typ="hcsv", xpath="0,branch", all_branches=True + ["metric_hcsv"], typ="hcsv", xpath="0,branch", all_branches=True ) self.assertEqual(len(ret), 3) self.assertSequenceEqual(ret["foo"]["metric_hcsv"], ["foo"]) @@ -132,7 +132,7 @@ class TestMetrics(TestMetricsBase): self.assertSequenceEqual(ret["baz"]["metric_hcsv"], ["baz"]) ret = self.dvc.metrics.show( - "metric_json_ext", + ["metric_json_ext"], typ="json", xpath="$.metrics[?(@.deviation_mse<0.30) & (@.value_mse>0.4)]", all_branches=True, @@ -153,7 +153,7 @@ class TestMetrics(TestMetricsBase): def test_unknown_type_ignored(self): ret = self.dvc.metrics.show( - "metric_hcsv", typ="unknown", xpath="0,branch", all_branches=True + ["metric_hcsv"], typ="unknown", xpath="0,branch", all_branches=True ) self.assertEqual(len(ret), 3) for b in ["foo", "bar", "baz"]: @@ -161,7 +161,7 @@ class TestMetrics(TestMetricsBase): def test_type_case_normalized(self): ret = self.dvc.metrics.show( - "metric_hcsv", typ=" hCSV ", xpath="0,branch", all_branches=True + ["metric_hcsv"], typ=" hCSV ", xpath="0,branch", all_branches=True ) self.assertEqual(len(ret), 3) for b in ["foo", "bar", "baz"]: @@ -169,7 +169,7 @@ class TestMetrics(TestMetricsBase): def test_xpath_is_empty(self): ret = self.dvc.metrics.show( - "metric_json", typ="json", xpath="", all_branches=True + ["metric_json"], typ="json", xpath="", all_branches=True ) self.assertEqual(len(ret), 3) for b in ["foo", "bar", "baz"]: @@ -177,7 +177,7 @@ class TestMetrics(TestMetricsBase): def test_xpath_is_none(self): ret = self.dvc.metrics.show( - "metric_json", typ="json", xpath=None, all_branches=True + ["metric_json"], typ="json", xpath=None, all_branches=True ) self.assertEqual(len(ret), 3) for b in ["foo", "bar", "baz"]: @@ -185,7 +185,7 @@ class TestMetrics(TestMetricsBase): def test_xpath_all_columns(self): ret = self.dvc.metrics.show( - "metric_hcsv", typ="hcsv ", xpath="0,", all_branches=True + ["metric_hcsv"], typ="hcsv ", xpath="0,", all_branches=True ) self.assertEqual(len(ret), 3) for b in ["foo", "bar", "baz"]: @@ -193,7 +193,7 @@ class TestMetrics(TestMetricsBase): def test_xpath_all_rows(self): ret = self.dvc.metrics.show( - "metric_csv", typ="csv", xpath=",0", all_branches=True + ["metric_csv"], typ="csv", xpath=",0", all_branches=True ) self.assertEqual(len(ret), 3) for b in ["foo", "bar", "baz"]: @@ -201,7 +201,7 @@ class TestMetrics(TestMetricsBase): def test_xpath_all(self): ret = self.dvc.metrics.show( - "metric_csv", typ="csv", xpath=",", all_branches=True + ["metric_csv"], typ="csv", xpath=",", all_branches=True ) self.assertEqual(len(ret), 3) for b in ["foo", "bar", "baz"]: @@ -209,7 +209,7 @@ class TestMetrics(TestMetricsBase): def test_xpath_all_with_header(self): ret = self.dvc.metrics.show( - "metric_hcsv", typ="hcsv", xpath=",", all_branches=True + ["metric_hcsv"], typ="hcsv", xpath=",", all_branches=True ) self.assertEqual(len(ret), 3) for b in ["foo", "bar", "baz"]: @@ -413,10 +413,12 @@ class TestMetricsRecursive(TestDvcGit): def test(self): with self.assertRaises(BadMetricError): - self.dvc.metrics.show("nested", all_branches=True, recursive=False) + self.dvc.metrics.show( + ["nested"], all_branches=True, recursive=False + ) ret = self.dvc.metrics.show( - "nested", all_branches=True, recursive=True + ["nested"], all_branches=True, recursive=True ) self.assertEqual(len(ret), 1) self.assertEqual( @@ -697,7 +699,7 @@ class TestCachedMetrics(TestDvcGit): self.dvc = DvcRepo(".") res = self.dvc.metrics.show( - "metrics.json", all_branches=True, typ="json", xpath="metrics" + ["metrics.json"], all_branches=True, typ="json", xpath="metrics" ) self.assertEqual( @@ -711,7 +713,7 @@ class TestCachedMetrics(TestDvcGit): ) res = self.dvc.metrics.show( - "", all_branches=True, typ="json", xpath="metrics" + all_branches=True, typ="json", xpath="metrics" ) self.assertEqual( @@ -777,7 +779,9 @@ class TestMetricsType(TestDvcGit): self._do_show(file_name, xpath) def _do_show(self, file_name, xpath): - ret = self.dvc.metrics.show(file_name, xpath=xpath, all_branches=True) + ret = self.dvc.metrics.show( + [file_name], xpath=xpath, all_branches=True + ) self.assertEqual(len(ret), 3) for branch in self.branches: if isinstance(ret[branch][file_name], list): @@ -844,3 +848,19 @@ def test_show_xpath_should_override_stage_xpath(dvc_repo): result = dvc_repo.metrics.show(xpath="m1") assert result == {"": {metric_file: [0.1]}} + + +def test_show_multiple_outputs(dvc_repo, caplog): + with open("1.json", "w") as fobj: + json.dump({"AUC": 1}, fobj) + + with open("2.json", "w") as fobj: + json.dump({"AUC": 2}, fobj) + + dvc_repo.run(cmd="", overwrite=True, metrics=["1.json"]) + dvc_repo.run(cmd="", overwrite=True, metrics=["2.json"]) + + with caplog.at_level(logging.INFO, logger="dvc"): + assert 0 == main(["metrics", "show", "1.json", "2.json"]) + assert '1.json: {"AUC": 1}' in caplog.text + assert '2.json: {"AUC": 2}' in caplog.text