diff --git a/dvc/commands/experiments/show.py b/dvc/commands/experiments/show.py index ead3ed5426..2be3602dcc 100644 --- a/dvc/commands/experiments/show.py +++ b/dvc/commands/experiments/show.py @@ -39,14 +39,17 @@ def _update_names(names, items): def _collect_names(all_experiments, **kwargs): metric_names = defaultdict(dict) param_names = defaultdict(dict) + deps_names = set() for _, experiments in all_experiments.items(): for exp_data in experiments.values(): exp = exp_data.get("data", {}) _update_names(metric_names, exp.get("metrics", {}).items()) _update_names(param_names, exp.get("params", {}).items()) + for dep_name in exp.get("deps", {}): + deps_names.add(dep_name) - return metric_names, param_names + return metric_names, param_names, deps_names experiment_types = { @@ -64,6 +67,7 @@ def _collect_rows( experiments, metric_names, param_names, + deps_names, precision=DEFAULT_PRECISION, sort_by=None, sort_order=None, @@ -150,7 +154,11 @@ def _collect_rows( precision, fill_value=fill_value, ) - + for dep in deps_names: + 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 @@ -245,6 +253,7 @@ def experiments_table( metric_names, param_headers, param_names, + deps_names, sort_by=None, sort_order=None, precision=DEFAULT_PRECISION, @@ -256,7 +265,8 @@ def experiments_table( from dvc.compare import TabularData td = TabularData( - lconcat(headers, metric_headers, param_headers), fill_value=fill_value + lconcat(headers, metric_headers, param_headers, deps_names), + fill_value=fill_value, ) for base_rev, experiments in all_experiments.items(): rows = _collect_rows( @@ -264,6 +274,7 @@ def experiments_table( experiments, metric_names, param_names, + deps_names, sort_by=sort_by, sort_order=sort_order, precision=precision, @@ -310,7 +321,7 @@ def show_experiments( ): from funcy.seqs import flatten as flatten_list - metric_names, param_names = _collect_names(all_experiments) + metric_names, param_names, deps_names = _collect_names(all_experiments) headers = [ "Experiment", @@ -335,6 +346,7 @@ def show_experiments( metric_names, param_headers, param_names, + deps_names, kwargs.get("sort_by"), kwargs.get("sort_order"), kwargs.get("precision"), @@ -359,14 +371,22 @@ def show_experiments( ) td.drop(*merge_headers[1:]) - headers = {"metrics": metric_headers, "params": param_headers} + headers = { + "metrics": metric_headers, + "params": param_headers, + "deps": deps_names, + } styles = { "Experiment": {"no_wrap": True, "header_style": "black on grey93"}, "Created": {"header_style": "black on grey93"}, "State": {"header_style": "black on grey93"}, "Executor": {"header_style": "black on grey93"}, } - header_bg_colors = {"metrics": "cornsilk1", "params": "light_cyan1"} + header_bg_colors = { + "metrics": "cornsilk1", + "params": "light_cyan1", + "deps": "plum2", + } styles.update( { header: { diff --git a/dvc/repo/experiments/show.py b/dvc/repo/experiments/show.py index 2a4a96730f..f1e3c267cb 100644 --- a/dvc/repo/experiments/show.py +++ b/dvc/repo/experiments/show.py @@ -23,6 +23,8 @@ def _collect_experiment_commit( running=None, onerror: Optional[Callable] = None, ): + from dvc.dependency import ParamsDependency, RepoDependency + res: Dict[str, Optional[Any]] = defaultdict(dict) for rev in repo.brancher(revs=[exp_rev]): if rev == "workspace": @@ -39,6 +41,26 @@ def _collect_experiment_commit( if params: res["params"] = params + res["deps"] = { + dep.def_path: { + "hash": dep.hash_info.value, + "size": dep.meta.size, + "nfiles": dep.meta.nfiles, + } + for dep in repo.index.deps + if not isinstance(dep, (ParamsDependency, RepoDependency)) + } + + res["outs"] = { + out.def_path: { + "hash": out.hash_info.value, + "size": out.meta.size, + "nfiles": out.meta.nfiles, + } + for out in repo.index.outs + if not (out.is_metric or out.is_plot) + } + res["queued"] = stash if running is not None and exp_rev in running: res["running"] = True @@ -111,6 +133,7 @@ def show( param_deps=False, onerror: Optional[Callable] = None, ): + if onerror is None: onerror = onerror_collect diff --git a/tests/func/experiments/test_show.py b/tests/func/experiments/test_show.py index 2b69ea07d6..1cb13ffd7e 100644 --- a/tests/func/experiments/test_show.py +++ b/tests/func/experiments/test_show.py @@ -1,6 +1,7 @@ import logging import os from datetime import datetime +from unittest.mock import ANY import pytest from funcy import first, get_in @@ -38,7 +39,15 @@ def test_show_simple(tmp_dir, scm, dvc, exp_stage): assert dvc.experiments.show()["workspace"] == { "baseline": { "data": { + "deps": { + "copy.py": { + "hash": ANY, + "size": ANY, + "nfiles": None, + } + }, "metrics": {"metrics.yaml": {"data": {"foo": 1}}}, + "outs": {}, "params": {"params.yaml": {"data": {"foo": 1}}}, "queued": False, "running": False, @@ -63,7 +72,15 @@ def test_show_experiment(tmp_dir, scm, dvc, exp_stage, workspace): expected_baseline = { "data": { + "deps": { + "copy.py": { + "hash": ANY, + "size": ANY, + "nfiles": None, + } + }, "metrics": {"metrics.yaml": {"data": {"foo": 1}}}, + "outs": {}, "params": {"params.yaml": {"data": {"foo": 1}}}, "queued": False, "running": False, @@ -318,8 +335,16 @@ def test_show_running_workspace(tmp_dir, scm, dvc, exp_stage, capsys): assert dvc.experiments.show()["workspace"] == { "baseline": { "data": { + "deps": { + "copy.py": { + "hash": ANY, + "size": ANY, + "nfiles": None, + } + }, "metrics": {"metrics.yaml": {"data": {"foo": 1}}}, "params": {"params.yaml": {"data": {"foo": 1}}}, + "outs": {}, "queued": False, "running": True, "executor": info.location, @@ -329,7 +354,7 @@ def test_show_running_workspace(tmp_dir, scm, dvc, exp_stage, capsys): } capsys.readouterr() - assert main(["exp", "show", "--no-pager"]) == 0 + assert main(["exp", "show", "--csv"]) == 0 cap = capsys.readouterr() assert "Running" in cap.out assert info.location in cap.out @@ -450,26 +475,26 @@ def _get_rev_isotimestamp(rev): capsys.readouterr() assert main(["exp", "show", "--csv"]) == 0 cap = capsys.readouterr() + data_dep = first(x for x in dvc.index.deps if "copy.py" in x.fspath) + data_hash = data_dep.hash_info.value[:7] + assert "Experiment,rev,typ,Created,parent" in cap.out + assert "metrics.yaml:foo,params.yaml:foo,copy.py" in cap.out + assert f",workspace,baseline,,,3,3,{data_hash}" in cap.out assert ( - "Experiment,rev,typ,Created,parent,metrics.yaml:foo,params.yaml:foo" - in cap.out - ) - assert ",workspace,baseline,,,3,3" in cap.out - assert ( - "master,{},baseline,{},,1,1".format( - baseline_rev[:7], _get_rev_isotimestamp(baseline_rev) + "master,{},baseline,{},,1,1,{}".format( + baseline_rev[:7], _get_rev_isotimestamp(baseline_rev), data_hash ) in cap.out ) assert ( - "{},{},branch_base,{},,2,2".format( - ref_info1.name, rev1[:7], _get_rev_isotimestamp(rev1) + "{},{},branch_base,{},,2,2,{}".format( + ref_info1.name, rev1[:7], _get_rev_isotimestamp(rev1), data_hash ) in cap.out ) assert ( - "{},{},branch_commit,{},,3,3".format( - ref_info2.name, rev2[:7], _get_rev_isotimestamp(rev2) + "{},{},branch_commit,{},,3,3,{}".format( + ref_info2.name, rev2[:7], _get_rev_isotimestamp(rev2), data_hash ) in cap.out ) @@ -596,3 +621,42 @@ def test_show_parallel_coordinates(tmp_dir, dvc, scm, mocker, capsys): html_text = (tmp_dir / "dvc_plots" / "index.html").read_text() assert '"label": "Created"' not in html_text assert '"label": "foobar"' not in html_text + + +def test_show_outs(tmp_dir, dvc, scm): + tmp_dir.gen("copy.py", COPY_SCRIPT) + params_file = tmp_dir / "params.yaml" + params_data = { + "foo": 1, + "bar": 1, + } + (tmp_dir / params_file).dump(params_data) + + dvc.run( + cmd="python copy.py params.yaml metrics.yaml && echo out > out", + metrics_no_cache=["metrics.yaml"], + params=["foo", "bar"], + name="copy-file", + 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"] + assert outs == { + "out": { + "hash": ANY, + "size": ANY, + "nfiles": None, + } + }