From ff77dd91760f0e4bda137e45a8fb089d7bc0f1cb Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Wed, 10 Jul 2024 15:14:50 -0400 Subject: [PATCH] feat!(plugins): make PluginProperties more complete This does several things: - Merges PluginProperties and PluginPropertiesModel - Makes PluginProperties.unmarshal() work for most plugins be default - Makes PluginProperties.marshal() fully dump json-able objects - Adds a `plugin` field to all plugins, providing their names. --- craft_parts/plugins/__init__.py | 2 +- craft_parts/plugins/ant_plugin.py | 23 +++--------- craft_parts/plugins/autotools_plugin.py | 23 +++--------- craft_parts/plugins/base.py | 11 ++++-- craft_parts/plugins/cmake_plugin.py | 23 +++--------- craft_parts/plugins/dotnet_plugin.py | 23 +++--------- craft_parts/plugins/dump_plugin.py | 25 +++---------- craft_parts/plugins/go_plugin.py | 23 +++--------- craft_parts/plugins/make_plugin.py | 23 +++--------- craft_parts/plugins/maven_plugin.py | 23 +++--------- craft_parts/plugins/meson_plugin.py | 23 +++--------- craft_parts/plugins/nil_plugin.py | 18 +++------- craft_parts/plugins/npm_plugin.py | 23 +++--------- craft_parts/plugins/properties.py | 47 +++++++++++++++---------- craft_parts/plugins/python_plugin.py | 23 +++--------- craft_parts/plugins/qmake_plugin.py | 22 +++--------- craft_parts/plugins/rust_plugin.py | 25 +++---------- craft_parts/plugins/scons_plugin.py | 23 +++--------- tests/unit/plugins/test_base.py | 13 +++---- tests/unit/plugins/test_dump_plugin.py | 3 +- tests/unit/plugins/test_plugins.py | 1 + tests/unit/plugins/test_properties.py | 9 ++--- tests/unit/plugins/test_validator.py | 2 +- 23 files changed, 111 insertions(+), 320 deletions(-) diff --git a/craft_parts/plugins/__init__.py b/craft_parts/plugins/__init__.py index 6e62e2fb..356fccac 100644 --- a/craft_parts/plugins/__init__.py +++ b/craft_parts/plugins/__init__.py @@ -18,7 +18,6 @@ from .base import Plugin, extract_plugin_properties from .plugins import ( - PluginProperties, extract_part_properties, get_plugin, get_plugin_class, @@ -27,6 +26,7 @@ unregister, unregister_all, ) +from .properties import PluginProperties from .validator import PluginEnvironmentValidator __all__ = [ diff --git a/craft_parts/plugins/ant_plugin.py b/craft_parts/plugins/ant_plugin.py index daa204a7..4310c2ca 100644 --- a/craft_parts/plugins/ant_plugin.py +++ b/craft_parts/plugins/ant_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2022 Canonical Ltd. +# Copyright 2022,2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -20,7 +20,7 @@ import os import shlex from collections.abc import Iterator -from typing import Any, cast +from typing import Any, Literal, cast from urllib.parse import urlsplit from overrides import override @@ -34,8 +34,9 @@ logger = logging.getLogger(__name__) -class AntPluginProperties(PluginProperties): +class AntPluginProperties(PluginProperties, frozen=True): """The part properties used by the Ant plugin.""" + plugin: Literal["ant"] = "ant" ant_build_targets: list[str] = [] ant_build_file: str | None = None @@ -43,22 +44,6 @@ class AntPluginProperties(PluginProperties): source: str - @classmethod - @override - def unmarshal(cls, data: dict[str, Any]) -> "AntPluginProperties": - """Populate make properties from the part specification. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - - :raise pydantic.ValidationError: If validation fails. - """ - plugin_data = extract_plugin_properties( - data, plugin_name="ant", required=["source"] - ) - return cls(**plugin_data) - class AntPluginEnvironmentValidator(validator.PluginEnvironmentValidator): """Check the execution environment for the Ant plugin. diff --git a/craft_parts/plugins/autotools_plugin.py b/craft_parts/plugins/autotools_plugin.py index 3e8bfa23..aced7205 100644 --- a/craft_parts/plugins/autotools_plugin.py +++ b/craft_parts/plugins/autotools_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2020 Canonical Ltd. +# Copyright 2020,2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -16,7 +16,7 @@ """The autotools plugin implementation.""" -from typing import Any, cast +from typing import Any, Literal, cast from overrides import override @@ -24,8 +24,9 @@ from .properties import PluginProperties -class AutotoolsPluginProperties(PluginProperties): +class AutotoolsPluginProperties(PluginProperties, frozen=True): """The part properties used by the autotools plugin.""" + plugin: Literal["autotools"] = "autotools" autotools_configure_parameters: list[str] = [] autotools_bootstrap_parameters: list[str] = [] @@ -33,22 +34,6 @@ class AutotoolsPluginProperties(PluginProperties): # part properties required by the plugin source: str - @classmethod - @override - def unmarshal(cls, data: dict[str, Any]) -> "AutotoolsPluginProperties": - """Populate autotools properties from the part specification. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - - :raise pydantic.ValidationError: If validation fails. - """ - plugin_data = extract_plugin_properties( - data, plugin_name="autotools", required=["source"] - ) - return cls(**plugin_data) - class AutotoolsPlugin(Plugin): """The autotools plugin is used for autotools-based parts. diff --git a/craft_parts/plugins/base.py b/craft_parts/plugins/base.py index c44dab37..37ce5d1d 100644 --- a/craft_parts/plugins/base.py +++ b/craft_parts/plugins/base.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2020-2023 Canonical Ltd. +# Copyright 2020-2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -16,9 +16,14 @@ """Plugin base class and definitions.""" +from __future__ import annotations + import abc from copy import deepcopy -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, ClassVar, Collection +from typing_extensions import Self + +import pydantic from craft_parts.actions import ActionProperties @@ -123,7 +128,7 @@ def _get_java_post_build_commands(self) -> list[str]: def extract_plugin_properties( - data: dict[str, Any], *, plugin_name: str, required: list[str] | None = None + data: dict[str, Any], *, plugin_name: str, required: Collection[str] | None = None ) -> dict[str, Any]: """Obtain plugin-specific entries from part properties. diff --git a/craft_parts/plugins/cmake_plugin.py b/craft_parts/plugins/cmake_plugin.py index f8398fb5..3f317548 100644 --- a/craft_parts/plugins/cmake_plugin.py +++ b/craft_parts/plugins/cmake_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2020-2022 Canonical Ltd. +# Copyright 2020-2022,2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -16,7 +16,7 @@ """The cmake plugin.""" -from typing import Any, cast +from typing import Any, Literal, cast from overrides import override @@ -24,8 +24,9 @@ from .properties import PluginProperties -class CMakePluginProperties(PluginProperties): +class CMakePluginProperties(PluginProperties, frozen=True): """The part properties used by the cmake plugin.""" + plugin: Literal["cmake"] = "cmake" cmake_parameters: list[str] = [] cmake_generator: str = "Unix Makefiles" @@ -33,22 +34,6 @@ class CMakePluginProperties(PluginProperties): # part properties required by the plugin source: str - @classmethod - @override - def unmarshal(cls, data: dict[str, Any]) -> "CMakePluginProperties": - """Populate class attributes from the part specification. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - - :raise pydantic.ValidationError: If validation fails. - """ - plugin_data = extract_plugin_properties( - data, plugin_name="cmake", required=["source"] - ) - return cls(**plugin_data) - class CMakePlugin(Plugin): """The cmake plugin is useful for building cmake based parts. diff --git a/craft_parts/plugins/dotnet_plugin.py b/craft_parts/plugins/dotnet_plugin.py index feae2275..932e55a8 100644 --- a/craft_parts/plugins/dotnet_plugin.py +++ b/craft_parts/plugins/dotnet_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2021 Canonical Ltd. +# Copyright 2021,2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -17,7 +17,7 @@ """The Dotnet plugin.""" import logging -from typing import Any, cast +from typing import Any, Literal, cast from overrides import override @@ -28,8 +28,9 @@ logger = logging.getLogger(__name__) -class DotnetPluginProperties(PluginProperties): +class DotnetPluginProperties(PluginProperties, frozen=True): """The part properties used by the Dotnet plugin.""" + plugin: Literal["dotnet"] = "dotnet" dotnet_build_configuration: str = "Release" dotnet_self_contained_runtime_identifier: str | None = None @@ -37,22 +38,6 @@ class DotnetPluginProperties(PluginProperties): # part properties required by the plugin source: str - @classmethod - @override - def unmarshal(cls, data: dict[str, Any]) -> "DotnetPluginProperties": - """Populate make properties from the part specification. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - - :raise pydantic.ValidationError: If validation fails. - """ - plugin_data = extract_plugin_properties( - data, plugin_name="dotnet", required=["source"] - ) - return cls(**plugin_data) - class DotPluginEnvironmentValidator(validator.PluginEnvironmentValidator): """Check the execution environment for the Dotnet plugin. diff --git a/craft_parts/plugins/dump_plugin.py b/craft_parts/plugins/dump_plugin.py index 54a00cdc..6a0f2016 100644 --- a/craft_parts/plugins/dump_plugin.py +++ b/craft_parts/plugins/dump_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2020 Canonical Ltd. +# Copyright 2020,2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -19,7 +19,7 @@ This plugin just dumps the content from a specified part source. """ -from typing import Any +from typing import Any, Literal from overrides import override @@ -27,25 +27,10 @@ from .properties import PluginProperties -class DumpPluginProperties(PluginProperties): +class DumpPluginProperties(PluginProperties, frozen=True): """The part properties used by the dump plugin.""" - - @classmethod - @override - def unmarshal(cls, data: dict[str, Any]) -> "DumpPluginProperties": - """Populate dump properties from the part specification. - - 'source' is a required part property. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - - :raise ValueError: If a required property is not found. - """ - if "source" not in data: - raise ValueError("'source' is required by the dump plugin") - return cls() + plugin: Literal["dump"] = "dump" + source: str class DumpPlugin(Plugin): diff --git a/craft_parts/plugins/go_plugin.py b/craft_parts/plugins/go_plugin.py index 4a2a1605..5eb7348d 100644 --- a/craft_parts/plugins/go_plugin.py +++ b/craft_parts/plugins/go_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2020-2021 Canonical Ltd. +# Copyright 2020-2021,2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -17,7 +17,7 @@ """The Go plugin.""" import logging -from typing import Any, cast +from typing import Any, Literal, cast from overrides import override @@ -30,8 +30,9 @@ logger = logging.getLogger(__name__) -class GoPluginProperties(PluginProperties): +class GoPluginProperties(PluginProperties, frozen=True): """The part properties used by the Go plugin.""" + plugin: Literal["go"] = "go" go_buildtags: list[str] = [] go_generate: list[str] = [] @@ -39,22 +40,6 @@ class GoPluginProperties(PluginProperties): # part properties required by the plugin source: str - @classmethod - @override - def unmarshal(cls, data: dict[str, Any]) -> "GoPluginProperties": - """Populate make properties from the part specification. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - - :raise pydantic.ValidationError: If validation fails. - """ - plugin_data = extract_plugin_properties( - data, plugin_name="go", required=["source"] - ) - return cls(**plugin_data) - class GoPluginEnvironmentValidator(validator.PluginEnvironmentValidator): """Check the execution environment for the Go plugin. diff --git a/craft_parts/plugins/make_plugin.py b/craft_parts/plugins/make_plugin.py index 7ce44110..574bffcc 100644 --- a/craft_parts/plugins/make_plugin.py +++ b/craft_parts/plugins/make_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2020-2021 Canonical Ltd. +# Copyright 2020-2021,2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -16,7 +16,7 @@ """The make plugin implementation.""" -from typing import Any, cast +from typing import Any, Literal, cast from overrides import override @@ -24,30 +24,15 @@ from .properties import PluginProperties -class MakePluginProperties(PluginProperties): +class MakePluginProperties(PluginProperties, frozen=True): """The part properties used by the make plugin.""" + plugin: Literal["make"] = "make" make_parameters: list[str] = [] # part properties required by the plugin source: str - @classmethod - @override - def unmarshal(cls, data: dict[str, Any]) -> "MakePluginProperties": - """Populate make properties from the part specification. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - - :raise pydantic.ValidationError: If validation fails. - """ - plugin_data = extract_plugin_properties( - data, plugin_name="make", required=["source"] - ) - return cls(**plugin_data) - class MakePlugin(Plugin): """A plugin useful for building make-based parts. diff --git a/craft_parts/plugins/maven_plugin.py b/craft_parts/plugins/maven_plugin.py index df7dd8d7..c1ffadef 100644 --- a/craft_parts/plugins/maven_plugin.py +++ b/craft_parts/plugins/maven_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2015-2023 Canonical Ltd. +# Copyright 2015-2023,2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -19,7 +19,7 @@ import os import re from pathlib import Path -from typing import Any, cast +from typing import Any, Literal, cast from urllib.parse import urlparse from xml.etree import ElementTree @@ -32,30 +32,15 @@ from .properties import PluginProperties -class MavenPluginProperties(PluginProperties): +class MavenPluginProperties(PluginProperties, frozen=True): """The part properties used by the maven plugin.""" + plugin: Literal["maven"] = "maven" maven_parameters: list[str] = [] # part properties required by the plugin source: str - @classmethod - @override - def unmarshal(cls, data: dict[str, Any]) -> "MavenPluginProperties": - """Populate class attributes from the part specification. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - - :raise pydantic.ValidationError: If validation fails. - """ - plugin_data = extract_plugin_properties( - data, plugin_name="maven", required=["source"] - ) - return cls(**plugin_data) - class MavenPluginEnvironmentValidator(validator.PluginEnvironmentValidator): """Check the execution environment for the maven plugin. diff --git a/craft_parts/plugins/meson_plugin.py b/craft_parts/plugins/meson_plugin.py index ebd7229d..0e8d1350 100644 --- a/craft_parts/plugins/meson_plugin.py +++ b/craft_parts/plugins/meson_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2022 Canonical Ltd. +# Copyright 2022,2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -19,7 +19,7 @@ import logging import shlex -from typing import Any, cast +from typing import Any, Literal, cast from overrides import override @@ -30,30 +30,15 @@ logger = logging.getLogger(__name__) -class MesonPluginProperties(PluginProperties): +class MesonPluginProperties(PluginProperties, frozen=True): """The part properties used by the Meson plugin.""" + plugin: Literal["meson"] = "meson" meson_parameters: list[str] = [] # part properties required by the plugin source: str - @classmethod - @override - def unmarshal(cls, data: dict[str, Any]) -> "MesonPluginProperties": - """Populate make properties from the part specification. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - - :raise pydantic.ValidationError: If validation fails. - """ - plugin_data = extract_plugin_properties( - data, plugin_name="meson", required=["source"] - ) - return cls(**plugin_data) - class MesonPluginEnvironmentValidator(validator.PluginEnvironmentValidator): """Check the execution environment for the Meson plugin. diff --git a/craft_parts/plugins/nil_plugin.py b/craft_parts/plugins/nil_plugin.py index a5957e2d..f09c4f1e 100644 --- a/craft_parts/plugins/nil_plugin.py +++ b/craft_parts/plugins/nil_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2020 Canonical Ltd. +# Copyright 2020,2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -20,7 +20,7 @@ automatically included, e.g. stage-packages. """ -from typing import Any +from typing import Any, ClassVar, Collection, Literal from overrides import override @@ -28,19 +28,11 @@ from .properties import PluginProperties -class NilPluginProperties(PluginProperties): +class NilPluginProperties(PluginProperties, frozen=True): """The part properties used by the nil plugin.""" + plugin: Literal["nil"] = "nil" - @classmethod - @override - def unmarshal(cls, data: dict[str, Any]) -> "NilPluginProperties": # noqa: ARG003 - """Populate class attributes from the part specification. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - """ - return cls() + _required_fields: ClassVar[Collection[str]] = () class NilPlugin(Plugin): diff --git a/craft_parts/plugins/npm_plugin.py b/craft_parts/plugins/npm_plugin.py index 252e942e..3ffa4373 100644 --- a/craft_parts/plugins/npm_plugin.py +++ b/craft_parts/plugins/npm_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2022 Canonical Ltd. +# Copyright 2022,2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -21,7 +21,7 @@ import platform import re from textwrap import dedent -from typing import Any, cast +from typing import Any, Literal, cast import requests from overrides import override @@ -47,8 +47,9 @@ _NODE_ARCH_FROM_PLATFORM = {"x86_64": {"32bit": "x86", "64bit": "x64"}} -class NpmPluginProperties(PluginProperties): +class NpmPluginProperties(PluginProperties, frozen=True): """The part properties used by the npm plugin.""" + plugin: Literal["npm"] = "npm" # part properties required by the plugin npm_include_node: bool = False @@ -66,22 +67,6 @@ def node_version_defined(self) -> Self: ) return self - @classmethod - @override - def unmarshal(cls, data: dict[str, Any]) -> "NpmPluginProperties": - """Populate class attributes from the part specification. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - - :raise pydantic.ValidationError: If validation fails. - """ - plugin_data = extract_plugin_properties( - data, plugin_name="npm", required=["source"] - ) - return cls(**plugin_data) - class NpmPluginEnvironmentValidator(validator.PluginEnvironmentValidator): """Check the execution environment for the npm plugin. diff --git a/craft_parts/plugins/properties.py b/craft_parts/plugins/properties.py index 7ccace90..eb927d5f 100644 --- a/craft_parts/plugins/properties.py +++ b/craft_parts/plugins/properties.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2021-2023 Canonical Ltd. +# Copyright 2021-2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -16,23 +16,16 @@ """Definitions and helpers for plugin options.""" -from typing import Any +from typing import Any, ClassVar, Collection +from typing_extensions import Self -from pydantic import BaseModel, ConfigDict +import pydantic +from . import base -class PluginPropertiesModel(BaseModel): - """Model for plugins properties using pydantic validation.""" - model_config = ConfigDict( - validate_assignment=True, - extra="forbid", - frozen=True, - alias_generator=lambda s: s.replace("_", "-"), - ) - - -class PluginProperties(PluginPropertiesModel): +# `frozen=True` here so that pyright will warn if child classes are mutable. +class PluginProperties(pydantic.BaseModel, frozen=True): """Options specific to a plugin. PluginProperties should be subclassed into plugin-specific property @@ -42,19 +35,34 @@ class PluginProperties(PluginPropertiesModel): build step is dirty. This can be overridden in each plugin if needed. """ + model_config = pydantic.ConfigDict( + validate_assignment=True, + extra="forbid", + frozen=True, + alias_generator=lambda s: s.replace("_", "-"), + ) + plugin: str = "" + + _required_fields: ClassVar[Collection[str]] = ("source",) + @classmethod - def unmarshal(cls, data: dict[str, Any]) -> "PluginProperties": # noqa: ARG003 + def unmarshal(cls, data: dict[str, Any]) -> Self: """Populate class attributes from the part specification. :param data: A dictionary containing part properties. :return: The populated plugin properties data object. """ - return cls() + return cls.model_validate( + base.extract_plugin_properties( + data, plugin_name=cls.__fields__["plugin"].default, + required=cls._required_fields, + ) + ) def marshal(self) -> dict[str, Any]: """Obtain a dictionary containing the plugin properties.""" - return self.dict(by_alias=True) + return self.model_dump(mode="json", by_alias=True, exclude={"plugin"}) @classmethod def get_pull_properties(cls) -> list[str]: @@ -65,4 +73,7 @@ def get_pull_properties(cls) -> list[str]: def get_build_properties(cls) -> list[str]: """Obtain the list of properties affecting the build stage.""" properties = cls.schema(by_alias=True).get("properties") - return list(properties.keys()) if properties else [] + if properties: + del properties["plugin"] + return list(properties) + return [] diff --git a/craft_parts/plugins/python_plugin.py b/craft_parts/plugins/python_plugin.py index bb132d4a..34323be1 100644 --- a/craft_parts/plugins/python_plugin.py +++ b/craft_parts/plugins/python_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2020-2023 Canonical Ltd. +# Copyright 2020-2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -18,7 +18,7 @@ import shlex from textwrap import dedent -from typing import Any, cast +from typing import Any, Literal, cast from overrides import override @@ -26,8 +26,9 @@ from .properties import PluginProperties -class PythonPluginProperties(PluginProperties): +class PythonPluginProperties(PluginProperties, frozen=True): """The part properties used by the python plugin.""" + plugin: Literal["python"] = "python" python_requirements: list[str] = [] python_constraints: list[str] = [] @@ -36,22 +37,6 @@ class PythonPluginProperties(PluginProperties): # part properties required by the plugin source: str - @classmethod - @override - def unmarshal(cls, data: dict[str, Any]) -> "PythonPluginProperties": - """Populate make properties from the part specification. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - - :raise pydantic.ValidationError: If validation fails. - """ - plugin_data = extract_plugin_properties( - data, plugin_name="python", required=["source"] - ) - return cls(**plugin_data) - class PythonPlugin(Plugin): """A plugin to build python parts.""" diff --git a/craft_parts/plugins/qmake_plugin.py b/craft_parts/plugins/qmake_plugin.py index d010bd0a..9391d276 100644 --- a/craft_parts/plugins/qmake_plugin.py +++ b/craft_parts/plugins/qmake_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2023 Canonical Ltd. +# Copyright 2023,2024 Canonical Ltd. # Copyright 2023 Scarlett Moore . # # This program is free software; you can redistribute it and/or @@ -18,7 +18,7 @@ """The qmake plugin.""" -from typing import Any, cast +from typing import Any, Literal, cast from overrides import override from typing_extensions import Self @@ -27,8 +27,9 @@ from .properties import PluginProperties -class QmakePluginProperties(PluginProperties): +class QmakePluginProperties(PluginProperties, frozen=True): """The part properties used by the qmake plugin.""" + plugin: Literal["qmake"] = "qmake" qmake_parameters: list[str] = [] qmake_project_file: str = "" @@ -37,21 +38,6 @@ class QmakePluginProperties(PluginProperties): # part properties required by the plugin source: str - @classmethod - def unmarshal(cls, data: dict[str, Any]) -> Self: - """Populate class attributes from the part specification. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - - :raise pydantic.ValidationError: If validation fails. - """ - plugin_data = extract_plugin_properties( - data, plugin_name="qmake", required=["source"] - ) - return cls(**plugin_data) - class QmakePlugin(Plugin): """The qmake plugin is useful for building qmake-based parts. diff --git a/craft_parts/plugins/rust_plugin.py b/craft_parts/plugins/rust_plugin.py index a8777f8e..084f796f 100644 --- a/craft_parts/plugins/rust_plugin.py +++ b/craft_parts/plugins/rust_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2022-2023 Canonical Ltd. +# Copyright 2022-2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -22,7 +22,7 @@ import re import subprocess from textwrap import dedent -from typing import Annotated, Any, cast +from typing import Annotated, Any, Literal, cast from overrides import override from pydantic import AfterValidator @@ -46,8 +46,9 @@ def _validate_list_is_unique(value: list) -> list: UniqueStrList = Annotated[list[str], AfterValidator(_validate_list_is_unique)] -class RustPluginProperties(PluginProperties): +class RustPluginProperties(PluginProperties, frozen=True): """The part properties used by the Rust plugin.""" + plugin: Literal["rust"] = "rust" # part properties required by the plugin rust_features: UniqueStrList = [] @@ -78,24 +79,6 @@ def validate_rust_channel(cls, value: str | None) -> str | None: return value raise ValueError(f"Invalid rust-channel: {value}") - @classmethod - @override - def unmarshal(cls, data: dict[str, Any]) -> "RustPluginProperties": - """Populate class attributes from the part specification. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - - :raise pydantic.ValidationError: If validation fails. - """ - plugin_data = extract_plugin_properties( - data, - plugin_name="rust", - required=["source"], - ) - return cls(**plugin_data) - class RustPluginEnvironmentValidator(validator.PluginEnvironmentValidator): """Check the execution environment for the Rust plugin. diff --git a/craft_parts/plugins/scons_plugin.py b/craft_parts/plugins/scons_plugin.py index 53fd0eb8..373e7cf9 100644 --- a/craft_parts/plugins/scons_plugin.py +++ b/craft_parts/plugins/scons_plugin.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2022 Canonical Ltd. +# Copyright 2022,2024 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -16,7 +16,7 @@ """The SCons plugin.""" -from typing import Any, cast +from typing import Any, Literal, cast from overrides import override @@ -27,30 +27,15 @@ from .properties import PluginProperties -class SConsPluginProperties(PluginProperties): +class SConsPluginProperties(PluginProperties, frozen=True): """The part properties used by the SCons plugin.""" + plugin: Literal["scons"] = "scons" scons_parameters: list[str] = [] # part properties required by the plugin source: str - @classmethod - @override - def unmarshal(cls, data: dict[str, Any]) -> "SConsPluginProperties": - """Populate make properties from the part specification. - - :param data: A dictionary containing part properties. - - :return: The populated plugin properties data object. - - :raise pydantic.ValidationError: If validation fails. - """ - plugin_data = extract_plugin_properties( - data, plugin_name="scons", required=["source"] - ) - return cls(**plugin_data) - class SConsPluginEnvironmentValidator(validator.PluginEnvironmentValidator): """Check the execution environment for the SCons plugin. diff --git a/tests/unit/plugins/test_base.py b/tests/unit/plugins/test_base.py index 534865d4..54d72dbf 100644 --- a/tests/unit/plugins/test_base.py +++ b/tests/unit/plugins/test_base.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -from typing import Any, cast +from typing import Any, ClassVar, Collection, Literal, cast import pytest from craft_parts.infos import PartInfo, ProjectInfo @@ -23,14 +23,11 @@ from craft_parts.plugins.base import extract_plugin_properties -class FooPluginProperties(PluginProperties): +class FooPluginProperties(PluginProperties, frozen=True): """Test plugin properties.""" + plugin: Literal["foo"] = "foo" - name: str - - @classmethod - def unmarshal(cls, data: dict[str, Any]): - return cls(name=data.get("foo-name", "nothing")) + foo_name: str class FooPlugin(Plugin): @@ -49,7 +46,7 @@ def get_build_environment(self) -> dict[str, str]: def get_build_commands(self) -> list[str]: options = cast(FooPluginProperties, self._options) - return ["hello", options.name] + return ["hello", options.foo_name] def test_plugin(new_dir): diff --git a/tests/unit/plugins/test_dump_plugin.py b/tests/unit/plugins/test_dump_plugin.py index 80561490..114fa85d 100644 --- a/tests/unit/plugins/test_dump_plugin.py +++ b/tests/unit/plugins/test_dump_plugin.py @@ -40,9 +40,8 @@ def setup_method_fixture(self, new_dir): self._plugin = DumpPlugin(properties=properties, part_info=part_info) def test_unmarshal_error(self): - with pytest.raises(ValueError) as raised: # noqa: PT011 + with pytest.raises(ValueError, match=r"source\n\s+Field required") as raised: DumpPlugin.properties_class.unmarshal({}) - assert str(raised.value) == "'source' is required by the dump plugin" def test_get_build_packages(self): assert self._plugin.get_build_packages() == set() diff --git a/tests/unit/plugins/test_plugins.py b/tests/unit/plugins/test_plugins.py index 06503331..36e5b270 100644 --- a/tests/unit/plugins/test_plugins.py +++ b/tests/unit/plugins/test_plugins.py @@ -54,6 +54,7 @@ class TestGetPlugin: ("make", MakePlugin, {"source": "."}), ("meson", MesonPlugin, {"source": "."}), ("nil", NilPlugin, {}), + ("nil", NilPlugin, {"source": "."}), ("npm", NpmPlugin, {"source": "."}), ("python", PythonPlugin, {"source": "."}), ("qmake", QmakePlugin, {"source": "."}), diff --git a/tests/unit/plugins/test_properties.py b/tests/unit/plugins/test_properties.py index ddf6dba7..40667530 100644 --- a/tests/unit/plugins/test_properties.py +++ b/tests/unit/plugins/test_properties.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -from typing import Any +from typing import Any, Literal from craft_parts.plugins import PluginProperties @@ -24,13 +24,10 @@ def test_properties_unmarshal(): assert isinstance(prop, PluginProperties) -class FooProperties(PluginProperties): +class FooProperties(PluginProperties, frozen=True): + plugin: Literal["foo"] = "foo" foo_parameters: list[str] | None = None - @classmethod - def unmarshal(cls, data: dict[str, Any]) -> "FooProperties": - return cls(**data) - def test_properties_marshal(): prop = FooProperties.unmarshal({"foo-parameters": ["foo", "bar"]}) diff --git a/tests/unit/plugins/test_validator.py b/tests/unit/plugins/test_validator.py index 0494b1f7..8e62a285 100644 --- a/tests/unit/plugins/test_validator.py +++ b/tests/unit/plugins/test_validator.py @@ -53,7 +53,7 @@ def part_info(new_dir): @dataclass -class FooPluginProperties(PluginProperties): +class FooPluginProperties(PluginProperties, frozen=True): """Test plugin properties.""" @classmethod