diff --git a/dvc/commands/experiments/show.py b/dvc/commands/experiments/show.py index e837c7745..0e8080102 100644 --- a/dvc/commands/experiments/show.py +++ b/dvc/commands/experiments/show.py @@ -65,6 +65,9 @@ experiment_types = { def _collect_rows( base_rev, experiments, + all_headers, + metric_headers, + param_headers, metric_names, param_names, deps_names, @@ -87,14 +90,18 @@ def _collect_rows( new_checkpoint = True for i, (rev, results) in enumerate(experiments.items()): + fill_value = FILL_VALUE_ERRORED if results.get("error") else fill_value + row_dict = {k: fill_value for k in all_headers} + exp = results.get("data", {}) + if exp.get("running"): state = "Running" elif exp.get("queued"): state = "Queued" else: state = fill_value - executor = exp.get("executor", fill_value) + is_baseline = rev == "baseline" if is_baseline: @@ -102,9 +109,7 @@ def _collect_rows( else: name_rev = rev[:7] - exp_name = exp.get("name", "") tip = exp.get("checkpoint_tip") - parent_rev = exp.get("checkpoint_parent", "") parent_exp = experiments.get(parent_rev, {}).get("data", {}) parent_tip = parent_exp.get("checkpoint_tip") @@ -130,26 +135,28 @@ def _collect_rows( if not is_baseline: new_checkpoint = not (tip and tip == parent_tip) - row = [ - exp_name, - name_rev, - typ, - _format_time(exp.get("timestamp"), fill_value, iso), - parent, - state, - executor, - ] - fill_value = FILL_VALUE_ERRORED if results.get("error") else fill_value + row_dict["Experiment"] = exp.get("name", "") + row_dict["rev"] = name_rev + row_dict["typ"] = typ + row_dict["Created"] = _format_time( + exp.get("timestamp"), fill_value, iso + ) + row_dict["parent"] = parent + row_dict["State"] = state + row_dict["Executor"] = exp.get("executor", fill_value) + _extend_row( - row, + row_dict, metric_names, + metric_headers, exp.get("metrics", {}).items(), precision, fill_value=fill_value, ) _extend_row( - row, + row_dict, param_names, + param_headers, exp.get("params", {}).items(), precision, fill_value=fill_value, @@ -158,8 +165,8 @@ def _collect_rows( hash_info = exp.get("deps", {}).get(dep, {}).get("hash") if hash_info is not None: hash_info = hash_info[:7] - row.append(hash_info or fill_value) - yield row + row_dict[dep] = hash_info + yield list(row_dict.values()) def _sort_column(sort_by, metric_names, param_names): @@ -225,13 +232,9 @@ def _format_time(datetime_obj, fill_value=FILL_VALUE, iso=False): return datetime_obj.strftime(fmt) -def _extend_row(row, names, items, precision, fill_value=FILL_VALUE): +def _extend_row(row, names, headers, items, precision, fill_value=FILL_VALUE): from dvc.compare import _format_field, with_value - if not items: - row.extend(fill_value for keys in names.values() for _ in keys) - return - for fname, data in items: item = data.get("data", {}) item = flatten(item) if isinstance(item, dict) else {fname: item} @@ -243,7 +246,11 @@ def _extend_row(row, names, items, precision, fill_value=FILL_VALUE): # wrap field data in ui.rich_text, otherwise rich may # interpret unescaped braces from list/dict types as rich # markup tags - row.append(ui.rich_text(str(_format_field(value, precision)))) + value = ui.rich_text(str(_format_field(value, precision))) + if name in headers: + row[name] = value + else: + row[f"{fname}:{name}"] = value def experiments_table( @@ -264,14 +271,15 @@ def experiments_table( from dvc.compare import TabularData - td = TabularData( - lconcat(headers, metric_headers, param_headers, deps_names), - fill_value=fill_value, - ) + all_headers = lconcat(headers, metric_headers, param_headers, deps_names) + td = TabularData(all_headers, fill_value=fill_value) for base_rev, experiments in all_experiments.items(): rows = _collect_rows( base_rev, experiments, + all_headers, + metric_headers, + param_headers, metric_names, param_names, deps_names, diff --git a/tests/func/experiments/test_show.py b/tests/func/experiments/test_show.py index 7102ddc98..ddeb795ec 100644 --- a/tests/func/experiments/test_show.py +++ b/tests/func/experiments/test_show.py @@ -640,16 +640,7 @@ def test_show_outs(tmp_dir, dvc, scm): deps=["copy.py"], outs=["out"], ) - scm.add( - [ - "dvc.yaml", - "dvc.lock", - "copy.py", - "params.yaml", - "metrics.yaml", - ".gitignore", - ] - ) + scm.commit("init") outs = dvc.experiments.show()["workspace"]["baseline"]["data"]["outs"] @@ -662,6 +653,76 @@ def test_show_outs(tmp_dir, dvc, scm): } +def test_metrics_renaming(tmp_dir, dvc, scm, capsys): + tmp_dir.gen("copy.py", COPY_SCRIPT) + params_file = tmp_dir / "params.yaml" + params_data = { + "foo": 1, + } + (tmp_dir / params_file).dump(params_data) + + dvc.run( + cmd="python copy.py params.yaml metrics.yaml", + metrics_no_cache=["metrics.yaml"], + params=["foo"], + name="copy-file", + deps=["copy.py"], + ) + scm.add( + [ + "dvc.yaml", + "dvc.lock", + "copy.py", + "params.yaml", + "metrics.yaml", + ".gitignore", + ] + ) + + scm.commit("metrics.yaml") + metrics_rev = scm.get_rev() + + dvc.run( + cmd="python copy.py params.yaml scores.yaml", + metrics_no_cache=["scores.yaml"], + params=["foo"], + name="copy-file", + deps=["copy.py"], + ) + scm.add( + [ + "dvc.yaml", + "dvc.lock", + "params.yaml", + "scores.yaml", + ] + ) + scm.commit("scores.yaml") + scores_rev = scm.get_rev() + + capsys.readouterr() + assert main(["exp", "show", "--csv", "-A"]) == 0 + cap = capsys.readouterr() + + def _get_rev_isotimestamp(rev): + return datetime.fromtimestamp( + scm.gitpython.repo.rev_parse(rev).committed_date + ).isoformat() + + assert ( + "master,{},baseline,{},,1,,1".format( + scores_rev[:7], _get_rev_isotimestamp(scores_rev) + ) + in cap.out + ) + assert ( + ",{},baseline,{},,,1,1".format( + metrics_rev[:7], _get_rev_isotimestamp(metrics_rev) + ) + in cap.out + ) + + def test_show_sorted_deps(tmp_dir, dvc, scm, capsys): tmp_dir.gen("a", "a") tmp_dir.gen("b", "b")