diff --git a/dvc/command/metrics.py b/dvc/command/metrics.py index 81dc25f18..76971cd1d 100644 --- a/dvc/command/metrics.py +++ b/dvc/command/metrics.py @@ -12,33 +12,60 @@ DEFAULT_PRECISION = 5 def _show_metrics( - metrics, all_branches=False, all_tags=False, all_commits=False + metrics, + all_branches=False, + all_tags=False, + all_commits=False, + precision=None, ): - from dvc.utils.diff import format_dict + from dvc.utils.diff import format_dict, table from dvc.utils.flatten import flatten # When `metrics` contains a `None` key, it means that some files # specified as `targets` in `repo.metrics.show` didn't contain any metrics. missing = metrics.pop(None, None) + with_rev = any([all_branches, all_tags, all_commits]) + header_set = set() + rows = [] - lines = [] - for branch, val in metrics.items(): - if all_branches or all_tags or all_commits: - lines.append(f"{branch}:") + if precision is None: + precision = DEFAULT_PRECISION - for fname, metric in val.items(): + def _round(val): + if isinstance(val, float): + return round(val, precision) + return val + + for _branch, val in metrics.items(): + for _fname, metric in val.items(): if not isinstance(metric, dict): - lines.append("\t{}: {}".format(fname, str(metric))) + header_set.add("") continue + for key, _val in flatten(format_dict(metric)).items(): + header_set.add(key) + header = sorted(header_set) + for branch, val in metrics.items(): + for fname, metric in val.items(): + row = [] + if with_rev: + row.append(branch) + row.append(fname) + if not isinstance(metric, dict): + row.append(str(metric)) + rows.append(row) + continue + flattened_val = flatten(format_dict(metric)) - lines.append(f"\t{fname}:") - for key, value in flatten(format_dict(metric)).items(): - lines.append(f"\t\t{key}: {value}") + for i in header: + row.append(_round(flattened_val.get(i))) + rows.append(row) + header.insert(0, "Path") + if with_rev: + header.insert(0, "Revision") if missing: raise BadMetricError(missing) - - return "\n".join(lines) + return table(header, rows, markdown=False) class CmdMetricsBase(CmdBase): @@ -211,6 +238,15 @@ def add_parser(subparsers, parent_parser): "metrics files." ), ) + metrics_show_parser.add_argument( + "--precision", + type=int, + help=( + "Round metrics to `n` digits precision after the decimal point. " + f"Rounds to {DEFAULT_PRECISION} digits by default." + ), + metavar="", + ) metrics_show_parser.set_defaults(func=CmdMetricsShow) METRICS_DIFF_HELP = ( diff --git a/tests/func/test_repro.py b/tests/func/test_repro.py index bd414b290..146aec770 100644 --- a/tests/func/test_repro.py +++ b/tests/func/test_repro.py @@ -982,7 +982,7 @@ class TestShouldDisplayMetricsOnReproWithMetricsOption(TestDvc): ) self.assertEqual(0, ret) - expected_metrics_display = f"{metrics_file}: {metrics_value}" + expected_metrics_display = f"Path\n{metrics_file} {metrics_value}\n" self.assertIn(expected_metrics_display, self._caplog.text) diff --git a/tests/unit/command/test_metrics.py b/tests/unit/command/test_metrics.py index aede55a30..ac06eeeff 100644 --- a/tests/unit/command/test_metrics.py +++ b/tests/unit/command/test_metrics.py @@ -226,9 +226,107 @@ def test_metrics_show_with_valid_falsey_values(): all_branches=True, ) == textwrap.dedent( """\ - branch_1: - \tmetrics.json: - \t\ta: 0 - \t\tb.ad: 0.0 - \t\tb.bc: 0.0""" + Revision Path a b.ad b.bc + branch_1 metrics.json 0 0.0 0.0""" + ) + + +def test_metrics_show_with_no_revision(): + assert _show_metrics( + {"branch_1": {"metrics.json": {"a": 0, "b": {"ad": 0.0, "bc": 0.0}}}}, + all_branches=False, + ) == textwrap.dedent( + """\ + Path a b.ad b.bc + metrics.json 0 0.0 0.0""" + ) + + +def test_metrics_show_with_non_dict_values(): + assert _show_metrics( + {"branch_1": {"metrics.json": 1}}, all_branches=True, + ) == textwrap.dedent( + """\ + Revision Path + branch_1 metrics.json 1""" + ) + + +def test_metrics_show_with_multiple_revision(): + assert _show_metrics( + { + "branch_1": {"metrics.json": {"a": 1, "b": {"ad": 1, "bc": 2}}}, + "branch_2": {"metrics.json": {"a": 1, "b": {"ad": 3, "bc": 4}}}, + }, + all_branches=True, + ) == textwrap.dedent( + """\ + Revision Path a b.ad b.bc + branch_1 metrics.json 1 1 2 + branch_2 metrics.json 1 3 4""" + ) + + +def test_metrics_show_with_one_revision_multiple_paths(): + assert _show_metrics( + { + "branch_1": { + "metrics.json": {"a": 1, "b": {"ad": 0.1, "bc": 1.03}}, + "metrics_1.json": {"a": 2.3, "b": {"ad": 6.5, "bc": 7.9}}, + } + }, + all_branches=True, + ) == textwrap.dedent( + """\ + Revision Path a b.ad b.bc + branch_1 metrics.json 1 0.1 1.03 + branch_1 metrics_1.json 2.3 6.5 7.9""" + ) + + +def test_metrics_show_with_different_metrics_header(): + assert _show_metrics( + { + "branch_1": {"metrics.json": {"b": {"ad": 1, "bc": 2}, "c": 4}}, + "branch_2": {"metrics.json": {"a": 1, "b": {"ad": 3, "bc": 4}}}, + }, + all_branches=True, + ) == textwrap.dedent( + """\ + Revision Path a b.ad b.bc c + branch_1 metrics.json — 1 2 4 + branch_2 metrics.json 1 3 4 —""" + ) + + +def test_metrics_show_precision(): + metrics = { + "branch_1": { + "metrics.json": { + "a": 1.098765366365355, + "b": {"ad": 1.5342673, "bc": 2.987725527}, + } + } + } + + assert _show_metrics(metrics, all_branches=True,) == textwrap.dedent( + """\ + Revision Path a b.ad b.bc + branch_1 metrics.json 1.09877 1.53427 2.98773""" + ) + + assert _show_metrics( + metrics, all_branches=True, precision=4 + ) == textwrap.dedent( + """\ + Revision Path a b.ad b.bc + branch_1 metrics.json 1.0988 1.5343 2.9877""" + ) + + assert _show_metrics( + metrics, all_branches=True, precision=7 + ) == textwrap.dedent( + """\ + Revision Path a b.ad b.bc + branch_1 metrics.json 1.0987654 1.5342673 2.9877255""" )