diff --git a/dvc/command/experiments/show.py b/dvc/command/experiments/show.py index 4f27419f0f..976751ae19 100644 --- a/dvc/command/experiments/show.py +++ b/dvc/command/experiments/show.py @@ -3,7 +3,7 @@ from collections import Counter, OrderedDict, defaultdict from datetime import date, datetime from fnmatch import fnmatch -from typing import TYPE_CHECKING, Dict, Iterable, Optional +from typing import TYPE_CHECKING from funcy import lmap @@ -25,64 +25,6 @@ logger = logging.getLogger(__name__) -def _filter_name(names, label, filter_strs): - ret = defaultdict(dict) - path_filters = defaultdict(list) - - for filter_s in filter_strs: - path, _, name = filter_s.rpartition(":") - path_filters[path].append(name) - - for path, filters in path_filters.items(): - if path: - match_paths = [path] - else: - match_paths = names.keys() - for match_path in match_paths: - for f in filters: - matches = [ - name for name in names[match_path] if fnmatch(name, f) - ] - if not matches: - raise InvalidArgumentError( - f"'{f}' does not match any known {label}" - ) - ret[match_path].update({match: None for match in matches}) - - return ret - - -def _filter_names( - names: Dict[str, Dict[str, None]], - label: str, - include: Optional[Iterable], - exclude: Optional[Iterable], -): - if include and exclude: - intersection = set(include) & set(exclude) - if intersection: - values = ", ".join(intersection) - raise InvalidArgumentError( - f"'{values}' specified in both --include-{label} and" - f" --exclude-{label}" - ) - - if include: - ret = _filter_name(names, label, include) - else: - ret = names - - if exclude: - to_remove = _filter_name(names, label, exclude) - for path in to_remove: - if path in ret: - for key in to_remove[path]: - if key in ret[path]: - del ret[path][key] - - return ret - - def _update_names(names, items): for name, item in items: item = item.get("data", {}) @@ -100,18 +42,6 @@ def _collect_names(all_experiments, **kwargs): exp = exp_data.get("data", {}) _update_names(metric_names, exp.get("metrics", {}).items()) _update_names(param_names, exp.get("params", {}).items()) - metric_names = _filter_names( - metric_names, - "metrics", - kwargs.get("include_metrics"), - kwargs.get("exclude_metrics"), - ) - param_names = _filter_names( - param_names, - "params", - kwargs.get("include_params"), - kwargs.get("exclude_params"), - ) return metric_names, param_names @@ -305,17 +235,6 @@ def _extend_row(row, names, items, precision, fill_value=FILL_VALUE): row.append(ui.rich_text(str(_format_field(value, precision)))) -def _parse_filter_list(param_list): - ret = [] - for param_str in param_list: - path, _, param_str = param_str.rpartition(":") - if path: - ret.extend(f"{path}:{param}" for param in param_str.split(",")) - else: - ret.extend(param_str.split(",")) - return ret - - def experiments_table( all_experiments, headers, @@ -378,26 +297,16 @@ def baseline_styler(typ): def show_experiments( all_experiments, + keep=None, + drop=None, pager=True, - no_timestamp=False, csv=False, markdown=False, **kwargs, ): from funcy.seqs import flatten as flatten_list - include_metrics = _parse_filter_list(kwargs.pop("include_metrics", [])) - exclude_metrics = _parse_filter_list(kwargs.pop("exclude_metrics", [])) - include_params = _parse_filter_list(kwargs.pop("include_params", [])) - exclude_params = _parse_filter_list(kwargs.pop("exclude_params", [])) - - metric_names, param_names = _collect_names( - all_experiments, - include_metrics=include_metrics, - exclude_metrics=exclude_metrics, - include_params=include_params, - exclude_params=exclude_params, - ) + metric_names, param_names = _collect_names(all_experiments) headers = [ "Experiment", @@ -429,10 +338,9 @@ def show_experiments( kwargs.get("iso"), ) - if no_timestamp: - td.drop("Created") - for col in ("State", "Executor"): + if keep and col in keep: + continue if td.is_empty(col): td.drop(col) @@ -467,7 +375,15 @@ def show_experiments( ) if kwargs.get("only_changed", False): - td.drop_duplicates("cols") + subset = None + if keep: + subset = [ + x for x in td.keys() if not any(fnmatch(x, k) for k in keep) + ] + td.drop_duplicates("cols", subset=subset) + + if drop is not None: + td.drop(*drop, keep=keep) td.render( pager=pager, @@ -530,11 +446,8 @@ def run(self): show_experiments( all_experiments, - include_metrics=self.args.include_metrics, - exclude_metrics=self.args.exclude_metrics, - include_params=self.args.include_params, - exclude_params=self.args.exclude_params, - no_timestamp=self.args.no_timestamp, + keep=self.args.keep, + drop=self.args.drop, sort_by=self.args.sort_by, sort_order=self.args.sort_order, precision=precision, @@ -594,32 +507,18 @@ def add_parser(experiments_subparsers, parent_parser): help="Do not pipe output into a pager.", ) experiments_show_parser.add_argument( - "--include-metrics", - action="append", - default=[], - help="Include the specified metrics in output table.", - metavar="", - ) - experiments_show_parser.add_argument( - "--exclude-metrics", - action="append", - default=[], - help="Exclude the specified metrics from output table.", - metavar="", - ) - experiments_show_parser.add_argument( - "--include-params", + "--keep", action="append", default=[], - help="Include the specified params in output table.", - metavar="", + help="Always show the specified columns in output table.", + metavar="", ) experiments_show_parser.add_argument( - "--exclude-params", + "--drop", action="append", default=[], - help="Exclude the specified params from output table.", - metavar="", + help="Remove the specified columns from output table.", + metavar="", ) experiments_show_parser.add_argument( "--param-deps", @@ -641,12 +540,6 @@ def add_parser(experiments_subparsers, parent_parser): choices=("asc", "desc"), default="asc", ) - experiments_show_parser.add_argument( - "--no-timestamp", - action="store_true", - default=False, - help="Do not show experiment timestamps.", - ) experiments_show_parser.add_argument( "--sha", action="store_true", diff --git a/tests/func/experiments/test_show.py b/tests/func/experiments/test_show.py index 1aa6cdcd81..4499328eab 100644 --- a/tests/func/experiments/test_show.py +++ b/tests/func/experiments/test_show.py @@ -13,7 +13,6 @@ from dvc.utils.fs import makedirs from dvc.utils.serialize import YAMLFileCorruptedError from tests.func.test_repro_multistage import COPY_SCRIPT -from tests.utils import console_width def test_show_simple(tmp_dir, scm, dvc, exp_stage): @@ -174,104 +173,12 @@ def test_show_checkpoint_branch( assert f"({branch_rev[:7]})" in cap.out -@pytest.mark.parametrize( - "i_metrics,i_params,e_metrics,e_params,included,excluded", - [ - ( - "foo", - "foo", - None, - None, - ["foo"], - ["bar", "train/foo", "nested.foo"], - ), - ( - None, - None, - "foo", - "foo", - ["bar", "train/foo", "nested.foo"], - ["foo"], - ), - ( - "foo,bar", - "foo,bar", - None, - None, - ["foo", "bar"], - ["train/foo", "train/bar", "nested.foo", "nested.bar"], - ), - ( - "metrics.yaml:foo,bar", - "params.yaml:foo,bar", - None, - None, - ["foo", "bar"], - ["train/foo", "train/bar", "nested.foo", "nested.bar"], - ), - ( - "train/*", - "train/*", - None, - None, - ["train/foo", "train/bar"], - ["foo", "bar", "nested.foo", "nested.bar"], - ), - ( - None, - None, - "train/*", - "train/*", - ["foo", "bar", "nested.foo", "nested.bar"], - ["train/foo", "train/bar"], - ), - ( - "train/*", - "train/*", - "*foo", - "*foo", - ["train/bar"], - ["train/foo", "foo", "bar", "nested.foo", "nested.bar"], - ), - ( - "nested.*", - "nested.*", - None, - None, - ["nested.foo", "nested.bar"], - ["foo", "bar", "train/foo", "train/bar"], - ), - ( - None, - None, - "nested.*", - "nested.*", - ["foo", "bar", "train/foo", "train/bar"], - ["nested.foo", "nested.bar"], - ), - ( - "*.*", - "*.*", - "*.bar", - "*.bar", - ["nested.foo"], - ["foo", "bar", "nested.bar", "train/foo", "train/bar"], - ), - ], -) def test_show_filter( tmp_dir, scm, dvc, capsys, - i_metrics, - i_params, - e_metrics, - e_params, - included, - excluded, ): - from dvc.ui import ui capsys.readouterr() div = "│" if os.name == "nt" else "┃" @@ -306,26 +213,33 @@ def test_show_filter( ) scm.commit("init") - command = ["exp", "show", "--no-pager", "--no-timestamp"] - if i_metrics is not None: - command.append(f"--include-metrics={i_metrics}") - if i_params is not None: - command.append(f"--include-params={i_params}") - if e_metrics is not None: - command.append(f"--exclude-metrics={e_metrics}") - if e_params is not None: - command.append(f"--exclude-params={e_params}") - - with console_width(ui.rich_console, 255): - assert main(command) == 0 + capsys.readouterr() + assert main(["exp", "show", "--drop=*foo"]) == 0 + cap = capsys.readouterr() + for filtered in ["foo", "train/foo", "nested.foo"]: + assert f"{div} params.yaml:{filtered} {div}" not in cap.out + assert f"{div} metrics.yaml:{filtered} {div}" not in cap.out + + capsys.readouterr() + assert main(["exp", "show", "--drop=*foo", "--keep=*train*"]) == 0 + cap = capsys.readouterr() + for filtered in ["foo", "nested.foo"]: + assert f"{div} params.yaml:{filtered} {div}" not in cap.out + assert f"{div} metrics.yaml:{filtered} {div}" not in cap.out + assert f"{div} params.yaml:train/foo {div}" in cap.out + assert f"{div} metrics.yaml:train/foo {div}" in cap.out + + capsys.readouterr() + assert main(["exp", "show", "--drop=params.yaml:*foo"]) == 0 cap = capsys.readouterr() + for filtered in ["foo", "train/foo", "nested.foo"]: + assert f"{div} params.yaml:{filtered} {div}" not in cap.out + assert f"{div} metrics.yaml:{filtered} {div}" in cap.out - for i in included: - assert f"{div} params.yaml:{i} {div}" in cap.out - assert f"{div} metrics.yaml:{i} {div}" in cap.out - for e in excluded: - assert f"{div} params.yaml:{e} {div}" not in cap.out - assert f"{div} metrics.yaml:{e} {div}" not in cap.out + capsys.readouterr() + assert main(["exp", "show", "--drop=Created"]) == 0 + cap = capsys.readouterr() + assert "Created" not in cap.out def test_show_multiple_commits(tmp_dir, scm, dvc, exp_stage): @@ -558,12 +472,15 @@ def test_show_only_changed(tmp_dir, dvc, scm, capsys): capsys.readouterr() assert main(["exp", "show"]) == 0 cap = capsys.readouterr() - - print(cap) assert "bar" in cap.out capsys.readouterr() assert main(["exp", "show", "--only-changed"]) == 0 cap = capsys.readouterr() - assert "bar" not in cap.out + + capsys.readouterr() + assert main(["exp", "show", "--only-changed", "--keep=*bar"]) == 0 + cap = capsys.readouterr() + assert "params.yaml:bar" in cap.out + assert "metrics.yaml:bar" in cap.out