diff --git a/dvc/command/experiments.py b/dvc/command/experiments.py index 5af865f10..19d96cefb 100644 --- a/dvc/command/experiments.py +++ b/dvc/command/experiments.py @@ -90,12 +90,41 @@ def _collect_names(all_experiments, **kwargs): def _collect_rows( - base_rev, experiments, metric_names, param_names, precision=None + base_rev, + experiments, + metric_names, + param_names, + precision=DEFAULT_PRECISION, + no_timestamp=False, ): - from flatten_json import flatten + for i, (rev, exp) in enumerate(experiments.items()): + row = [] + style = None + queued = "*" if exp.get("queued", False) else "" + if no_timestamp: + timestamp = "" + else: + commit_time = exp.get("timestamp") + timestamp = f" ({commit_time})" if commit_time is not None else "" - if precision is None: - precision = DEFAULT_PRECISION + if rev == "baseline": + row.append(f"{base_rev}{timestamp}") + style = "bold" + elif i < len(experiments) - 1: + row.append(f"├── {queued}{rev[:7]}{timestamp}") + else: + row.append(f"└── {queued}{rev[:7]}{timestamp}") + + _extend_row( + row, metric_names, exp.get("metrics", {}).items(), precision + ) + _extend_row(row, param_names, exp.get("params", {}).items(), precision) + + yield row, style + + +def _extend_row(row, names, items, precision): + from flatten_json import flatten def _round(val): if isinstance(val, float): @@ -103,40 +132,22 @@ def _collect_rows( return val - def _extend(row, names, items): - if not items: - row.extend(["-"] * len(names)) - return + if not items: + row.extend(["-"] * len(names)) + return - for fname, item in items: - if isinstance(item, dict): - item = flatten(item, ".") - else: - item = {fname: item} - for name in names: - if name in item: - value = item[name] - text = str(_round(value)) if value is not None else "-" - row.append(text) - else: - row.append("-") - - for i, (rev, exp) in enumerate(experiments.items()): - row = [] - style = None - queued = "*" if exp.get("queued", False) else "" - if rev == "baseline": - row.append(f"{base_rev}") - style = "bold" - elif i < len(experiments) - 1: - row.append(f"├── {queued}{rev[:7]}") + for fname, item in items: + if isinstance(item, dict): + item = flatten(item, ".") else: - row.append(f"└── {queued}{rev[:7]}") - - _extend(row, metric_names, exp.get("metrics", {}).items()) - _extend(row, param_names, exp.get("params", {}).items()) - - yield row, style + item = {fname: item} + for name in names: + if name in item: + value = item[name] + text = str(_round(value)) if value is not None else "-" + row.append(text) + else: + row.append("-") def _parse_list(param_list): @@ -150,15 +161,15 @@ def _parse_list(param_list): return ret -def _show_experiments(all_experiments, console, precision=None, **kwargs): +def _show_experiments(all_experiments, console, **kwargs): from rich.table import Table from dvc.scm.git import Git - include_metrics = _parse_list(kwargs.get("include_metrics", [])) - exclude_metrics = _parse_list(kwargs.get("exclude_metrics", [])) - include_params = _parse_list(kwargs.get("include_params", [])) - exclude_params = _parse_list(kwargs.get("exclude_params", [])) + include_metrics = _parse_list(kwargs.pop("include_metrics", [])) + exclude_metrics = _parse_list(kwargs.pop("exclude_metrics", [])) + include_params = _parse_list(kwargs.pop("include_params", [])) + exclude_params = _parse_list(kwargs.pop("exclude_params", [])) metric_names, param_names = _collect_names( all_experiments, @@ -180,11 +191,7 @@ def _show_experiments(all_experiments, console, precision=None, **kwargs): base_rev = base_rev[:7] for row, _, in _collect_rows( - base_rev, - experiments, - metric_names, - param_names, - precision=precision, + base_rev, experiments, metric_names, param_names, **kwargs, ): table.add_row(*row) @@ -223,6 +230,7 @@ class CmdExperimentsShow(CmdBase): exclude_metrics=self.args.exclude_metrics, include_params=self.args.include_params, exclude_params=self.args.exclude_params, + no_timestamp=self.args.no_timestamp, ) if not self.args.no_pager: @@ -400,6 +408,12 @@ def add_parser(subparsers, parent_parser): help="Exclude the specified params from output table.", metavar="", ) + experiments_show_parser.add_argument( + "--no-timestamp", + action="store_true", + default=False, + help="Do not show experiment timestamps.", + ) experiments_show_parser.set_defaults(func=CmdExperimentsShow) EXPERIMENTS_CHECKOUT_HELP = "Checkout experiments." diff --git a/dvc/repo/experiments/show.py b/dvc/repo/experiments/show.py index da84079e7..a52435f49 100644 --- a/dvc/repo/experiments/show.py +++ b/dvc/repo/experiments/show.py @@ -1,6 +1,7 @@ import logging import re from collections import OrderedDict, defaultdict +from datetime import datetime from dvc.repo import locked from dvc.repo.metrics.show import _collect_metrics, _read_metrics @@ -15,6 +16,12 @@ EXP_RE = re.compile(r"(?P[a-f0-9]{7})-(?P[a-f0-9]+)") def _collect_experiment(repo, branch, stash=False): res = defaultdict(dict) for rev in repo.brancher(revs=[branch]): + if rev == "workspace": + res["timestamp"] = None + else: + commit = repo.scm.repo.rev_parse(rev) + res["timestamp"] = datetime.fromtimestamp(commit.committed_date) + configs = _collect_configs(repo) params = _read_params(repo, configs, rev) if params: diff --git a/tests/func/experiments/test_show.py b/tests/func/experiments/test_show.py index 9571a8adb..98defd55f 100644 --- a/tests/func/experiments/test_show.py +++ b/tests/func/experiments/test_show.py @@ -1,3 +1,6 @@ +from datetime import datetime + +from dvc.dvcfile import PIPELINE_FILE from tests.func.test_repro_multistage import COPY_SCRIPT @@ -16,5 +19,45 @@ def test_show_simple(tmp_dir, scm, dvc): "metrics": {"metrics.yaml": {"foo": 1}}, "params": {"params.yaml": {"foo": 1}}, "queued": False, + "timestamp": None, } } + + +def test_show_experiment(tmp_dir, scm, dvc): + tmp_dir.gen("copy.py", COPY_SCRIPT) + tmp_dir.gen("params.yaml", "foo: 1") + dvc.run( + cmd="python copy.py params.yaml metrics.yaml", + metrics_no_cache=["metrics.yaml"], + params=["foo"], + name="foo", + ) + scm.add(["copy.py", "params.yaml", "metrics.yaml", "dvc.yaml", "dvc.lock"]) + scm.commit("baseline") + baseline_rev = scm.get_rev() + timestamp = datetime.fromtimestamp( + scm.repo.rev_parse(baseline_rev).committed_date + ) + + dvc.reproduce(PIPELINE_FILE, experiment=True, params=["foo=2"]) + results = dvc.experiments.show() + + expected_baseline = { + "metrics": {"metrics.yaml": {"foo": 1}}, + "params": {"params.yaml": {"foo": 1}}, + "queued": False, + "timestamp": timestamp, + } + expected_params = {"foo": 2} + + assert set(results.keys()) == {"workspace", baseline_rev} + experiments = results[baseline_rev] + assert len(experiments) == 2 + for rev, exp in experiments.items(): + if rev == "baseline": + assert exp == expected_baseline + else: + assert exp["metrics"]["metrics.yaml"] == expected_params + assert exp["params"]["params.yaml"] == expected_params + assert exp["timestamp"] > timestamp