diff --git a/dvc/repo/plots/__init__.py b/dvc/repo/plots/__init__.py index 15a5ae92a..d95394efc 100644 --- a/dvc/repo/plots/__init__.py +++ b/dvc/repo/plots/__init__.py @@ -444,6 +444,10 @@ def _collect_pipeline_files(repo, targets: List[str], props, onerror=None): if isinstance(dvcfile, PipelineFile): dvcfile_path = _relpath(repo.dvcfs, dvcfile.path) dvcfile_defs = dvcfile.load().get("plots", {}) + if isinstance(dvcfile_defs, list): + dvcfile_defs = { + k: v for elem in dvcfile_defs for k, v in elem.items() + } resolved = _resolve_definitions( repo.dvcfs, targets, diff --git a/dvc/schema.py b/dvc/schema.py index 5b5c24baf..191e67eea 100644 --- a/dvc/schema.py +++ b/dvc/schema.py @@ -118,7 +118,7 @@ PLOT_DEFINITION = { Output.PARAM_PLOT_TITLE: str, Output.PARAM_PLOT_TEMPLATE: str, } -PLOTS_SCHEMA = {str: Any(PLOT_DEFINITION, None)} +SINGLE_PLOT_SCHEMA = {str: Any(PLOT_DEFINITION, None)} FOREACH_IN = { Required(FOREACH_KWD): Any(dict, list, str), Required(DO_KWD): STAGE_DEFINITION, @@ -127,9 +127,9 @@ SINGLE_PIPELINE_STAGE_SCHEMA = { str: either_or(STAGE_DEFINITION, FOREACH_IN, [FOREACH_KWD, DO_KWD]) } MULTI_STAGE_SCHEMA = { + PLOTS: Any(SINGLE_PLOT_SCHEMA, [SINGLE_PLOT_SCHEMA]), STAGES: SINGLE_PIPELINE_STAGE_SCHEMA, VARS_KWD: VARS_SCHEMA, - PLOTS: PLOTS_SCHEMA, } COMPILED_SINGLE_STAGE_SCHEMA = Schema(SINGLE_STAGE_SCHEMA) diff --git a/tests/func/test_dvcfile.py b/tests/func/test_dvcfile.py index 5ec903da9..9b2f6b88f 100644 --- a/tests/func/test_dvcfile.py +++ b/tests/func/test_dvcfile.py @@ -15,6 +15,16 @@ from dvc.stage.exceptions import StageFileDoesNotExistError from dvc.stage.loader import StageNotFound from dvc.utils.strictyaml import YAMLValidationError +STAGE_EXAMPLE = { + "stage1": { + "cmd": "cp foo bar", + "desc": "stage desc", + "meta": {"key1": "value1", "key2": "value2"}, + "deps": ["foo"], + "outs": [{"bar": {"desc": "bar desc", "meta": {"key": "value"}}}], + } +} + def test_run_load_one_for_multistage(tmp_dir, dvc): tmp_dir.gen("foo", "foo") @@ -386,19 +396,7 @@ def test_dvcfile_try_dumping_parametrized_stage(tmp_dir, dvc, data, name): def test_dvcfile_load_dump_stage_with_desc_meta(tmp_dir, dvc): - data = { - "stages": { - "stage1": { - "cmd": "cp foo bar", - "desc": "stage desc", - "meta": {"key1": "value1", "key2": "value2"}, - "deps": ["foo"], - "outs": [ - {"bar": {"desc": "bar desc", "meta": {"key": "value"}}} - ], - } - } - } + data = {"stages": STAGE_EXAMPLE} (tmp_dir / "dvc.yaml").dump(data) stage = dvc.stage.load_one(name="stage1") @@ -411,3 +409,34 @@ def test_dvcfile_load_dump_stage_with_desc_meta(tmp_dir, dvc): # sanity check stage.dump() assert (tmp_dir / "dvc.yaml").parse() == data + + +@pytest.mark.parametrize( + "data", + ( + { + "plots": { + "path/to/plot": {"x": "value", "y": "value"}, + "path/to/another/plot": {"x": "value", "y": "value"}, + }, + "stages": STAGE_EXAMPLE, + }, + { + "plots": [ + {"path/to/plot": {"x": "value", "y": "value"}}, + {"path/to/another/plot": {"x": "value", "y": "value"}}, + ], + "stages": STAGE_EXAMPLE, + }, + ), +) +def test_dvcfile_load_with_plots(tmp_dir, dvc, data): + (tmp_dir / "dvc.yaml").dump(data) + plots = list(dvc.plots.collect()) + top_level_plots = plots[0]["workspace"]["definitions"]["data"]["dvc.yaml"][ + "data" + ] + assert all( + name in top_level_plots + for name in ("path/to/plot", "path/to/another/plot") + )