From c01c5c0bd36552217e36cbc03c5291c2ce8e415a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Wed, 26 Mar 2025 17:43:48 +0100 Subject: [PATCH 1/2] Add an API to describe variant using labels Add a minimal API that lets plugins process the variant description and add short labels that could be used to describe the wheel. The labels are entirely optional -- plugins may not return any, and clients may ignore them. The design also assumes that the client is responsible for choosing how many labels to use before truncating -- though I suppose I'll add a helper function to `variantlib` for that purpose. The design assumes that plugin get complete unfiltered `VariantDescription` -- and therefore also see variant metadata from other plugins. I don't think that's really a problem, though it assumes that the plugin must compare both against provider and key names. At this point, the protocol makes the API mandatory, even if it would only return an empty list unconditionally. Perhaps we should make the function optional somehow instead. --- tests/test_plugins.py | 61 ++++++++++++++++++++++++++++++++++++++++++- variantlib/base.py | 8 ++++++ variantlib/plugins.py | 9 +++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 73869db..4bb9f73 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -5,6 +5,7 @@ from variantlib.base import PluginBase from variantlib.config import KeyConfig, ProviderConfig +from variantlib.meta import VariantDescription, VariantMeta from variantlib.plugins import PluginLoader @@ -20,6 +21,12 @@ def get_supported_configs(self) -> Optional[ProviderConfig]: ], ) + def get_variant_labels(self, variant_desc: VariantDescription) -> list[str]: + for meta in variant_desc: + if meta.namespace == self.namespace and meta.key == "key1": + return [meta.value.removeprefix("val")] + return [] + # NB: this plugin deliberately does not inherit from PluginBase # to test that we don't rely on that inheritance @@ -34,13 +41,25 @@ def get_supported_configs(self) -> Optional[ProviderConfig]: ], ) + def get_variant_labels(self, variant_desc: VariantDescription) -> list[str]: + if VariantMeta(self.namespace, "key3", "val3a") in variant_desc: + return ["sec"] + return [] + class MockedPluginC(PluginBase): - namespace = "incompatible_plugin" + namespace = "other_plugin" def get_supported_configs(self) -> Optional[ProviderConfig]: return None + def get_variant_labels(self, variant_desc: VariantDescription) -> list[str]: + ret = [] + for meta in variant_desc: + if meta.namespace == self.namespace and meta.value == "on": + ret.append(meta.key) + return ret + class ClashingPlugin(PluginBase): namespace = "test_plugin" @@ -48,6 +67,13 @@ class ClashingPlugin(PluginBase): def get_supported_configs(self) -> Optional[ProviderConfig]: return None + def get_variant_labels(self, variant_desc: VariantDescription) -> list[str]: + ret = [] + for meta in variant_desc: + if meta.namespace == self.namespace and meta.value == "on": + ret.append(meta.key) + return ret + @dataclass class MockedDistribution: @@ -137,3 +163,36 @@ def test_namespace_clash(mocker): assert "same namespace test_plugin" in str(exc) assert "test-plugin" in str(exc) assert "clashing-plugin" in str(exc) + + +@pytest.mark.parametrize("variant_desc,expected", +[ + (VariantDescription([ + VariantMeta("test_plugin", "key1", "val1a"), + VariantMeta("test_plugin", "key2", "val2b"), + VariantMeta("second_plugin", "key3", "val3a"), + VariantMeta("other_plugin", "flag2", "on"), + ]), ["1a", "sec", "flag2"]), + (VariantDescription([ + # note that VariantMetas don't actually have to be supported + # by the system in question -- we could be cross-building + # for another system + VariantMeta("test_plugin", "key1", "val1f"), + VariantMeta("test_plugin", "key2", "val2b"), + VariantMeta("second_plugin", "key3", "val3a"), + ]), ["1f", "sec"]), + (VariantDescription([ + VariantMeta("test_plugin", "key2", "val2b"), + VariantMeta("second_plugin", "key3", "val3a"), + ]), ["sec"]), + (VariantDescription([ + VariantMeta("test_plugin", "key2", "val2b"), + ]), []), + (VariantDescription([ + VariantMeta("test_plugin", "key2", "val2b"), + VariantMeta("other_plugin", "flag1", "on"), + VariantMeta("other_plugin", "flag2", "on"), + ]), ["flag1", "flag2"]), +]) +def test_get_variant_labels(mocked_plugin_loader, variant_desc, expected): + assert mocked_plugin_loader.get_variant_labels(variant_desc) == expected diff --git a/variantlib/base.py b/variantlib/base.py index 80b17c1..aa9d49a 100644 --- a/variantlib/base.py +++ b/variantlib/base.py @@ -2,6 +2,7 @@ from typing import Protocol, runtime_checkable from variantlib.config import ProviderConfig +from variantlib.meta import VariantDescription @runtime_checkable @@ -17,6 +18,10 @@ def get_supported_configs(self) -> ProviderConfig: """Get supported configs for the current system""" ... + def get_variant_labels(self, variant_desc: VariantDescription) -> list[str]: + """Get list of short labels to describe the variant""" + ... + class PluginBase(ABC): """An abstract base class that can be used to implement plugins""" @@ -26,3 +31,6 @@ def namespace(self) -> str: ... @abstractmethod def get_supported_configs(self) -> ProviderConfig: ... + + def get_variant_labels(self, variant_desc: VariantDescription) -> list[str]: + return [] diff --git a/variantlib/plugins.py b/variantlib/plugins.py index 8fe7068..22ab5f9 100644 --- a/variantlib/plugins.py +++ b/variantlib/plugins.py @@ -92,3 +92,12 @@ def get_dist_name_mapping(self) -> dict[str, str]: """Get a mapping from plugin names to distribution names""" return self._dist_names + + def get_variant_labels(self, variant_desc: VariantDescription) -> list[str]: + """Get list of short labels to describe the variant""" + + labels = [] + for plugin in self._plugins.values(): + labels += plugin.get_variant_labels(variant_desc) + + return labels From 625a64ce066e0e17a04c26cdfb6c7ad92bcbfc36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Wed, 26 Mar 2025 17:51:43 +0100 Subject: [PATCH 2/2] Make get_variant_labels() method optional --- tests/test_plugins.py | 12 ++++++++++++ variantlib/base.py | 1 - variantlib/plugins.py | 10 ++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 4bb9f73..2cb536f 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -61,6 +61,13 @@ def get_variant_labels(self, variant_desc: VariantDescription) -> list[str]: return ret +class MockedPluginD: + namespace = "plugin_without_labels" + + def get_supported_configs(self) -> Optional[ProviderConfig]: + return None + + class ClashingPlugin(PluginBase): namespace = "test_plugin" @@ -113,6 +120,11 @@ def mocked_plugin_loader(session_mocker): value="tests.test_plugins:MockedPluginC", plugin=MockedPluginC, ), + MockedEntryPoint( + name="no_labels", + value="tests.test_plugins:MockedPluginD", + plugin=MockedPluginD, + ), ] yield PluginLoader() diff --git a/variantlib/base.py b/variantlib/base.py index aa9d49a..480a311 100644 --- a/variantlib/base.py +++ b/variantlib/base.py @@ -5,7 +5,6 @@ from variantlib.meta import VariantDescription -@runtime_checkable class PluginType(Protocol): """A protocol for plugin classes""" diff --git a/variantlib/plugins.py b/variantlib/plugins.py index 22ab5f9..ca3e2c8 100644 --- a/variantlib/plugins.py +++ b/variantlib/plugins.py @@ -44,7 +44,12 @@ def load_plugins(self) -> None: # Instantiate the plugin plugin_instance = plugin_class() - assert isinstance(plugin_instance, PluginType) + + # Check for obligatory members + for attr in ("namespace", "get_supported_configs"): + assert hasattr( + plugin_instance, attr + ), f"Plugin is missing required member: {attr}" except Exception: logging.exception("An unknown error happened - Ignoring plugin") else: @@ -98,6 +103,7 @@ def get_variant_labels(self, variant_desc: VariantDescription) -> list[str]: labels = [] for plugin in self._plugins.values(): - labels += plugin.get_variant_labels(variant_desc) + if hasattr(plugin, "get_variant_labels"): + labels += plugin.get_variant_labels(variant_desc) return labels