diff --git a/tests/mocked_plugins.py b/tests/mocked_plugins.py index c62cf80..5fcf1b2 100644 --- a/tests/mocked_plugins.py +++ b/tests/mocked_plugins.py @@ -18,6 +18,8 @@ class MockedEntryPoint: class MockedPluginA(PluginType): namespace = "test_namespace" # pyright: ignore[reportAssignmentType,reportIncompatibleMethodOverride] + is_build_plugin = True + def get_all_configs(self) -> list[VariantFeatureConfigType]: return [ VariantFeatureConfig("name1", ["val1a", "val1b", "val1c", "val1d"]), diff --git a/tests/test_api.py b/tests/test_api.py index c7ed8de..f82c821 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -49,6 +49,7 @@ from variantlib.constants import VARIANTS_JSON_SCHEMA_URL from variantlib.constants import VARIANTS_JSON_VARIANT_DATA_KEY from variantlib.constants import VariantsJsonDict +from variantlib.errors import PluginError from variantlib.errors import ValidationError from variantlib.models import provider as pconfig from variantlib.models import variant as vconfig @@ -764,3 +765,32 @@ def test_make_variant_dist_info_invalid_build_plugin() -> None: variant_info=vinfo, expand_build_plugin_properties=True, ) + + +def test_make_variant_dist_info_really_invalid_build_plugin() -> None: + vdesc = VariantDescription( + [ + VariantProperty("second_namespace", "name3", "val3a"), + ] + ) + plugin_api = "tests.mocked_plugins:MockedPluginB" + vinfo = VariantInfo( + namespace_priorities=["second_namespace"], + providers={ + "second_namespace": ProviderInfo( + plugin_api=plugin_api, + plugin_use=PluginUse.BUILD, + ) + }, + ) + + with pytest.raises( + PluginError, + match=r"Providers for namespaces {'second_namespace'} do not provide fixed " + r"supported configs, they cannot be used with plugin-use = 'build'", + ): + make_variant_dist_info( + vdesc, + variant_info=vinfo, + expand_build_plugin_properties=True, + ) diff --git a/variantlib/api.py b/variantlib/api.py index 6fa7296..02568dd 100644 --- a/variantlib/api.py +++ b/variantlib/api.py @@ -202,7 +202,9 @@ def make_variant_dist_info( filter_plugins=list(build_namespaces), include_build_plugins=True, ) as plugin_loader: - configs = plugin_loader.get_supported_configs().values() + configs = plugin_loader.get_supported_configs( + require_fixed=True + ).values() for config in configs: if config.namespace not in build_namespaces: diff --git a/variantlib/plugins/_subprocess.py b/variantlib/plugins/_subprocess.py index c1aaef5..43cfd7e 100644 --- a/variantlib/plugins/_subprocess.py +++ b/variantlib/plugins/_subprocess.py @@ -90,10 +90,27 @@ def main() -> int: help="Load specified plugin API", required=True, ) + parser.add_argument( + "--require-fixed", + action="store_true", + help="Require all plugins to provide fixed supported configs", + ) args = parser.parse_args() commands = json.load(sys.stdin) plugins = dict(zip(args.plugin_api, load_plugins(args.plugin_api), strict=True)) + if args.require_fixed: + non_fixed_plugins = { + plugin.namespace + for plugin in plugins.values() + if not getattr(plugin, "is_build_plugin", False) + } + if non_fixed_plugins: + raise TypeError( + f"Providers for namespaces {non_fixed_plugins} do not provide fixed " + f"supported configs, they cannot be used with plugin-use = 'build'" + ) + retval: dict[str, Any] = {} for command, command_args in commands.items(): if command == "namespaces": diff --git a/variantlib/plugins/loader.py b/variantlib/plugins/loader.py index c613f34..5e0258a 100644 --- a/variantlib/plugins/loader.py +++ b/variantlib/plugins/loader.py @@ -83,7 +83,10 @@ def __exit__( self._namespace_map = None def _call_subprocess( - self, plugin_apis: list[str], commands: dict[str, Any] + self, + plugin_apis: list[str], + commands: dict[str, Any], + args: Collection[str] = (), ) -> dict[str, Any]: with TemporaryDirectory(prefix="variantlib") as temp_dir: # Copy `variantlib/plugins/loader.py` into the temp_dir @@ -110,7 +113,7 @@ def _call_subprocess( ).read_bytes() ) - args = [] + args = [*args] for plugin_api in plugin_apis: args += ["--plugin-api", plugin_api] @@ -184,6 +187,7 @@ def _get_configs( self, method: Literal["get_all_configs", "get_supported_configs"], require_non_empty: bool, + require_fixed: bool, ) -> dict[str, ProviderConfig]: self._check_plugins_loaded() assert self._namespace_map is not None @@ -208,6 +212,7 @@ def _get_configs( configs = self._call_subprocess( list(self._namespace_map.keys()), {method: {}}, + args=["--require-fixed"] if require_fixed else [], )[method] for plugin_api, plugin_configs in configs.items(): @@ -233,13 +238,20 @@ def get_all_configs( self, ) -> dict[str, ProviderConfig]: """Get a mapping of namespaces to all valid configs""" - return self._get_configs("get_all_configs", require_non_empty=True) + return self._get_configs( + "get_all_configs", require_non_empty=True, require_fixed=False + ) def get_supported_configs( self, + require_fixed: bool = False, ) -> dict[str, ProviderConfig]: """Get a mapping of namespaces to supported configs""" - return self._get_configs("get_supported_configs", require_non_empty=False) + return self._get_configs( + "get_supported_configs", + require_non_empty=False, + require_fixed=require_fixed, + ) @property def plugin_api_values(self) -> dict[str, str]: diff --git a/variantlib/protocols.py b/variantlib/protocols.py index bdcf45a..1b3a5e9 100644 --- a/variantlib/protocols.py +++ b/variantlib/protocols.py @@ -67,6 +67,22 @@ def namespace(self) -> VariantNamespace: """Plugin namespace""" raise NotImplementedError + @property + def is_build_plugin(self) -> bool: + """ + Is this plugin valid for `plugin-use = "build"`? + + If this is True, then `get_supported_configs()` must always + return the same values, irrespective of the platform used. + This permits the plugin to be used with `plugin-use = "build"`, + where the supported properties are recorded at build time. + + If the value of `get_supported_configs()` may change in any way + depending on the platform used, then it must be False + (the default). + """ + return False + @abstractmethod def get_all_configs(self) -> list[VariantFeatureConfigType]: """Get all valid configs for the plugin"""