From be463e2bbae560614aceb20491c1a960c2437c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 8 Sep 2026 16:16:26 +0200 Subject: [PATCH 1/5] Fix `make-variant` command to account for multi-value violations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- variantlib/commands/make_variant.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/variantlib/commands/make_variant.py b/variantlib/commands/make_variant.py index 3ea16b3..3d45774 100644 --- a/variantlib/commands/make_variant.py +++ b/variantlib/commands/make_variant.py @@ -209,6 +209,14 @@ def _make_variant( if venv is not None else None, ) + if vdesc_valid.multi_value_violations: + invalid_str = ", ".join( + x.to_str() for x in vdesc_valid.multi_value_violations + ) + raise ValidationError( + "The following variant features specify multiple values " + f"while only one is allowed: {invalid_str}" + ) if vdesc_valid.invalid_properties: invalid_str = ", ".join( x.to_str() for x in vdesc_valid.invalid_properties @@ -225,6 +233,7 @@ def _make_variant( "The following variant properties use namespaces that are not " f"provided by any installed plugin: {unknown_str}" ) + assert vdesc_valid.is_valid() else: # Create a null variant vdesc = VariantDescription() From 67a62d53fb8b5a91334d9632bc7d1546ee67349e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 8 Sep 2026 16:27:23 +0200 Subject: [PATCH 2/5] Disallow empty namespace list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- tests/test_api.py | 25 +++++++---- tests/test_pyproject_toml.py | 74 ++++++++++++++++++++++++++----- tests/test_variants_json.py | 29 +++++++++--- variantlib/models/variant_info.py | 2 + 4 files changed, 103 insertions(+), 27 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 6dd1dda..0135675 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -42,7 +42,6 @@ from variantlib.errors import ValidationError from variantlib.models import provider as pconfig from variantlib.models import variant as vconfig -from variantlib.models.configuration import VariantConfiguration as VConfigurationModel from variantlib.models.provider import ProviderConfig from variantlib.models.provider import VariantFeatureConfig from variantlib.models.variant import VariantDescription @@ -185,9 +184,6 @@ def test_get_variants_by_priority_roundtrip_fuzz( mocker: MockerFixture, configs: list[ProviderConfig] ) -> None: namespace_priorities = list({provider_cfg.namespace for provider_cfg in configs}) - mocker.patch( - "variantlib.configuration.VariantConfiguration.get_config" - ).return_value = VConfigurationModel(namespace_priorities=namespace_priorities) def get_or_skip_combinations() -> Generator[VariantDescription]: for i, x in enumerate(get_combinations(configs, namespace_priorities)): @@ -198,9 +194,24 @@ def get_or_skip_combinations() -> Generator[VariantDescription]: combinations: list[VariantDescription] = [*list(get_or_skip_combinations())] variants_json = { + VARIANT_INFO_DEFAULT_PRIO_KEY: { + VARIANT_INFO_NAMESPACE_KEY: namespace_priorities, + }, + VARIANT_INFO_PROVIDER_DATA_KEY: { + provider_cfg.namespace: { + VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: { + feature_cfg.name: feature_cfg.values + for feature_cfg in provider_cfg.configs + }, + VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY: [ + feature_cfg.name for feature_cfg in provider_cfg.configs + ], + } + for provider_cfg in configs + }, VARIANTS_JSON_VARIANT_DATA_KEY: { get_variant_label(vdesc): vdesc.to_dict() for vdesc in combinations - } + }, } if (typed_variants_json := trycast(VariantsJsonDict, variants_json)) is None: @@ -208,10 +219,6 @@ def get_or_skip_combinations() -> Generator[VariantDescription]: f"Did not conform the `VariantsJsonDict` format: {variants_json}" ) - mocker.patch( - "variantlib.plugins.loader.BasePluginLoader.get_supported_configs" - ).return_value = {provider_cfg.namespace: provider_cfg for provider_cfg in configs} - assert get_variants_by_priority(variants_json=typed_variants_json) == combinations diff --git a/tests/test_pyproject_toml.py b/tests/test_pyproject_toml.py index 4a4a319..3744527 100644 --- a/tests/test_pyproject_toml.py +++ b/tests/test_pyproject_toml.py @@ -103,7 +103,14 @@ def test_invalid_table_type(table: str) -> None: match=rf"{PYPROJECT_TOML_TOP_KEY}\.{table}: expected dict\[str, " r"typing\.Any\], got ", ): - VariantPyProjectToml({PYPROJECT_TOML_TOP_KEY: {table: [123]}}) + VariantPyProjectToml( + { + PYPROJECT_TOML_TOP_KEY: { + VARIANT_INFO_DEFAULT_PRIO_KEY: {VARIANT_INFO_NAMESPACE_KEY: ["x"]}, + table: [123], + } + } + ) @pytest.mark.parametrize( @@ -157,7 +164,10 @@ def test_invalid_provider_namespace() -> None: VariantPyProjectToml( { PYPROJECT_TOML_TOP_KEY: { - VARIANT_INFO_PROVIDER_DATA_KEY: {"invalid namespace": {}} + VARIANT_INFO_DEFAULT_PRIO_KEY: { + VARIANT_INFO_NAMESPACE_KEY: ["ns"], + }, + VARIANT_INFO_PROVIDER_DATA_KEY: {"invalid namespace": {}}, } } ) @@ -170,7 +180,14 @@ def test_invalid_provider_table_type() -> None: r"ns: expected dict\[str, typing.Any\], got ", ): VariantPyProjectToml( - {PYPROJECT_TOML_TOP_KEY: {VARIANT_INFO_PROVIDER_DATA_KEY: {"ns": [123]}}} + { + PYPROJECT_TOML_TOP_KEY: { + VARIANT_INFO_DEFAULT_PRIO_KEY: { + VARIANT_INFO_NAMESPACE_KEY: ["ns"], + }, + VARIANT_INFO_PROVIDER_DATA_KEY: {"ns": [123]}, + } + } ) @@ -190,7 +207,10 @@ def test_invalid_provider_data_type(key: str, expected: str) -> None: VariantPyProjectToml( { PYPROJECT_TOML_TOP_KEY: { - VARIANT_INFO_PROVIDER_DATA_KEY: {"ns": {key: 123}} + VARIANT_INFO_DEFAULT_PRIO_KEY: { + VARIANT_INFO_NAMESPACE_KEY: ["ns"], + }, + VARIANT_INFO_PROVIDER_DATA_KEY: {"ns": {key: 123}}, } } ) @@ -205,6 +225,9 @@ def test_invalid_provider_requires() -> None: VariantPyProjectToml( { PYPROJECT_TOML_TOP_KEY: { + VARIANT_INFO_DEFAULT_PRIO_KEY: { + VARIANT_INFO_NAMESPACE_KEY: ["ns"], + }, VARIANT_INFO_PROVIDER_DATA_KEY: { "ns": { VARIANT_INFO_PROVIDER_REQUIRES_KEY: [ @@ -212,7 +235,7 @@ def test_invalid_provider_requires() -> None: "", ] } - } + }, } } ) @@ -228,9 +251,12 @@ def test_invalid_provider_plugin_api() -> None: VariantPyProjectToml( { PYPROJECT_TOML_TOP_KEY: { + VARIANT_INFO_DEFAULT_PRIO_KEY: { + VARIANT_INFO_NAMESPACE_KEY: ["ns"], + }, VARIANT_INFO_PROVIDER_DATA_KEY: { "ns": {VARIANT_INFO_PROVIDER_PLUGIN_API_KEY: "foo:bar:baz"} - } + }, } } ) @@ -266,17 +292,24 @@ def test_missing_namespace_priority() -> None: match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_DEFAULT_PRIO_KEY}\." rf"{VARIANT_INFO_NAMESPACE_KEY} must specify the same namespaces as " rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_PROVIDER_DATA_KEY} " - r"keys; currently: set\(\) vs\. \{'ns'\}", + r"keys; currently: \{'foo'\} vs\.", ): VariantPyProjectToml( { PYPROJECT_TOML_TOP_KEY: { + VARIANT_INFO_DEFAULT_PRIO_KEY: { + VARIANT_INFO_NAMESPACE_KEY: ["foo"], + }, VARIANT_INFO_PROVIDER_DATA_KEY: { - "ns": { + "foo": { VARIANT_INFO_PROVIDER_REQUIRES_KEY: ["frobnicate"], VARIANT_INFO_PROVIDER_PLUGIN_API_KEY: "foo:Plugin", - } - } + }, + "bar": { + VARIANT_INFO_PROVIDER_REQUIRES_KEY: ["frobnicate"], + VARIANT_INFO_PROVIDER_PLUGIN_API_KEY: "foo:Plugin", + }, + }, } } ) @@ -306,7 +339,14 @@ def test_extra_default_priority_key() -> None: r"unexpected subkeys: \{'foo'\}", ): VariantPyProjectToml( - {PYPROJECT_TOML_TOP_KEY: {VARIANT_INFO_DEFAULT_PRIO_KEY: {"foo": {}}}} + { + PYPROJECT_TOML_TOP_KEY: { + VARIANT_INFO_DEFAULT_PRIO_KEY: { + VARIANT_INFO_NAMESPACE_KEY: ["ns"], + "foo": {}, + } + } + } ) @@ -319,13 +359,14 @@ def test_extra_provider_data_key() -> None: VariantPyProjectToml( { PYPROJECT_TOML_TOP_KEY: { + VARIANT_INFO_DEFAULT_PRIO_KEY: {VARIANT_INFO_NAMESPACE_KEY: ["ns"]}, VARIANT_INFO_PROVIDER_DATA_KEY: { "ns": { VARIANT_INFO_PROVIDER_PLUGIN_API_KEY: "frobnicate:Plugin", VARIANT_INFO_PROVIDER_REQUIRES_KEY: ["foo"], "foo": {}, } - } + }, } } ) @@ -556,3 +597,12 @@ def test_requires_and_feature_order(requires_key: str) -> None: } } ) + + +def test_no_namespaces() -> None: + with pytest.raises( + ValidationError, + match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_DEFAULT_PRIO_KEY}\." + rf"{VARIANT_INFO_NAMESPACE_KEY}: no namespace specified", + ): + VariantPyProjectToml({PYPROJECT_TOML_TOP_KEY: {}}) diff --git a/tests/test_variants_json.py b/tests/test_variants_json.py index e9a6fae..dac6bfe 100644 --- a/tests/test_variants_json.py +++ b/tests/test_variants_json.py @@ -231,10 +231,6 @@ def test_validate_variants_json() -> None: } -def test_validate_variants_json_empty() -> None: - assert VariantsJson({VARIANTS_JSON_VARIANT_DATA_KEY: {}}).variants == {} - - @pytest.mark.parametrize("cls", [VariantPyProjectToml, VariantsJson]) def test_conversion(cls: type[VariantPyProjectToml | VariantsJson]) -> None: json_file = Path( @@ -484,13 +480,33 @@ def test_null_variant_label(): match=rf"{NULL_VARIANT_LABEL!r} label can be used only for the null variant", ): VariantsJson( - {VARIANTS_JSON_VARIANT_DATA_KEY: {NULL_VARIANT_LABEL: {"x": {"y": ["z"]}}}} + { + VARIANT_INFO_DEFAULT_PRIO_KEY: {VARIANT_INFO_NAMESPACE_KEY: ["x"]}, + VARIANT_INFO_PROVIDER_DATA_KEY: { + "x": { + VARIANT_INFO_PROVIDER_REQUIRES_KEY: ["dummy-dep"], + } + }, + VARIANTS_JSON_VARIANT_DATA_KEY: { + NULL_VARIANT_LABEL: {"x": {"y": ["z"]}} + }, + } ) with pytest.raises( ValidationError, match=rf"Null variant must always use {NULL_VARIANT_LABEL!r} label", ): - VariantsJson({VARIANTS_JSON_VARIANT_DATA_KEY: {"zuul": {}}}) + VariantsJson( + { + VARIANT_INFO_DEFAULT_PRIO_KEY: {VARIANT_INFO_NAMESPACE_KEY: ["x"]}, + VARIANT_INFO_PROVIDER_DATA_KEY: { + "x": { + VARIANT_INFO_PROVIDER_REQUIRES_KEY: ["dummy-dep"], + } + }, + VARIANTS_JSON_VARIANT_DATA_KEY: {"zuul": {}}, + } + ) def test_build_requires(): @@ -501,6 +517,7 @@ def test_build_requires(): ): VariantsJson( { + VARIANT_INFO_DEFAULT_PRIO_KEY: {VARIANT_INFO_NAMESPACE_KEY: ["x"]}, VARIANT_INFO_PROVIDER_DATA_KEY: { "x": { VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY: ["example"], diff --git a/variantlib/models/variant_info.py b/variantlib/models/variant_info.py index 8d0a8af..7635e9b 100644 --- a/variantlib/models/variant_info.py +++ b/variantlib/models/variant_info.py @@ -133,6 +133,8 @@ def _process_common(self, validator: KeyTrackingValidator) -> None: VARIANT_INFO_NAMESPACE_KEY, list[VariantNamespace], [] ) as namespace_priorities, ): + if not namespace_priorities: + raise ValidationError(f"{validator.key}: no namespace specified") validator.list_matches_re(VALIDATION_NAMESPACE_REGEX) self.namespace_priorities = list(namespace_priorities) From 454dd1a1acebfe1118b06e222c5f57f5ca0383f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Wed, 9 Sep 2026 16:24:59 +0200 Subject: [PATCH 3/5] Remove `enable-if` key from provider information MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- .../dummy_project-1.0.0-variants.json | 1 - tests/commands/test_update_pyproject_toml.py | 5 ----- tests/plugins/test_loader.py | 8 -------- tests/test_api.py | 2 -- tests/test_pyproject_toml.py | 5 ----- tests/test_variants_json.py | 17 ----------------- variantlib/constants.py | 3 --- variantlib/models/variant_info.py | 10 ---------- variantlib/plugins/loader.py | 11 ----------- variantlib/variants_json.py | 3 --- 10 files changed, 65 deletions(-) diff --git a/tests/artifacts/variant_json_files/dummy_project-1.0.0-variants.json b/tests/artifacts/variant_json_files/dummy_project-1.0.0-variants.json index b5dc51f..29bbad1 100644 --- a/tests/artifacts/variant_json_files/dummy_project-1.0.0-variants.json +++ b/tests/artifacts/variant_json_files/dummy_project-1.0.0-variants.json @@ -11,7 +11,6 @@ "requires": [ "provider-fictional-hw == 1.0.0" ], - "enable-if": "python_version >= '3.12'", "plugin-api": "provider_fictional_hw.plugin:FictionalHWPlugin" }, "fictional_tech": { diff --git a/tests/commands/test_update_pyproject_toml.py b/tests/commands/test_update_pyproject_toml.py index 41eb7dc..a684f78 100644 --- a/tests/commands/test_update_pyproject_toml.py +++ b/tests/commands/test_update_pyproject_toml.py @@ -9,7 +9,6 @@ from variantlib.constants import VARIANT_INFO_DEFAULT_PRIO_KEY from variantlib.constants import VARIANT_INFO_NAMESPACE_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_DATA_KEY -from variantlib.constants import VARIANT_INFO_PROVIDER_ENABLE_IF_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_REQUIRES_KEY @@ -34,7 +33,6 @@ def test_update_pyproject_toml( VARIANT_INFO_PROVIDER_DATA_KEY: { "test_namespace": { VARIANT_INFO_PROVIDER_REQUIRES_KEY: ["frobnicate", "barnicate"], - VARIANT_INFO_PROVIDER_ENABLE_IF_KEY: "python_version >= '3.11'", VARIANT_INFO_PROVIDER_PLUGIN_API_KEY: "wrong_value", }, "foo": { @@ -77,9 +75,6 @@ def test_update_pyproject_toml( "test_namespace", ] ) - del toml_data[PYPROJECT_TOML_TOP_KEY][VARIANT_INFO_PROVIDER_DATA_KEY][ - "test_namespace" - ][VARIANT_INFO_PROVIDER_ENABLE_IF_KEY] toml_data[PYPROJECT_TOML_TOP_KEY][VARIANT_INFO_PROVIDER_DATA_KEY]["test_namespace"][ VARIANT_INFO_PROVIDER_REQUIRES_KEY ].clear() diff --git a/tests/plugins/test_loader.py b/tests/plugins/test_loader.py index c9e1918..056bf02 100644 --- a/tests/plugins/test_loader.py +++ b/tests/plugins/test_loader.py @@ -368,17 +368,9 @@ def test_load_plugin_invalid_arg() -> None: plugin_api="tests.mocked_plugins:MockedPluginA", ), "second_namespace": ProviderInfo( - # always true - enable_if="python_version >= '3.10'", requires=["variantlib"], plugin_api="tests.mocked_plugins:MockedPluginB", ), - "incompatible_namespace": ProviderInfo( - # always false (hopefully) - enable_if='platform_machine == "frobnicator"', - requires=["variantlib"], - plugin_api="tests.mocked_plugins:MockedPluginC", - ), "one_more": ProviderInfo( requires=["variantlib"], plugin_api="tests.mocked_plugins:NoSuchClass", diff --git a/tests/test_api.py b/tests/test_api.py index 0135675..6ceca63 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -28,7 +28,6 @@ from variantlib.constants import VARIANT_INFO_NAMESPACE_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_DATA_KEY -from variantlib.constants import VARIANT_INFO_PROVIDER_ENABLE_IF_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY @@ -372,7 +371,6 @@ def test_make_variant_dist_info( { "ns1": { VARIANT_INFO_PROVIDER_REQUIRES_KEY: ["ns1-provider >= 1.2.3"], - VARIANT_INFO_PROVIDER_ENABLE_IF_KEY: "python_version >= '3.12'", VARIANT_INFO_PROVIDER_PLUGIN_API_KEY: "ns1_provider.plugin:NS1Plugin", # noqa: E501 }, "ns2": { diff --git a/tests/test_pyproject_toml.py b/tests/test_pyproject_toml.py index 3744527..e50e1c3 100644 --- a/tests/test_pyproject_toml.py +++ b/tests/test_pyproject_toml.py @@ -9,7 +9,6 @@ from variantlib.constants import VARIANT_INFO_NAMESPACE_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_DATA_KEY -from variantlib.constants import VARIANT_INFO_PROVIDER_ENABLE_IF_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY @@ -39,7 +38,6 @@ [{PYPROJECT_TOML_TOP_KEY}.{VARIANT_INFO_PROVIDER_DATA_KEY}.ns1] {VARIANT_INFO_PROVIDER_REQUIRES_KEY} = ["ns1-provider >= 1.2.3"] -{VARIANT_INFO_PROVIDER_ENABLE_IF_KEY} = "python_version >= '3.12'" {VARIANT_INFO_PROVIDER_PLUGIN_API_KEY} = "ns1_provider.plugin:NS1Plugin" [{PYPROJECT_TOML_TOP_KEY}.{VARIANT_INFO_PROVIDER_DATA_KEY}.ns2] @@ -67,7 +65,6 @@ def test_pyproject_toml() -> None: assert pyproj.providers == { "ns1": ProviderInfo( requires=["ns1-provider >= 1.2.3"], - enable_if="python_version >= '3.12'", plugin_api="ns1_provider.plugin:NS1Plugin", ), "ns2": ProviderInfo( @@ -380,14 +377,12 @@ def test_conversion(cls: type[VariantPyProjectToml | VariantsJson]) -> None: # Mangle the original to ensure everything was copied pyproj.namespace_priorities.append("ns4") pyproj.providers["ns4"] = ProviderInfo(requires=["foo"], plugin_api="foo:bar") - pyproj.providers["ns1"].enable_if = None pyproj.providers["ns2"].requires.append("frobnicate") assert converted.namespace_priorities == ["ns1", "ns2", "ns3"] assert converted.providers == { "ns1": ProviderInfo( requires=["ns1-provider >= 1.2.3"], - enable_if="python_version >= '3.12'", plugin_api="ns1_provider.plugin:NS1Plugin", ), "ns2": ProviderInfo( diff --git a/tests/test_variants_json.py b/tests/test_variants_json.py index dac6bfe..574517c 100644 --- a/tests/test_variants_json.py +++ b/tests/test_variants_json.py @@ -11,7 +11,6 @@ from variantlib.constants import VARIANT_INFO_NAMESPACE_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_DATA_KEY -from variantlib.constants import VARIANT_INFO_PROVIDER_ENABLE_IF_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_REQUIRES_KEY from variantlib.constants import VARIANTS_JSON_SCHEMA_KEY @@ -221,7 +220,6 @@ def test_validate_variants_json() -> None: assert variants_json.providers == { "fictional_hw": ProviderInfo( requires=["provider-fictional-hw == 1.0.0"], - enable_if="python_version >= '3.12'", plugin_api="provider_fictional_hw.plugin:FictionalHWPlugin", ), "fictional_tech": ProviderInfo( @@ -249,14 +247,12 @@ def test_conversion(cls: type[VariantPyProjectToml | VariantsJson]) -> None: # Mangle variants_json to ensure everything was copied variants_json.namespace_priorities.append("ns") variants_json.providers["ns"] = ProviderInfo(requires=["bar"], plugin_api="foo:bar") - variants_json.providers["fictional_hw"].enable_if = None variants_json.providers["fictional_tech"].requires.append("frobnicate") assert converted.namespace_priorities == ["fictional_hw", "fictional_tech"] assert converted.providers == { "fictional_hw": ProviderInfo( requires=["provider-fictional-hw == 1.0.0"], - enable_if="python_version >= '3.12'", plugin_api="provider_fictional_hw.plugin:FictionalHWPlugin", ), "fictional_tech": ProviderInfo( @@ -277,7 +273,6 @@ def test_to_str() -> None: providers={ "ns1": ProviderInfo( requires=["ns1-pkg >= 1.0.0", "ns1-dep"], - enable_if="python_version >= '3.12'", plugin_api="ns1_pkg:Plugin", ), "ns2": ProviderInfo(requires=["ns2_pkg"], plugin_api="ns2_pkg:Plugin"), @@ -309,7 +304,6 @@ def test_to_str() -> None: VARIANT_INFO_PROVIDER_DATA_KEY: { "ns1": { VARIANT_INFO_PROVIDER_REQUIRES_KEY: ["ns1-pkg >= 1.0.0", "ns1-dep"], - VARIANT_INFO_PROVIDER_ENABLE_IF_KEY: "python_version >= '3.12'", VARIANT_INFO_PROVIDER_PLUGIN_API_KEY: "ns1_pkg:Plugin", }, "ns2": { @@ -345,7 +339,6 @@ def test_merge_variants() -> None: }, "b": { VARIANT_INFO_PROVIDER_REQUIRES_KEY: ["b"], - VARIANT_INFO_PROVIDER_ENABLE_IF_KEY: "python_version > '3.12'", VARIANT_INFO_PROVIDER_PLUGIN_API_KEY: "b:B", }, } @@ -441,16 +434,6 @@ def test_merge_variants() -> None: v1.merge(VariantsJson(_json_data)) # Test for mismatches in provider information. - _json_data = copy.deepcopy(json_a) - del _json_data[VARIANT_INFO_PROVIDER_DATA_KEY]["b"][ - VARIANT_INFO_PROVIDER_ENABLE_IF_KEY - ] - with pytest.raises( - ValidationError, - match=r"Inconsistency in providers\.b", - ): - v1.merge(VariantsJson(_json_data)) - _json_data = copy.deepcopy(json_b) _json_data[VARIANT_INFO_PROVIDER_DATA_KEY]["a"][ VARIANT_INFO_PROVIDER_PLUGIN_API_KEY diff --git a/variantlib/constants.py b/variantlib/constants.py index 81d4a90..e3b8053 100644 --- a/variantlib/constants.py +++ b/variantlib/constants.py @@ -13,7 +13,6 @@ VARIANT_INFO_NAMESPACE_KEY: Literal["namespace"] = "namespace" VARIANT_INFO_PROVIDER_DATA_KEY: Literal["providers"] = "providers" VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY: Literal["build-requires"] = "build-requires" -VARIANT_INFO_PROVIDER_ENABLE_IF_KEY: Literal["enable-if"] = "enable-if" VARIANT_INFO_PROVIDER_OPTIONAL_KEY: Literal["optional"] = "optional" VARIANT_INFO_PROVIDER_PLUGIN_API_KEY: Literal["plugin-api"] = "plugin-api" VARIANT_INFO_PROVIDER_REQUIRES_KEY: Literal["requires"] = "requires" @@ -54,7 +53,6 @@ re.VERBOSE, ) -VALIDATION_PROVIDER_ENABLE_IF_REGEX = re.compile(r"[\S ]+") VALIDATION_PROVIDER_PLUGIN_API_REGEX = re.compile( r""" (?P [\w.]+) @@ -113,7 +111,6 @@ class PriorityJsonDict(TypedDict, total=False): "ProviderPluginJsonDict", { "build-requires": list[str], - "enable-if": str, "optional": bool, "plugin-api": str, "requires": list[str], diff --git a/variantlib/models/variant_info.py b/variantlib/models/variant_info.py index 7635e9b..70966b1 100644 --- a/variantlib/models/variant_info.py +++ b/variantlib/models/variant_info.py @@ -9,7 +9,6 @@ from variantlib.constants import VALIDATION_FEATURE_NAME_REGEX from variantlib.constants import VALIDATION_NAMESPACE_REGEX -from variantlib.constants import VALIDATION_PROVIDER_ENABLE_IF_REGEX from variantlib.constants import VALIDATION_PROVIDER_PLUGIN_API_REGEX from variantlib.constants import VALIDATION_PROVIDER_REQUIRES_REGEX from variantlib.constants import VALIDATION_VALUE_REGEX @@ -17,7 +16,6 @@ from variantlib.constants import VARIANT_INFO_NAMESPACE_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_DATA_KEY -from variantlib.constants import VARIANT_INFO_PROVIDER_ENABLE_IF_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY @@ -35,7 +33,6 @@ @dataclass class ProviderInfo: plugin_api: str | None = None - enable_if: str | None = None optional: bool = False requires: list[str] = field(default_factory=list) static_properties: dict[VariantFeatureName, list[VariantFeatureValue]] = field( @@ -82,7 +79,6 @@ def copy_as_kwargs(self) -> dict[str, Any]: "namespace_priorities": list(self.namespace_priorities), "providers": { namespace: ProviderInfo( - enable_if=provider_data.enable_if, optional=provider_data.optional, plugin_api=provider_data.plugin_api, requires=list(provider_data.requires), @@ -159,11 +155,6 @@ def _process_common(self, validator: KeyTrackingValidator) -> None: ) as provider_plugin_api: if provider_plugin_api is not None: validator.matches_re(VALIDATION_PROVIDER_PLUGIN_API_REGEX) - with validator.get( - VARIANT_INFO_PROVIDER_ENABLE_IF_KEY, str, None - ) as provider_enable_if: - if provider_enable_if is not None: - validator.matches_re(VALIDATION_PROVIDER_ENABLE_IF_REGEX) with validator.get( VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY, list[VariantFeatureName], @@ -235,7 +226,6 @@ def _process_common(self, validator: KeyTrackingValidator) -> None: ) self.providers[namespace] = ProviderInfo( - enable_if=provider_enable_if, optional=provider_optional, plugin_api=provider_plugin_api, requires=list(provider_requires), diff --git a/variantlib/plugins/loader.py b/variantlib/plugins/loader.py index 3ea32e5..24e9dbd 100644 --- a/variantlib/plugins/loader.py +++ b/variantlib/plugins/loader.py @@ -17,7 +17,6 @@ from typing import Any from typing import cast -from packaging.markers import Marker from packaging.markers import default_environment from variantlib.constants import VALIDATION_PROVIDER_PLUGIN_API_REGEX @@ -354,16 +353,6 @@ def _plugin_enabled(self, namespace: str, provider_data: ProviderInfo) -> bool: ) return False - if (marker := provider_data.enable_if) is not None: - if not Marker(marker).evaluate(self._environment): - logger.debug( - "The variant provider plugin corresponding " - "to namespace `%(ns)s` has been skipped - Not compatible with " - "the environmment. Details: %(data)s.", - {"ns": namespace, "data": provider_data}, - ) - return False - return True def _load_all_plugins(self) -> None: diff --git a/variantlib/variants_json.py b/variantlib/variants_json.py index 8898234..84cfb82 100644 --- a/variantlib/variants_json.py +++ b/variantlib/variants_json.py @@ -13,7 +13,6 @@ from variantlib.constants import VARIANT_INFO_NAMESPACE_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_DATA_KEY -from variantlib.constants import VARIANT_INFO_PROVIDER_ENABLE_IF_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY @@ -61,8 +60,6 @@ def _provider_info_to_json( ) -> Generator[tuple[str, str | list[str] | dict[str, list[str]] | bool]]: if provider_info.requires: yield (VARIANT_INFO_PROVIDER_REQUIRES_KEY, provider_info.requires) - if provider_info.enable_if is not None: - yield (VARIANT_INFO_PROVIDER_ENABLE_IF_KEY, provider_info.enable_if) if provider_info.optional: yield (VARIANT_INFO_PROVIDER_OPTIONAL_KEY, provider_info.optional) if provider_info.plugin_api is not None: From 2c2888a782ab551ea36550ba1174aba79a5ddb5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Wed, 9 Sep 2026 17:30:55 +0200 Subject: [PATCH 4/5] Rename `is_aot_plugin` to `all_properties_compatible` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- tests/mocked_plugin_as_module.py | 2 +- tests/mocked_plugins.py | 4 ++-- tests/test_api.py | 4 +++- variantlib/cache.py | 26 -------------------------- variantlib/plugins/_subprocess.py | 7 ++++--- variantlib/protocols.py | 2 +- 6 files changed, 11 insertions(+), 34 deletions(-) delete mode 100644 variantlib/cache.py diff --git a/tests/mocked_plugin_as_module.py b/tests/mocked_plugin_as_module.py index 835c060..6938d78 100644 --- a/tests/mocked_plugin_as_module.py +++ b/tests/mocked_plugin_as_module.py @@ -15,7 +15,7 @@ class VariantFeatureConfig: namespace = "module_namespace" -is_aot_plugin = False +all_properties_compatible = False def get_all_configs() -> list[VariantFeatureConfigType]: diff --git a/tests/mocked_plugins.py b/tests/mocked_plugins.py index cefdc5a..fe9842c 100644 --- a/tests/mocked_plugins.py +++ b/tests/mocked_plugins.py @@ -91,7 +91,7 @@ def get_supported_configs() -> list[VariantFeatureConfigType]: class MockedAoTPlugin(PluginType): namespace = "aot_plugin" - is_aot_plugin = True + all_properties_compatible = True @staticmethod def get_all_configs() -> list[VariantFeatureConfigType]: @@ -115,7 +115,7 @@ def get_supported_configs() -> list[VariantFeatureConfigType]: class MultiValueAoTPlugin(PluginType): namespace = "aot_plugin" - is_aot_plugin = True + all_properties_compatible = True @staticmethod def get_all_configs() -> list[VariantFeatureConfigType]: diff --git a/tests/test_api.py b/tests/test_api.py index 6ceca63..38bc475 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -765,7 +765,9 @@ def test_make_variant_dist_info_really_invalid_build_plugin() -> None: with pytest.raises( PluginError, - match=r"Providers for namespaces {'second_namespace'} are not AoT plugins", + match=r"Providers for namespaces {'second_namespace'} do not declare " + r"having all their valid properties compatible, they cannot be used " + r"with build-requires", ): make_variant_dist_info( vdesc, diff --git a/variantlib/cache.py b/variantlib/cache.py deleted file mode 100644 index 88874f4..0000000 --- a/variantlib/cache.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING -from typing import Any -from typing import Generic -from typing import TypeVar - -if TYPE_CHECKING: - from collections.abc import Callable - -T = TypeVar("T") -RT = TypeVar("RT") - - -class VariantCache(Generic[RT]): - """This class is not necessary today - can be used for finer cache control later.""" - - cache: RT | None = None - - def __call__(self, func: Callable[[T], RT]) -> Callable[[T], RT]: - def wrapper(*args: Any, **kwargs: dict[str, Any]) -> RT: - if self.cache is None: - self.cache = func(*args, **kwargs) - return self.cache - - return wrapper diff --git a/variantlib/plugins/_subprocess.py b/variantlib/plugins/_subprocess.py index 0d5b5bd..15b6e54 100644 --- a/variantlib/plugins/_subprocess.py +++ b/variantlib/plugins/_subprocess.py @@ -94,12 +94,13 @@ def main() -> int: non_fixed_plugins = { plugin.namespace for plugin in plugins.values() - if not getattr(plugin, "is_aot_plugin", False) + if not getattr(plugin, "all_properties_compatible", False) } if non_fixed_plugins: raise TypeError( - f"Providers for namespaces {non_fixed_plugins} are not AoT plugins, " - f"they cannot be used with build-requires" + f"Providers for namespaces {non_fixed_plugins} do not declare " + "having all their valid properties compatible, they cannot be " + "used with build-requires" ) retval: dict[str, Any] = {} diff --git a/variantlib/protocols.py b/variantlib/protocols.py index 70f61c3..59d0f0b 100644 --- a/variantlib/protocols.py +++ b/variantlib/protocols.py @@ -77,7 +77,7 @@ def namespace(self) -> VariantNamespace: raise NotImplementedError @property - def is_aot_plugin(self) -> bool: + def all_properties_compatible(self) -> bool: """ Is this plugin valid for use with `build-requires`? From f40ee73054303cc49747531df890e61842882395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Wed, 9 Sep 2026 19:38:33 +0200 Subject: [PATCH 5/5] Remove `namespace` attribute from plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- .../test_plugin_package.py | 3 - tests/conftest.py | 22 +++--- tests/mocked_plugin_as_module.py | 1 - tests/mocked_plugins.py | 10 --- tests/plugins/test_loader.py | 76 ++++++------------- tests/test_api.py | 6 +- tests/test_protocols.py | 7 +- variantlib/plugins/_subprocess.py | 21 +++-- variantlib/plugins/loader.py | 59 +++++++------- variantlib/protocols.py | 6 -- 10 files changed, 76 insertions(+), 135 deletions(-) diff --git a/tests/artifacts/test-plugin-package/test_plugin_package.py b/tests/artifacts/test-plugin-package/test_plugin_package.py index 6cefe15..c45e14e 100644 --- a/tests/artifacts/test-plugin-package/test_plugin_package.py +++ b/tests/artifacts/test-plugin-package/test_plugin_package.py @@ -10,9 +10,6 @@ class FeatConfig: multi_value: bool = False -namespace = "installable_plugin" - - def get_all_configs( ) -> list[FeatConfig]: return [ diff --git a/tests/conftest.py b/tests/conftest.py index a68b5e5..b447b6d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,7 @@ from pytest_mock import MockerFixture from variantlib.plugins.loader import VARIANT_PROVIDER_CACHE_TABLE from variantlib.plugins.loader import BasePluginLoader -from variantlib.plugins.loader import ListPluginLoader +from variantlib.plugins.loader import DictPluginLoader from tests.mocked_plugins import MockedEntryPoint @@ -24,16 +24,16 @@ ) -MOCKED_PLUGIN_APIS = [ - "tests.mocked_plugins:MockedPluginA", - "tests.mocked_plugins:MockedPluginB", - "tests.mocked_plugins:MockedPluginC", -] +MOCKED_PLUGIN_APIS = { + "test_namespace": "tests.mocked_plugins:MockedPluginA", + "second_namespace": "tests.mocked_plugins:MockedPluginB", + "incompatible_namespace": "tests.mocked_plugins:MockedPluginC", +} @pytest.fixture(scope="session") def mocked_plugin_loader() -> Generator[BasePluginLoader]: - with ListPluginLoader(MOCKED_PLUGIN_APIS) as loader: + with DictPluginLoader(MOCKED_PLUGIN_APIS) as loader: yield loader @@ -42,9 +42,11 @@ def mocked_entry_points( mocker: MockerFixture, ) -> None: mocker.patch("variantlib.plugins.loader.entry_points")().select.return_value = [ - MockedEntryPoint("test", "tests.mocked_plugins:MockedPluginA"), - MockedEntryPoint("second", "tests.mocked_plugins:MockedPluginB"), - MockedEntryPoint("third", "tests.mocked_plugins:MockedPluginC"), + MockedEntryPoint("test_namespace", "tests.mocked_plugins:MockedPluginA"), + MockedEntryPoint("second_namespace", "tests.mocked_plugins:MockedPluginB"), + MockedEntryPoint( + "incompatible_namespace", "tests.mocked_plugins:MockedPluginC" + ), ] diff --git a/tests/mocked_plugin_as_module.py b/tests/mocked_plugin_as_module.py index 6938d78..1df2491 100644 --- a/tests/mocked_plugin_as_module.py +++ b/tests/mocked_plugin_as_module.py @@ -14,7 +14,6 @@ class VariantFeatureConfig: multi_value: bool = False -namespace = "module_namespace" all_properties_compatible = False diff --git a/tests/mocked_plugins.py b/tests/mocked_plugins.py index fe9842c..88826b9 100644 --- a/tests/mocked_plugins.py +++ b/tests/mocked_plugins.py @@ -16,8 +16,6 @@ class MockedEntryPoint: class MockedPluginA(PluginType): - namespace = "test_namespace" # pyright: ignore[reportAssignmentType,reportIncompatibleMethodOverride] - @staticmethod def get_all_configs() -> list[VariantFeatureConfigType]: return [ @@ -47,8 +45,6 @@ def get_supported_configs() -> list[VariantFeatureConfigType]: # NB: this plugin deliberately does not inherit from PluginType # to test that we don't rely on that inheritance class MockedPluginB: - namespace = "second_namespace" - @classmethod def get_all_configs(cls) -> list[MyVariantFeatureConfig]: return [ @@ -74,8 +70,6 @@ def __init__(self, name: str) -> None: class MockedPluginC(PluginType): - namespace = "incompatible_namespace" - @classmethod def get_all_configs(cls) -> list[VariantFeatureConfigType]: return [ @@ -89,8 +83,6 @@ def get_supported_configs() -> list[VariantFeatureConfigType]: class MockedAoTPlugin(PluginType): - namespace = "aot_plugin" - all_properties_compatible = True @staticmethod @@ -113,8 +105,6 @@ def get_supported_configs() -> list[VariantFeatureConfigType]: class MultiValueAoTPlugin(PluginType): - namespace = "aot_plugin" - all_properties_compatible = True @staticmethod diff --git a/tests/plugins/test_loader.py b/tests/plugins/test_loader.py index 056bf02..c7b3d04 100644 --- a/tests/plugins/test_loader.py +++ b/tests/plugins/test_loader.py @@ -22,8 +22,8 @@ from variantlib.models.variant_info import ProviderInfo from variantlib.models.variant_info import VariantInfo from variantlib.plugins.loader import BasePluginLoader +from variantlib.plugins.loader import DictPluginLoader from variantlib.plugins.loader import EntryPointPluginLoader -from variantlib.plugins.loader import ListPluginLoader from variantlib.plugins.loader import PluginLoader from variantlib.protocols import PluginType from variantlib.protocols import VariantFeatureConfigType @@ -46,8 +46,6 @@ class ClashingPlugin(PluginType): - namespace = "test_namespace" # pyright: ignore[reportAssignmentType,reportIncompatibleMethodOverride] - @classmethod def get_all_configs(cls) -> list[VariantFeatureConfigType]: return [ @@ -62,8 +60,6 @@ def get_supported_configs(cls) -> list[VariantFeatureConfigType]: class ExceptionPluginBase(PluginType): - namespace = "exception_test" # pyright: ignore[reportAssignmentType,reportIncompatibleMethodOverride] - returned_value: list[VariantFeatureConfigType] @classmethod @@ -156,25 +152,6 @@ def test_get_supported_configs( } -def test_namespace_clash() -> None: - with ( - pytest.raises( - RuntimeError, - match=( - r"Two plugins found using the same namespace test_namespace. " - r"Refusing to proceed." - ), - ), - ListPluginLoader( - [ - "tests.mocked_plugins:MockedPluginA", - "tests.plugins.test_loader:ClashingPlugin", - ] - ), - ): - pass - - class IncorrectListTypePlugin(ExceptionPluginBase): returned_value = ( VariantFeatureConfig("k1", ["v1"], multi_value=False), @@ -185,14 +162,14 @@ class IncorrectListTypePlugin(ExceptionPluginBase): @pytest.mark.parametrize("method", GET_CONFIG_METHODS) def test_get_supported_configs_incorrect_list_type(method: str) -> None: with ( - ListPluginLoader( - ["tests.plugins.test_loader:IncorrectListTypePlugin"] + DictPluginLoader( + {"test": "tests.plugins.test_loader:IncorrectListTypePlugin"} ) as loader, pytest.raises( PluginError, match=r".*" + re.escape( - f"Provider exception_test, {method}() method returned " + f"IncorrectListTypePlugin, {method}() method returned " "incorrect type. Expected " "list[_variantlib_protocols.VariantFeatureConfigType], " "got " @@ -208,8 +185,8 @@ class IncorrectListLengthPlugin(ExceptionPluginBase): def test_get_configs_empty_list() -> None: with ( - ListPluginLoader( - ["tests.plugins.test_loader:IncorrectListLengthPlugin"] + DictPluginLoader( + {"exception_test": "tests.plugins.test_loader:IncorrectListLengthPlugin"} ) as loader, pytest.raises( PluginError, @@ -229,14 +206,14 @@ class IncorrectListMemberTypePlugin(ExceptionPluginBase): @pytest.mark.parametrize("method", GET_CONFIG_METHODS) def test_get_configs_incorrect_list_member_type(method: str) -> None: with ( - ListPluginLoader( - ["tests.plugins.test_loader:IncorrectListMemberTypePlugin"] + DictPluginLoader( + {"test": "tests.plugins.test_loader:IncorrectListMemberTypePlugin"} ) as loader, pytest.raises( PluginError, match=r".*" + re.escape( - f"Provider exception_test, {method}() method returned " + f"IncorrectListMemberTypePlugin, {method}() method returned " "incorrect type. Expected " "list[_variantlib_protocols.VariantFeatureConfigType], " "got list[typing.Union[_variantlib_protocols.VariantFeatureConfigType, " @@ -256,7 +233,7 @@ def test_namespace_missing_module() -> None: r"No module named 'tests.no_such_module'" ), ), - ListPluginLoader(["tests.no_such_module:foo"]), + DictPluginLoader({"test": "tests.no_such_module:foo"}), ): pass @@ -271,14 +248,12 @@ def test_namespace_incorrect_name() -> None: "'no_such_name'" ), ), - ListPluginLoader([("tests.plugins.test_loader:no_such_name")]), + DictPluginLoader({"test": "tests.plugins.test_loader:no_such_name"}), ): pass class IncompletePlugin: - namespace = "incomplete_plugin" - @classmethod def get_supported_configs(cls) -> list[VariantFeatureConfigType]: return [] @@ -290,9 +265,9 @@ def test_namespace_incorrect_type() -> None: PluginError, match=r"'tests.plugins.test_loader:RANDOM_STUFF' does not meet " r"the PluginType prototype: 123 \(missing attributes: " - r"get_all_configs, get_supported_configs, namespace\)", + r"get_all_configs, get_supported_configs\)", ), - ListPluginLoader(["tests.plugins.test_loader:RANDOM_STUFF"]), + DictPluginLoader({"test": "tests.plugins.test_loader:RANDOM_STUFF"}), ): pass @@ -310,7 +285,7 @@ def test_namespace_instantiation_returns_incorrect_type( "(missing attributes: get_all_configs)" ), ), - ListPluginLoader([f"tests.plugins.test_loader:{cls}"]), + DictPluginLoader({"test": f"tests.plugins.test_loader:{cls}"}), ): pass @@ -326,20 +301,19 @@ def test_namespaces( def test_non_callable_plugin() -> None: - with ListPluginLoader( - [ - "tests.mocked_plugins:IndirectPath.MoreIndirection.object_a", - "tests.mocked_plugins:OBJECT_B", - ] - ) as loader: + plugins = { + "test_namespace": "tests.mocked_plugins:IndirectPath.MoreIndirection.object_a", + "second_namespace": "tests.mocked_plugins:OBJECT_B", + } + with DictPluginLoader(plugins) as loader: assert loader.namespaces == ["test_namespace", "second_namespace"] def test_plugin_module() -> None: - with ListPluginLoader( - [ - "tests.mocked_plugin_as_module", - ] + with DictPluginLoader( + { + "module_namespace": "tests.mocked_plugin_as_module", + } ) as loader: assert loader.namespaces == ["module_namespace"] @@ -347,7 +321,7 @@ def test_plugin_module() -> None: def test_load_plugin_invalid_arg() -> None: with ( pytest.raises(ValidationError), - ListPluginLoader(["tests.mocked_plugins:foo:bar"]), + DictPluginLoader({"test": "tests.mocked_plugins:foo:bar"}), ): pass @@ -507,7 +481,7 @@ def test_optional_plugins(value: bool | list[VariantNamespace], expected: bool) "loader_call", [ partial(PluginLoader, VariantInfo(), include_aot_plugins=True), - partial(ListPluginLoader, []), + partial(DictPluginLoader, {}), ], ) def test_empty_plugin_list(loader_call: Callable[[], BasePluginLoader]) -> None: diff --git a/tests/test_api.py b/tests/test_api.py index 38bc475..f5962dd 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -765,9 +765,9 @@ def test_make_variant_dist_info_really_invalid_build_plugin() -> None: with pytest.raises( PluginError, - match=r"Providers for namespaces {'second_namespace'} do not declare " - r"having all their valid properties compatible, they cannot be used " - r"with build-requires", + match=r"Plugins identified by API " + r"\{\'tests.mocked_plugins:MockedPluginB\'} do not declare having all " + "their valid properties compatible, they cannot be used with build-requires", ): make_variant_dist_info( vdesc, diff --git a/tests/test_protocols.py b/tests/test_protocols.py index f7d37db..621b1d2 100644 --- a/tests/test_protocols.py +++ b/tests/test_protocols.py @@ -50,14 +50,9 @@ def test_plugin_type(cls: type) -> None: assert isinstance(cls, PluginType) -@pytest.mark.parametrize( - "missing", ["namespace", "get_all_configs", "get_supported_configs"] -) +@pytest.mark.parametrize("missing", ["get_all_configs", "get_supported_configs"]) def test_plugin_type_abstract(missing: str) -> None: class PartialPluginTypeSubclass(PluginType): - if missing != "namespace": - namespace = "ns" - if missing != "get_all_configs": @staticmethod diff --git a/variantlib/plugins/_subprocess.py b/variantlib/plugins/_subprocess.py index 15b6e54..8220629 100644 --- a/variantlib/plugins/_subprocess.py +++ b/variantlib/plugins/_subprocess.py @@ -58,14 +58,13 @@ def load_plugins(plugin_apis: list[str]) -> Generator[PluginType]: def process_configs( - configs: list[VariantFeatureConfigType], plugin_instance: PluginType, method: str + configs: list[VariantFeatureConfigType], plugin_api: str, method: str ) -> list[dict[str, bool | str | list[str]]]: try: validate_type(configs, list[VariantFeatureConfigType]) except ValidationError as err: raise TypeError( - f"Provider {plugin_instance.namespace}, {method}() " - f"method returned incorrect type. {err}" + f"Provider {plugin_api}, {method}() method returned incorrect type. {err}" ) from None return [ {"name": vfeat.name, "values": vfeat.values, "multi_value": vfeat.multi_value} @@ -92,30 +91,28 @@ def main() -> int: if args.require_fixed: non_fixed_plugins = { - plugin.namespace - for plugin in plugins.values() + plugin_api + for plugin_api, plugin in plugins.items() if not getattr(plugin, "all_properties_compatible", False) } if non_fixed_plugins: raise TypeError( - f"Providers for namespaces {non_fixed_plugins} do not declare " + f"Plugins identified by API {non_fixed_plugins} do not declare " "having all their valid properties compatible, they cannot be " "used with build-requires" ) retval: dict[str, Any] = {} for command, command_args in commands.items(): - if command == "namespaces": + if command == "load": assert not command_args - retval[command] = { - plugin_api: plugin.namespace for plugin_api, plugin in plugins.items() - } + retval[command] = {} # pyright: ignore[reportArgumentType] elif command == "get_all_configs": assert not command_args retval[command] = { # pyright: ignore[reportArgumentType] plugin_api: process_configs( plugin.get_all_configs(), - plugin, + plugin_api, command, ) for plugin_api, plugin in plugins.items() @@ -125,7 +122,7 @@ def main() -> int: retval[command] = { # pyright: ignore[reportArgumentType] plugin_api: process_configs( plugin.get_supported_configs(), - plugin, + plugin_api, command, ) for plugin_api, plugin in plugins.items() diff --git a/variantlib/plugins/loader.py b/variantlib/plugins/loader.py index 24e9dbd..78a7ab5 100644 --- a/variantlib/plugins/loader.py +++ b/variantlib/plugins/loader.py @@ -157,7 +157,9 @@ def _run_call_subprocess( @abstractmethod def _load_all_plugins(self) -> None: ... - def _load_all_plugins_from_tuple(self, plugin_apis: list[str]) -> None: + def _load_all_plugins_from_map( + self, plugin_apis: dict[VariantNamespace, str] + ) -> None: if self._namespace_map is not None: raise RuntimeError( "Impossible to load plugins - `self._namespace_map` is not None" @@ -168,7 +170,7 @@ def _load_all_plugins_from_tuple(self, plugin_apis: list[str]) -> None: return normalized_plugin_apis = [] - for plugin_api in plugin_apis: + for namespace, plugin_api in plugin_apis.items(): plugin_api_match = validate_matches_re( plugin_api, VALIDATION_PROVIDER_PLUGIN_API_REGEX ) @@ -178,31 +180,17 @@ def _load_all_plugins_from_tuple(self, plugin_apis: list[str]) -> None: normalized_plugin_apis.append(f"{import_name}:{attr_path}") logger.info( - "Loading plugin via %(plugin_api)s", + "Loading plugin for namespace %(namespace)s via %(plugin_api)s", { + "namespace": namespace, "plugin_api": plugin_api, }, ) - namespaces = self._call_subprocess(normalized_plugin_apis, {"namespaces": {}})[ - "namespaces" - ] - - for plugin_api, namespace in namespaces.items(): - if namespace in self._namespace_map.values(): - raise RuntimeError( - "Two plugins found using the same namespace " - f"{namespace}. Refusing to proceed." - ) + self._call_subprocess(normalized_plugin_apis, {"load": {}}) + for namespace, plugin_api in plugin_apis.items(): self._namespace_map[plugin_api] = namespace - logger.info( - "Namespace %(namespace)s provided by plugin %(plugin_api)s", - { - "namespace": namespace, - "plugin_api": plugin_api, - }, - ) def _check_plugins_loaded(self) -> None: if self._namespace_map is None: @@ -361,13 +349,13 @@ def _load_all_plugins(self) -> None: "Impossible to load plugins - `self._namespace_map` is not None" ) - plugins = [ - provider_data.object_reference + plugins = { + namespace: provider_data.object_reference for namespace, provider_data in self._variant_info.providers.items() if self._plugin_enabled(namespace, provider_data) - ] + } - self._load_all_plugins_from_tuple(plugin_apis=plugins) + self._load_all_plugins_from_map(plugin_apis=plugins) def get_all_configs( self, @@ -395,7 +383,7 @@ def _load_all_plugins(self) -> None: ) self._plugin_provider_packages = {} - plugin_apis = [] + plugin_apis: dict[VariantNamespace, str] = {} eps = entry_points().select(group="variant_plugins") for ep in eps: logger.info( @@ -409,11 +397,16 @@ def _load_all_plugins(self) -> None: }, ) - plugin_apis.append(ep.value) + if ep.name in plugin_apis: + raise PluginError( + f"Two plugins use the same entry point name {ep.name!r}: " + f"{plugin_apis[ep.name]!r} and {ep.value!r}" + ) + plugin_apis[ep.name] = ep.value if ep.dist is not None: self._plugin_provider_packages[ep.value] = ep.dist - self._load_all_plugins_from_tuple(plugin_apis=plugin_apis) + self._load_all_plugins_from_map(plugin_apis=plugin_apis) @property def plugin_provider_packages(self) -> dict[str, Distribution]: @@ -424,17 +417,17 @@ def plugin_provider_packages(self) -> dict[str, Distribution]: return self._plugin_provider_packages -class ListPluginLoader(BasePluginLoader): - """Load plugins from an explicit plugin-api list""" +class DictPluginLoader(BasePluginLoader): + """Load plugins from an explicit plugin-api map""" - _plugin_apis: list[str] + _plugin_apis: dict[VariantNamespace, str] def __init__( self, - plugin_apis: list[str], + plugin_apis: dict[VariantNamespace, str], venv_python_executable: Path | None = None, ) -> None: - self._plugin_apis = list(plugin_apis) + self._plugin_apis = plugin_apis super().__init__(venv_python_executable=venv_python_executable) def _load_all_plugins(self) -> None: @@ -443,4 +436,4 @@ def _load_all_plugins(self) -> None: "Impossible to load plugins - `self._namespace_map` is not None" ) - self._load_all_plugins_from_tuple(plugin_apis=self._plugin_apis) + self._load_all_plugins_from_map(plugin_apis=self._plugin_apis) diff --git a/variantlib/protocols.py b/variantlib/protocols.py index 59d0f0b..35d962f 100644 --- a/variantlib/protocols.py +++ b/variantlib/protocols.py @@ -70,12 +70,6 @@ class PluginType(Protocol): # Note: properties are used here for docstring purposes, these must # be actually implemented as attributes. - @property - @abstractmethod - def namespace(self) -> VariantNamespace: - """Plugin namespace""" - raise NotImplementedError - @property def all_properties_compatible(self) -> bool: """