Skip to content

Commit d89555c

Browse files
committed
exp show: handle renaming of metrics/params.
_extend_row did not considered partially empty `items`, which can occur when metris or params are renamed between commits. Fixes #7070
1 parent b6fa117 commit d89555c

File tree

2 files changed

+105
-36
lines changed

2 files changed

+105
-36
lines changed

dvc/commands/experiments/show.py

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ def _collect_names(all_experiments, **kwargs):
6565
def _collect_rows(
6666
base_rev,
6767
experiments,
68+
all_headers,
69+
metric_headers,
70+
param_headers,
6871
metric_names,
6972
param_names,
7073
deps_names,
@@ -87,24 +90,26 @@ def _collect_rows(
8790

8891
new_checkpoint = True
8992
for i, (rev, results) in enumerate(experiments.items()):
93+
fill_value = FILL_VALUE_ERRORED if results.get("error") else fill_value
94+
row_dict = {k: fill_value for k in all_headers}
95+
9096
exp = results.get("data", {})
97+
9198
if exp.get("running"):
9299
state = "Running"
93100
elif exp.get("queued"):
94101
state = "Queued"
95102
else:
96103
state = fill_value
97-
executor = exp.get("executor", fill_value)
104+
98105
is_baseline = rev == "baseline"
99106

100107
if is_baseline:
101108
name_rev = base_rev[:7] if Git.is_sha(base_rev) else base_rev
102109
else:
103110
name_rev = rev[:7]
104111

105-
exp_name = exp.get("name", "")
106112
tip = exp.get("checkpoint_tip")
107-
108113
parent_rev = exp.get("checkpoint_parent", "")
109114
parent_exp = experiments.get(parent_rev, {}).get("data", {})
110115
parent_tip = parent_exp.get("checkpoint_tip")
@@ -130,26 +135,28 @@ def _collect_rows(
130135
if not is_baseline:
131136
new_checkpoint = not (tip and tip == parent_tip)
132137

133-
row = [
134-
exp_name,
135-
name_rev,
136-
typ,
137-
_format_time(exp.get("timestamp"), fill_value, iso),
138-
parent,
139-
state,
140-
executor,
141-
]
142-
fill_value = FILL_VALUE_ERRORED if results.get("error") else fill_value
138+
row_dict["Experiment"] = exp.get("name", "")
139+
row_dict["rev"] = name_rev
140+
row_dict["typ"] = typ
141+
row_dict["Created"] = _format_time(
142+
exp.get("timestamp"), fill_value, iso
143+
)
144+
row_dict["parent"] = parent
145+
row_dict["State"] = state
146+
row_dict["Executor"] = exp.get("executor", fill_value)
147+
143148
_extend_row(
144-
row,
149+
row_dict,
145150
metric_names,
151+
metric_headers,
146152
exp.get("metrics", {}).items(),
147153
precision,
148154
fill_value=fill_value,
149155
)
150156
_extend_row(
151-
row,
157+
row_dict,
152158
param_names,
159+
param_headers,
153160
exp.get("params", {}).items(),
154161
precision,
155162
fill_value=fill_value,
@@ -158,8 +165,8 @@ def _collect_rows(
158165
hash_info = exp.get("deps", {}).get(dep, {}).get("hash")
159166
if hash_info is not None:
160167
hash_info = hash_info[:7]
161-
row.append(hash_info or fill_value)
162-
yield row
168+
row_dict[dep] = hash_info
169+
yield list(row_dict.values())
163170

164171

165172
def _sort_column(sort_by, metric_names, param_names):
@@ -225,13 +232,9 @@ def _format_time(datetime_obj, fill_value=FILL_VALUE, iso=False):
225232
return datetime_obj.strftime(fmt)
226233

227234

228-
def _extend_row(row, names, items, precision, fill_value=FILL_VALUE):
235+
def _extend_row(row, names, headers, items, precision, fill_value=FILL_VALUE):
229236
from dvc.compare import _format_field, with_value
230237

231-
if not items:
232-
row.extend(fill_value for keys in names.values() for _ in keys)
233-
return
234-
235238
for fname, data in items:
236239
item = data.get("data", {})
237240
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):
243246
# wrap field data in ui.rich_text, otherwise rich may
244247
# interpret unescaped braces from list/dict types as rich
245248
# markup tags
246-
row.append(ui.rich_text(str(_format_field(value, precision))))
249+
value = ui.rich_text(str(_format_field(value, precision)))
250+
if name in headers:
251+
row[name] = value
252+
else:
253+
row[f"{fname}:{name}"] = value
247254

248255

249256
def experiments_table(
@@ -264,14 +271,15 @@ def experiments_table(
264271

265272
from dvc.compare import TabularData
266273

267-
td = TabularData(
268-
lconcat(headers, metric_headers, param_headers, deps_names),
269-
fill_value=fill_value,
270-
)
274+
all_headers = lconcat(headers, metric_headers, param_headers, deps_names)
275+
td = TabularData(all_headers, fill_value=fill_value)
271276
for base_rev, experiments in all_experiments.items():
272277
rows = _collect_rows(
273278
base_rev,
274279
experiments,
280+
all_headers,
281+
metric_headers,
282+
param_headers,
275283
metric_names,
276284
param_names,
277285
deps_names,

tests/func/experiments/test_show.py

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,34 @@ def test_show_outs(tmp_dir, dvc, scm):
640640
deps=["copy.py"],
641641
outs=["out"],
642642
)
643+
644+
scm.commit("init")
645+
646+
outs = dvc.experiments.show()["workspace"]["baseline"]["data"]["outs"]
647+
assert outs == {
648+
"out": {
649+
"hash": ANY,
650+
"size": ANY,
651+
"nfiles": None,
652+
}
653+
}
654+
655+
656+
def test_metrics_renaming(tmp_dir, dvc, scm, capsys):
657+
tmp_dir.gen("copy.py", COPY_SCRIPT)
658+
params_file = tmp_dir / "params.yaml"
659+
params_data = {
660+
"foo": 1,
661+
}
662+
(tmp_dir / params_file).dump(params_data)
663+
664+
dvc.run(
665+
cmd="python copy.py params.yaml metrics.yaml",
666+
metrics_no_cache=["metrics.yaml"],
667+
params=["foo"],
668+
name="copy-file",
669+
deps=["copy.py"],
670+
)
643671
scm.add(
644672
[
645673
"dvc.yaml",
@@ -650,16 +678,49 @@ def test_show_outs(tmp_dir, dvc, scm):
650678
".gitignore",
651679
]
652680
)
653-
scm.commit("init")
654681

655-
outs = dvc.experiments.show()["workspace"]["baseline"]["data"]["outs"]
656-
assert outs == {
657-
"out": {
658-
"hash": ANY,
659-
"size": ANY,
660-
"nfiles": None,
661-
}
662-
}
682+
scm.commit("metrics.yaml")
683+
metrics_rev = scm.get_rev()
684+
685+
dvc.run(
686+
cmd="python copy.py params.yaml scores.yaml",
687+
metrics_no_cache=["scores.yaml"],
688+
params=["foo"],
689+
name="copy-file",
690+
deps=["copy.py"],
691+
)
692+
scm.add(
693+
[
694+
"dvc.yaml",
695+
"dvc.lock",
696+
"params.yaml",
697+
"scores.yaml",
698+
]
699+
)
700+
scm.commit("scores.yaml")
701+
scores_rev = scm.get_rev()
702+
703+
capsys.readouterr()
704+
assert main(["exp", "show", "--csv", "-A"]) == 0
705+
cap = capsys.readouterr()
706+
707+
def _get_rev_isotimestamp(rev):
708+
return datetime.fromtimestamp(
709+
scm.gitpython.repo.rev_parse(rev).committed_date
710+
).isoformat()
711+
712+
assert (
713+
"master,{},baseline,{},,1,,1".format(
714+
scores_rev[:7], _get_rev_isotimestamp(scores_rev)
715+
)
716+
in cap.out
717+
)
718+
assert (
719+
",{},baseline,{},,,1,1".format(
720+
metrics_rev[:7], _get_rev_isotimestamp(metrics_rev)
721+
)
722+
in cap.out
723+
)
663724

664725

665726
def test_show_sorted_deps(tmp_dir, dvc, scm, capsys):

0 commit comments

Comments
 (0)