diff --git a/dvc/command/plots.py b/dvc/command/plots.py index ef50de6e7f..7bb96068c5 100644 --- a/dvc/command/plots.py +++ b/dvc/command/plots.py @@ -4,6 +4,7 @@ from dvc.command.base import CmdBase, append_doc_link, fix_subparsers from dvc.exceptions import DvcException +from dvc.schema import PLOT_PROPS from dvc.utils import format_link logger = logging.getLogger(__name__) @@ -32,6 +33,11 @@ class CmdPlots(CmdBase): def _func(self, *args, **kwargs): raise NotImplementedError + def _props(self): + # Pass only props specified by user, to not shadow ones from plot def + props = {p: getattr(self.args, p) for p in PLOT_PROPS} + return {k: v for k, v in props.items() if v is not None} + def run(self): if self.args.show_vega: if not self.args.targets: @@ -44,16 +50,7 @@ def run(self): return 1 try: - plots = self._func( - targets=self.args.targets, - template=self.args.template, - x_field=self.args.x, - y_field=self.args.y, - csv_header=not self.args.no_csv_header, - title=self.args.title, - x_title=self.args.xlab, - y_title=self.args.ylab, - ) + plots = self._func(targets=self.args.targets, props=self._props()) if self.args.show_vega: target = self.args.targets[0] @@ -91,6 +88,14 @@ def _func(self, *args, **kwargs): return self.repo.plots.diff(*args, revs=self.args.revisions, **kwargs) +class CmdPlotsModify(CmdPlots): + def run(self): + self.repo.plots.modify( + self.args.target, props=self._props(), unset=self.args.unset, + ) + return 0 + + def add_parser(subparsers, parent_parser): PLOTS_HELP = ( "Generating plots for metrics stored in structured files " @@ -119,51 +124,13 @@ def add_parser(subparsers, parent_parser): help=SHOW_HELP, formatter_class=argparse.RawDescriptionHelpFormatter, ) - plots_show_parser.add_argument( - "-t", - "--template", - nargs="?", - default=None, - help=( - "Special JSON or HTML schema file to inject with the data. " - "See {}".format( - format_link("https://man.dvc.org/plots#plot-templates") - ) - ), - ) - plots_show_parser.add_argument( - "-o", "--out", default=None, help="Destination path to save plots to.", - ) - plots_show_parser.add_argument( - "-x", default=None, help="Field name for x axis." - ) - plots_show_parser.add_argument( - "-y", default=None, help="Field name for y axis." - ) - plots_show_parser.add_argument( - "--no-csv-header", - action="store_true", - default=False, - help="Required when CSV or TSV datafile does not have a header.", - ) - plots_show_parser.add_argument( - "--show-vega", - action="store_true", - default=False, - help="Show output in VEGA format.", - ) - plots_show_parser.add_argument("--title", default=None, help="Plot title.") - plots_show_parser.add_argument( - "--xlab", default=None, help="X axis title." - ) - plots_show_parser.add_argument( - "--ylab", default=None, help="Y axis title." - ) plots_show_parser.add_argument( "targets", nargs="*", - help="Metrics files to visualize. Shows all plots by default.", + help="Plots files to visualize. Shows all plots by default.", ) + _add_props_arguments(plots_show_parser) + _add_output_arguments(plots_show_parser) plots_show_parser.set_defaults(func=CmdPlotsShow) PLOTS_DIFF_HELP = ( @@ -178,6 +145,37 @@ def add_parser(subparsers, parent_parser): formatter_class=argparse.RawDescriptionHelpFormatter, ) plots_diff_parser.add_argument( + "--targets", + nargs="*", + help="Plots file to visualize. Shows all plots by default.", + ) + plots_diff_parser.add_argument( + "revisions", nargs="*", default=None, help="Git commits to plot from", + ) + _add_props_arguments(plots_diff_parser) + _add_output_arguments(plots_diff_parser) + plots_diff_parser.set_defaults(func=CmdPlotsDiff) + + PLOTS_MODIFY_HELP = "Modify plot props associated with a target file." + plots_modify_parser = plots_subparsers.add_parser( + "modify", + parents=[parent_parser], + description=append_doc_link(PLOTS_MODIFY_HELP, "plots/modify"), + help=PLOTS_MODIFY_HELP, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + plots_modify_parser.add_argument( + "target", help="Plot file to set props to.", + ) + _add_props_arguments(plots_modify_parser) + plots_modify_parser.add_argument( + "--unset", nargs="*", help="Props to unset.", + ) + plots_modify_parser.set_defaults(func=CmdPlotsModify) + + +def _add_props_arguments(parser): + parser.add_argument( "-t", "--template", nargs="?", @@ -189,40 +187,27 @@ def add_parser(subparsers, parent_parser): ) ), ) - plots_diff_parser.add_argument( - "--targets", - nargs="*", - help="Metrics file to visualize. Shows all plots by default.", - ) - plots_diff_parser.add_argument( - "-o", "--out", default=None, help="Destination path to save plots to.", - ) - plots_diff_parser.add_argument( - "-x", default=None, help="Field name for x axis." - ) - plots_diff_parser.add_argument( - "-y", default=None, help="Field name for y axis." - ) - plots_diff_parser.add_argument( + parser.add_argument("-x", default=None, help="Field name for x axis.") + parser.add_argument("-y", default=None, help="Field name for y axis.") + parser.add_argument( "--no-csv-header", - action="store_true", - default=False, + action="store_false", + dest="csv_header", + default=None, # Use default None to distinguish when it's not used help="Provided CSV ot TSV datafile does not have a header.", ) - plots_diff_parser.add_argument( + parser.add_argument("--title", default=None, help="Plot title.") + parser.add_argument("--xlab", default=None, help="X axis title.") + parser.add_argument("--ylab", default=None, help="Y axis title.") + + +def _add_output_arguments(parser): + parser.add_argument( + "-o", "--out", default=None, help="Destination path to save plots to.", + ) + parser.add_argument( "--show-vega", action="store_true", default=False, help="Show output in VEGA format.", ) - plots_diff_parser.add_argument("--title", default=None, help="Plot title.") - plots_diff_parser.add_argument( - "--xlab", default=None, help="X axis title." - ) - plots_diff_parser.add_argument( - "--ylab", default=None, help="Y axis title." - ) - plots_diff_parser.add_argument( - "revisions", nargs="*", default=None, help="Git commits to plot from", - ) - plots_diff_parser.set_defaults(func=CmdPlotsDiff) diff --git a/dvc/output/base.py b/dvc/output/base.py index 8cbae62efe..344774c2c1 100644 --- a/dvc/output/base.py +++ b/dvc/output/base.py @@ -56,6 +56,12 @@ class BaseOutput: PARAM_METRIC_XPATH = "xpath" PARAM_PLOT = "plot" PARAM_PLOT_TEMPLATE = "template" + PARAM_PLOT_X = "x" + PARAM_PLOT_Y = "y" + PARAM_PLOT_XLAB = "xlab" + PARAM_PLOT_YLAB = "ylab" + PARAM_PLOT_TITLE = "title" + PARAM_PLOT_CSV_HEADER = "csv_header" PARAM_PERSIST = "persist" METRIC_SCHEMA = Any( diff --git a/dvc/repo/__init__.py b/dvc/repo/__init__.py index 3115bffe28..5c203bd293 100644 --- a/dvc/repo/__init__.py +++ b/dvc/repo/__init__.py @@ -441,7 +441,7 @@ def _collect_graph(self, stages): stages = stages or self.stages outs = Trie() # Use trie to efficiently find overlapping outs and deps - for stage in filter(bool, stages): + for stage in filter(bool, stages): # bug? not using it later for out in stage.outs: out_key = out.path_info.parts diff --git a/dvc/repo/plots/__init__.py b/dvc/repo/plots/__init__.py index 5f06b74915..1a440d5e0f 100644 --- a/dvc/repo/plots/__init__.py +++ b/dvc/repo/plots/__init__.py @@ -11,3 +11,21 @@ def diff(self, *args, **kwargs): from .diff import diff return diff(self.repo, *args, **kwargs) + + def modify(self, path, props=None, unset=None): + from dvc.dvcfile import Dvcfile + + (out,) = self.repo.find_outs_by_path(path) + + # This out will become a plot unless it is one already + if not out.plot: + out.plot = {} + + for field in unset or (): + out.plot.pop(field, None) + out.plot.update(props or {}) + + out.verify_metric() + + dvcfile = Dvcfile(self.repo, out.stage.path) + dvcfile.dump(out.stage, update_pipeline=True) diff --git a/dvc/repo/plots/show.py b/dvc/repo/plots/show.py index 225b1dc825..c205fe495d 100644 --- a/dvc/repo/plots/show.py +++ b/dvc/repo/plots/show.py @@ -1,13 +1,14 @@ +import copy import logging import os -from collections import defaultdict -from funcy import first, last +from funcy import first, last, project from dvc.exceptions import DvcException from dvc.repo import locked +from dvc.schema import PLOT_PROPS -from .data import PlotData +from .data import NoMetricInHistoryError, PlotData from .template import NoDataForTemplateError, Template logger = logging.getLogger(__name__) @@ -43,29 +44,24 @@ def _evaluate_templatepath(repo, template=None): @locked def fill_template( - repo, - datafile, - template_path, - revisions, - fields=None, - path=None, - csv_header=True, - x_field=None, - y_field=None, - **kwargs, + repo, datafile, template_path, revisions, props, ): - if x_field and fields: - fields.add(x_field) + # Copy things to not modify passed values + props = props.copy() + fields = copy.copy(props.get("fields")) - if y_field and fields: - fields.add(y_field) + if props.get("x") and fields: + fields.add(props.get("x")) + + if props.get("y") and fields: + fields.add(props.get("y")) template_datafiles, x_anchor, y_anchor = _parse_template( template_path, datafile ) - append_index = x_anchor and not x_field + append_index = x_anchor and not props.get("x") if append_index: - x_field = PlotData.INDEX_FIELD + props["x"] = PlotData.INDEX_FIELD template_data = {} for template_datafile in template_datafiles: @@ -76,13 +72,13 @@ def fill_template( for pd in plot_datas: rev_data_points = pd.to_datapoints( fields=fields, - path=path, - csv_header=csv_header, + path=props.get("path"), + csv_header=props.get("csv_header", True), append_index=append_index, ) - if y_anchor and not y_field: - y_field = _infer_y_field(rev_data_points, x_field) + if y_anchor and not props.get("y"): + props["y"] = _infer_y_field(rev_data_points, props.get("x")) tmp_data.extend(rev_data_points) template_data[template_datafile] = tmp_data @@ -91,12 +87,7 @@ def fill_template( raise NoDataForTemplateError(template_path) content = Template.fill( - template_path, - template_data, - priority_datafile=datafile, - x_field=x_field, - y_field=y_field, - **kwargs, + template_path, template_data, priority_datafile=datafile, props=props, ) path = datafile or ",".join(template_datafiles) @@ -113,17 +104,17 @@ def _infer_y_field(rev_data_points, x_field): return y_field -def _show(repo, datafile=None, template=None, revs=None, **kwargs): +def _show(repo, datafile=None, revs=None, props=None): if revs is None: revs = ["working tree"] - if not datafile and not template: + if not datafile and not props.get("template"): raise NoDataOrTemplateProvided() - template_path = _evaluate_templatepath(repo, template) + template_path = _evaluate_templatepath(repo, props.get("template")) plot_datafile, plot_content = fill_template( - repo, datafile, template_path, revs, **kwargs + repo, datafile, template_path, revs, props ) return plot_datafile, plot_content @@ -148,67 +139,72 @@ def _parse_template(template_path, priority_datafile): ) -def _collect_plots(repo): - plots = defaultdict(set) +def _collect_plots(repo, targets=None): + from dvc.exceptions import OutputNotFoundError + from contextlib import suppress - for stage in repo.stages: - for out in stage.outs: - if not out.plot: - continue + def _targets_to_outs(targets): + for t in targets: + with suppress(OutputNotFoundError): + (out,) = repo.find_outs_by_path(t) + yield out - if isinstance(out.plot, dict): - template = out.plot[out.PARAM_PLOT_TEMPLATE] - else: - template = None + if targets: + outs = _targets_to_outs(targets) + else: + outs = (out for stage in repo.stages for out in stage.outs if out.plot) - plots[str(out)].add(template) + return {str(out): _plot_props(out) for out in outs} - return plots +def _plot_props(out): + if not out.plot: + raise DvcException( + f"'{out}' is not a plot. Use `dvc plots modify` to change that." + ) + if isinstance(out.plot, list): + raise DvcException("Multiple plots per data file not supported yet.") + if isinstance(out.plot, bool): + return {} + + return project(out.plot, PLOT_PROPS) -def show(repo, targets=None, template=None, revs=None, **kwargs) -> dict: + +def show(repo, targets=None, revs=None, props=None) -> dict: if isinstance(targets, str): targets = [targets] - - if targets: - for target in targets: - return { - target: _show( - repo, - datafile=target, - template=template, - revs=revs, - **kwargs, - )[1] - } - - if not revs: - plots = _collect_plots(repo) - else: - plots = defaultdict(set) - for rev in repo.brancher(revs=revs): - for plot, templates in _collect_plots(repo).items(): - plots[plot].update(templates) + if props is None: + props = {} + + # Collect plot data files with associated props + plots = {} + for rev in repo.brancher(revs=revs): + if revs is not None and rev not in revs: + continue + + for datafile, file_props in _collect_plots(repo, targets).items(): + # props from command line overwrite plot props from out definition + full_props = {**file_props, **props} + + if datafile in plots: + saved_rev, saved_props = plots[datafile] + if saved_props != props: + logger.warning( + f"Inconsistent plot props for '{datafile}' in " + f"'{saved_rev}' and '{rev}'. " + f"Going to use ones from '{saved_rev}'" + ) + else: + plots[datafile] = rev, full_props if not plots: - datafile, plot = _show( - repo, datafile=None, template=template, revs=revs, **kwargs - ) - return {datafile: plot} - - ret = {} - for plot, templates in plots.items(): - tmplt = template - if len(templates) == 1: - tmplt = list(templates)[0] - elif not template: - raise TooManyTemplatesError( - f"'{plot}' uses multiple templates '{templates}'. " - "Use `-t|--template` to specify the template to use. " - ) + if targets: + raise NoMetricInHistoryError(", ".join(targets)) - ret[plot] = _show( - repo, datafile=plot, template=tmplt, revs=revs, **kwargs - )[1] + datafile, plot = _show(repo, datafile=None, revs=revs, props=props) + return {datafile: plot} - return ret + return { + datafile: _show(repo, datafile=datafile, revs=revs, props=props)[1] + for datafile, (_, props) in plots.items() + } diff --git a/dvc/repo/plots/template.py b/dvc/repo/plots/template.py index 731897f5d2..0691492922 100644 --- a/dvc/repo/plots/template.py +++ b/dvc/repo/plots/template.py @@ -86,29 +86,24 @@ def get_datafile(anchor_string): @staticmethod def fill( - template_path, - data, - priority_datafile=None, - x_field=None, - y_field=None, - title=None, - x_title=None, - y_title=None, + template_path, data, priority_datafile=None, props=None, ): + props = props or {} + with open(template_path) as fobj: result_content = fobj.read() - if x_field: - Template._check_field_exists(data, x_field) - if y_field: - Template._check_field_exists(data, y_field) + if props.get("x"): + Template._check_field_exists(data, props.get("x")) + if props.get("y"): + Template._check_field_exists(data, props.get("y")) result_content = Template._replace_data_anchors( result_content, data, priority_datafile ) result_content = Template._replace_metadata_anchors( - result_content, title, x_field, x_title, y_field, y_title + result_content, props ) return result_content @@ -122,34 +117,23 @@ def _check_field_exists(data, field): raise NoFieldInDataError(field) @staticmethod - def _replace_metadata_anchors( - result_content, title, x_field, x_title, y_field, y_title - ): - if Template.TITLE_ANCHOR in result_content: - if title: - result_content = result_content.replace( - Template.TITLE_ANCHOR, title - ) - else: - result_content = result_content.replace( - Template.TITLE_ANCHOR, "" - ) - if Template.X_ANCHOR in result_content and x_field: - result_content = result_content.replace(Template.X_ANCHOR, x_field) - if Template.Y_ANCHOR in result_content and y_field: - result_content = result_content.replace(Template.Y_ANCHOR, y_field) - if Template.X_TITLE_ANCHOR in result_content: - if not x_title and x_field: - x_title = x_field - result_content = result_content.replace( - Template.X_TITLE_ANCHOR, x_title - ) - if Template.Y_TITLE_ANCHOR in result_content: - if not y_title and y_field: - y_title = y_field - result_content = result_content.replace( - Template.Y_TITLE_ANCHOR, y_title - ) + def _replace_metadata_anchors(result_content, props): + props.setdefault("title", "") + props.setdefault("xlab", props.get("x")) + props.setdefault("ylab", props.get("y")) + + replace_pairs = [ + (Template.TITLE_ANCHOR, "title"), + (Template.X_ANCHOR, "x"), + (Template.Y_ANCHOR, "y"), + (Template.X_TITLE_ANCHOR, "xlab"), + (Template.Y_TITLE_ANCHOR, "ylab"), + ] + for anchor, key in replace_pairs: + value = props.get(key) + if anchor in result_content and value is not None: + result_content = result_content.replace(anchor, value) + return result_content @staticmethod diff --git a/dvc/schema.py b/dvc/schema.py index affc63d751..2e45ca65e7 100644 --- a/dvc/schema.py +++ b/dvc/schema.py @@ -29,8 +29,20 @@ str: {BaseOutput.PARAM_CACHE: bool, BaseOutput.PARAM_PERSIST: bool} } -PLOT_PSTAGE_SCHEMA = OUT_PSTAGE_DETAILED_SCHEMA.copy() -PLOT_PSTAGE_SCHEMA[str][BaseOutput.PARAM_PLOT_TEMPLATE] = str +PLOT_PROPS = { + BaseOutput.PARAM_PLOT_TEMPLATE: str, + BaseOutput.PARAM_PLOT_X: str, + BaseOutput.PARAM_PLOT_Y: str, + BaseOutput.PARAM_PLOT_XLAB: str, + BaseOutput.PARAM_PLOT_YLAB: str, + BaseOutput.PARAM_PLOT_TITLE: str, + BaseOutput.PARAM_PLOT_CSV_HEADER: bool, +} +PLOT_PROPS_SCHEMA = { + **OUT_PSTAGE_DETAILED_SCHEMA[str], + **PLOT_PROPS, +} +PLOT_PSTAGE_SCHEMA = {str: Any(PLOT_PROPS_SCHEMA, [PLOT_PROPS_SCHEMA])} PARAM_PSTAGE_NON_DEFAULT_SCHEMA = {str: [str]} diff --git a/tests/func/plots/test_diff.py b/tests/func/plots/test_diff.py index 8a8500e583..9145d9b8de 100644 --- a/tests/func/plots/test_diff.py +++ b/tests/func/plots/test_diff.py @@ -28,7 +28,7 @@ def test_diff_dirty(tmp_dir, scm, dvc, run_copy_metrics): "metric_t.json", "metric.json", plots_no_cache=["metric.json"] ) - plot_string = dvc.plots.diff(fields={"y"})["metric.json"] + plot_string = dvc.plots.diff(props={"fields": {"y"}})["metric.json"] plot_content = json.loads(plot_string) assert plot_content["data"]["values"] == [ diff --git a/tests/func/plots/test_plots.py b/tests/func/plots/test_plots.py index 949c7be794..0b467dbb4b 100644 --- a/tests/func/plots/test_plots.py +++ b/tests/func/plots/test_plots.py @@ -22,10 +22,6 @@ ) -def _remove_whitespace(value): - return value.replace(" ", "").replace("\n", "") - - def _write_csv(metric, filename, header=True): with open(filename, "w", newline="") as csvobj: if header: @@ -53,12 +49,13 @@ def test_plot_csv_one_column(tmp_dir, scm, dvc, run_copy_metrics): "metric_t.csv", "metric.csv", plots_no_cache=["metric.csv"] ) - plot_string = dvc.plots.show( - csv_header=False, - x_title="x_title", - y_title="y_title", - title="mytitle", - )["metric.csv"] + props = { + "csv_header": False, + "xlab": "x_title", + "ylab": "y_title", + "title": "mytitle", + } + plot_string = dvc.plots.show(props=props)["metric.csv"] plot_content = json.loads(plot_string) assert plot_content["title"] == "mytitle" @@ -115,9 +112,8 @@ def test_plot_csv_choose_axes(tmp_dir, scm, dvc, run_copy_metrics): "metric_t.csv", "metric.csv", plots_no_cache=["metric.csv"] ) - plot_string = dvc.plots.show(x_field="first_val", y_field="second_val")[ - "metric.csv" - ] + props = {"x": "first_val", "y": "second_val"} + plot_string = dvc.plots.show(props=props)["metric.csv"] plot_content = json.loads(plot_string) assert plot_content["data"]["values"] == [ @@ -206,9 +202,8 @@ def test_plot_confusion(tmp_dir, dvc, run_copy_metrics): commit="first run", ) - plot_string = dvc.plots.show( - template="confusion", x_field="predicted", y_field="actual", - )["metric.json"] + props = {"template": "confusion", "x": "predicted", "y": "actual"} + plot_string = dvc.plots.show(props=props)["metric.json"] plot_content = json.loads(plot_string) assert plot_content["data"]["values"] == [ @@ -248,9 +243,9 @@ def test_plot_multiple_revs_default(tmp_dir, scm, dvc, run_copy_metrics): plots_no_cache=["metric.json"], commit="third", ) - plot_string = dvc.plots.show(fields={"y"}, revs=["HEAD", "v2", "v1"],)[ - "metric.json" - ] + plot_string = dvc.plots.show( + revs=["HEAD", "v2", "v1"], props={"fields": {"y"}} + )["metric.json"] plot_content = json.loads(plot_string) assert plot_content["data"]["values"] == [ @@ -291,9 +286,10 @@ def test_plot_multiple_revs(tmp_dir, scm, dvc, run_copy_metrics): scm.add(["metric.json", stage.path]) scm.commit("third") - plot_string = dvc.plots.show( - template="template.json", revs=["HEAD", "v2", "v1"], - )["metric.json"] + props = {"template": "template.json"} + plot_string = dvc.plots.show(revs=["HEAD", "v2", "v1"], props=props)[ + "metric.json" + ] plot_content = json.loads(plot_string) assert plot_content["data"]["values"] == [ @@ -379,9 +375,8 @@ def test_custom_template(tmp_dir, scm, dvc, custom_template, run_copy_metrics): tag="v1", ) - plot_string = dvc.plots.show( - template=os.fspath(custom_template), x_field="a", y_field="b" - )["metric.json"] + props = {"template": os.fspath(custom_template), "x": "a", "y": "b"} + plot_string = dvc.plots.show(props=props)["metric.json"] plot_content = json.loads(plot_string) assert plot_content["data"]["values"] == [ @@ -413,9 +408,8 @@ def test_custom_template_with_specified_data( tag="v1", ) - plot_string = dvc.plots.show( - template=os.fspath(custom_template), x_field="a", y_field="b", - )["metric.json"] + props = {"template": os.fspath(custom_template), "x": "a", "y": "b"} + plot_string = dvc.plots.show(props=props)["metric.json"] plot_content = json.loads(plot_string) assert plot_content["data"]["values"] == [ @@ -444,14 +438,15 @@ def test_plot_override_specified_data_source( run_copy_metrics( "metric1.json", "metric2.json", - outs_no_cache=["metric2.json"], + plots_no_cache=["metric2.json"], commit="init", tag="v1", ) - plot_string = dvc.plots.show( - targets=["metric2.json"], template="newtemplate.json", x_field="a" - )["metric2.json"] + props = {"template": "newtemplate.json", "x": "a"} + plot_string = dvc.plots.show(targets=["metric2.json"], props=props)[ + "metric2.json" + ] plot_content = json.loads(plot_string) assert plot_content["data"]["values"] == [ @@ -467,18 +462,35 @@ def test_should_raise_on_no_template_and_datafile(tmp_dir, dvc): dvc.plots.show() -def test_should_raise_on_no_template(tmp_dir, dvc): +def test_should_raise_on_no_template(tmp_dir, dvc, run_copy_metrics): + metric = [{"val": 2}, {"val": 3}] + _write_json(tmp_dir, metric, "metric_t.json") + run_copy_metrics( + "metric_t.json", + "metric.json", + plots_no_cache=["metric.json"], + commit="first run", + ) + with pytest.raises(TemplateNotFoundError): - dvc.plots.show("metric.json", template="non_existing_template.json") + props = {"template": "non_existing_template.json"} + dvc.plots.show("metric.json", props=props) def test_plot_no_data(tmp_dir, dvc): with pytest.raises(NoDataForTemplateError): - dvc.plots.show(template="default") + dvc.plots.show(props={"template": "default"}) + +def test_plot_wrong_metric_type(tmp_dir, scm, dvc, run_copy_metrics): + tmp_dir.gen("metric_t.txt", "some text") + run_copy_metrics( + "metric_t.txt", + "metric.txt", + plots_no_cache=["metric.txt"], + commit="add text metric", + ) -def test_plot_wrong_metric_type(tmp_dir, scm, dvc): - tmp_dir.scm_gen("metric.txt", "content", commit="initial") with pytest.raises(PlotMetricTypeError): dvc.plots.show(targets=["metric.txt"]) @@ -496,12 +508,13 @@ def test_plot_choose_columns( tag="v1", ) - plot_string = dvc.plots.show( - template=os.fspath(custom_template), - fields={"b", "c"}, - x_field="b", - y_field="c", - )["metric.json"] + props = { + "template": os.fspath(custom_template), + "fields": {"b", "c"}, + "x": "b", + "y": "c", + } + plot_string = dvc.plots.show(props=props)["metric.json"] plot_content = json.loads(plot_string) assert plot_content["data"]["values"] == [ @@ -523,7 +536,7 @@ def test_plot_default_choose_column(tmp_dir, scm, dvc, run_copy_metrics): tag="v1", ) - plot_string = dvc.plots.show(fields={"b"})["metric.json"] + plot_string = dvc.plots.show(props={"fields": {"b"}})["metric.json"] plot_content = json.loads(plot_string) assert plot_content["data"]["values"] == [ @@ -563,7 +576,7 @@ def test_raise_on_wrong_field(tmp_dir, scm, dvc, run_copy_metrics): ) with pytest.raises(NoFieldInDataError): - dvc.plots.show("metric.json", x_field="no_val") + dvc.plots.show("metric.json", props={"x": "no_val"}) with pytest.raises(NoFieldInDataError): - dvc.plots.show("metric.json", y_field="no_val") + dvc.plots.show("metric.json", props={"y": "no_val"}) diff --git a/tests/unit/command/test_plots.py b/tests/unit/command/test_plots.py index d1728f514a..69b8a1ac89 100644 --- a/tests/unit/command/test_plots.py +++ b/tests/unit/command/test_plots.py @@ -41,14 +41,15 @@ def test_metrics_diff(dvc, mocker): m.assert_called_once_with( cmd.repo, targets=["datafile"], - template="template", revs=["HEAD", "tag1", "tag2"], - x_field="x_field", - y_field="y_field", - csv_header=True, - title="my_title", - x_title="x_title", - y_title="y_title", + props={ + "template": "template", + "x": "x_field", + "y": "y_field", + "title": "my_title", + "xlab": "x_title", + "ylab": "y_title", + }, ) @@ -79,13 +80,7 @@ def test_metrics_show(dvc, mocker): m.assert_called_once_with( cmd.repo, targets=["datafile"], - template="template", - x_field=None, - y_field=None, - csv_header=False, - title=None, - x_title=None, - y_title=None, + props={"template": "template", "csv_header": False}, )