From 01c55e4c1e3f6071682fa2b2dae6794ace786e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 31 Aug 2026 17:02:45 +0200 Subject: [PATCH 1/7] Move `static-properties` inside provider information MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- tests/plugins/test_loader.py | 8 +- tests/test_api.py | 22 ++--- tests/test_pyproject_toml.py | 54 ++++-------- variantlib/api.py | 14 +-- variantlib/constants.py | 6 +- variantlib/models/variant_info.py | 139 ++++++++++++++---------------- variantlib/plugins/loader.py | 5 +- variantlib/pyproject_toml.py | 10 +-- variantlib/variants_json.py | 26 +++--- 9 files changed, 125 insertions(+), 159 deletions(-) diff --git a/tests/plugins/test_loader.py b/tests/plugins/test_loader.py index 3e9df671..046488ec 100644 --- a/tests/plugins/test_loader.py +++ b/tests/plugins/test_loader.py @@ -566,11 +566,6 @@ def test_package_defined_properties(include_aot_plugins: bool) -> None: "second_namespace", "private", ], - static_properties={ - "private": { - "baz": ["v5", "v6"], - }, - }, providers={ "test_namespace": ProviderInfo( requires=["variantlib"], plugin_api="tests.mocked_plugins:MockedPluginA" @@ -582,6 +577,9 @@ def test_package_defined_properties(include_aot_plugins: bool) -> None: ), "private": ProviderInfo( install_time=False, + static_properties={ + "baz": ["v5", "v6"], + }, ), }, ) diff --git a/tests/test_api.py b/tests/test_api.py index dab8da74..05696bf1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -34,7 +34,7 @@ from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_REQUIRES_KEY -from variantlib.constants import VARIANT_INFO_STATIC_PROPERTIES_KEY +from variantlib.constants import VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY from variantlib.constants import VARIANTS_JSON_SCHEMA_KEY from variantlib.constants import VARIANTS_JSON_SCHEMA_URL from variantlib.constants import VARIANTS_JSON_VARIANT_DATA_KEY @@ -314,9 +314,9 @@ def test_validate_variant(optional: bool) -> None: plugin_api="donotuseme", optional=optional, install_time=False, + static_properties={"build_type": ["debug", "release"]}, ), }, - static_properties={"private": {"build_type": ["debug", "release"]}}, ) expected = { @@ -397,6 +397,10 @@ def test_make_variant_dist_info( }, "ns3": { VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: False, + VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: { + "f1": ["v1", "v2"], + "f2": ["v3", "v4"], + }, }, } ) @@ -406,12 +410,6 @@ def test_make_variant_dist_info( VARIANT_INFO_FEATURE_KEY: {"ns3": ["f2", "f1"]}, }, ) - expected[VARIANT_INFO_STATIC_PROPERTIES_KEY] = { - "ns3": { - "f1": ["v1", "v2"], - "f2": ["v3", "v4"], - }, - } if pyproject_toml is PYPROJECT_TOML: expected[VARIANT_INFO_DEFAULT_PRIO_KEY].update( @@ -706,11 +704,9 @@ def test_make_variant_dist_info_expand_aot_plugin_properties( expected[VARIANT_INFO_DEFAULT_PRIO_KEY][VARIANT_INFO_FEATURE_KEY] = { "aot_plugin": ["name1", "name2"], } - expected[VARIANT_INFO_STATIC_PROPERTIES_KEY] = { - "aot_plugin": { - "name1": ["val1a", "val1b"], - "name2": ["val2a", "val2b", "val2c"], - }, + provider_data[VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY] = { + "name1": ["val1a", "val1b"], + "name2": ["val2a", "val2b", "val2c"], } assert ( diff --git a/tests/test_pyproject_toml.py b/tests/test_pyproject_toml.py index 304f1baa..9096f520 100644 --- a/tests/test_pyproject_toml.py +++ b/tests/test_pyproject_toml.py @@ -15,7 +15,7 @@ from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_REQUIRES_KEY -from variantlib.constants import VARIANT_INFO_STATIC_PROPERTIES_KEY +from variantlib.constants import VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY from variantlib.errors import ValidationError from variantlib.models.variant_info import ProviderInfo from variantlib.pyproject_toml import VariantPyProjectToml @@ -60,7 +60,7 @@ [{PYPROJECT_TOML_TOP_KEY}.{VARIANT_INFO_PROVIDER_DATA_KEY}.ns3] {VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY} = false -[{PYPROJECT_TOML_TOP_KEY}.{VARIANT_INFO_STATIC_PROPERTIES_KEY}.ns3] +[{PYPROJECT_TOML_TOP_KEY}.{VARIANT_INFO_PROVIDER_DATA_KEY}.ns3.{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY}] f1 = ["v1", "v2"] f2 = ["v3", "v4"] """ @@ -107,9 +107,9 @@ def test_pyproject_toml() -> None: ), "ns3": ProviderInfo( install_time=False, + static_properties={"f1": ["v1", "v2"], "f2": ["v3", "v4"]}, ), } - assert pyproj.static_properties == {"ns3": {"f1": ["v1", "v2"], "f2": ["v3", "v4"]}} def test_pyproject_toml_minimal() -> None: @@ -134,9 +134,9 @@ def test_pyproject_toml_minimal() -> None: ), "ns3": ProviderInfo( install_time=False, + static_properties={"f1": ["v1", "v2"], "f2": ["v3", "v4"]}, ), } - assert pyproj.static_properties == {"ns3": {"f1": ["v1", "v2"], "f2": ["v3", "v4"]}} def test_invalid_top_type() -> None: @@ -336,9 +336,9 @@ def test_missing_provider_requires_aot() -> None: "ns": { VARIANT_INFO_PROVIDER_REQUIRES_KEY: [], VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: False, + VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: {"test": ["val"]}, } }, - VARIANT_INFO_STATIC_PROPERTIES_KEY: {"ns": {"test": ["val"]}}, } } ) @@ -455,9 +455,9 @@ def test_conversion(cls: type[VariantPyProjectToml | VariantsJson]) -> None: ), "ns3": ProviderInfo( install_time=False, + static_properties={"f1": ["v1", "v2"], "f2": ["v3", "v4"]}, ), } - assert pyproj.static_properties == {"ns3": {"f1": ["v1", "v2"], "f2": ["v3", "v4"]}} # Non-common fields should be reset to defaults if isinstance(converted, VariantsJson): @@ -508,9 +508,9 @@ def test_no_plugin_api() -> None: def test_missing_static_properties() -> None: with pytest.raises( ValidationError, - match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_STATIC_PROPERTIES_KEY} " - r"must specify properties for all AoT providers; currently provided: set\(\); " - r"expected: {'ns'}", + match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_PROVIDER_DATA_KEY}\.ns: " + rf"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY} or " + rf"{VARIANT_INFO_PROVIDER_REQUIRES_KEY} must be specified for AoT providers", ): VariantPyProjectToml( { @@ -524,30 +524,6 @@ def test_missing_static_properties() -> None: ) -@pytest.mark.parametrize("install_time", [False, True]) -def test_extraneous_static_properties(install_time: bool) -> None: - with pytest.raises( - ValidationError, - match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_STATIC_PROPERTIES_KEY} " - r"must specify properties for all AoT providers; currently provided: {'ns'}; " - r"expected: set\(\)", - ): - VariantPyProjectToml( - { - PYPROJECT_TOML_TOP_KEY: { - VARIANT_INFO_DEFAULT_PRIO_KEY: {VARIANT_INFO_NAMESPACE_KEY: ["ns"]}, - VARIANT_INFO_PROVIDER_DATA_KEY: { - "ns": { - VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: install_time, - VARIANT_INFO_PROVIDER_REQUIRES_KEY: ["variantlib"], - } - }, - VARIANT_INFO_STATIC_PROPERTIES_KEY: {"ns": {"f": ["v"]}}, - } - } - ) - - def test_static_properties_one_feature() -> None: VariantPyProjectToml( { @@ -556,9 +532,9 @@ def test_static_properties_one_feature() -> None: VARIANT_INFO_PROVIDER_DATA_KEY: { "ns": { VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: False, + VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: {"f": ["v"]}, } }, - VARIANT_INFO_STATIC_PROPERTIES_KEY: {"ns": {"f": ["v"]}}, } } ) @@ -567,7 +543,8 @@ def test_static_properties_one_feature() -> None: def test_static_properties_missing_priorities() -> None: with pytest.raises( ValidationError, - match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_STATIC_PROPERTIES_KEY}\.ns: " + match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_PROVIDER_DATA_KEY}\.ns\." + rf"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY}: " r"for AoT providers with multiple features, priorities need to be specified " rf"via {VARIANT_INFO_DEFAULT_PRIO_KEY}\.{VARIANT_INFO_FEATURE_KEY}; missing: " r"{'f2'}", @@ -582,11 +559,12 @@ def test_static_properties_missing_priorities() -> None: VARIANT_INFO_PROVIDER_DATA_KEY: { "ns": { VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: False, + VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: { + "f1": ["v"], + "f2": ["v"], + }, } }, - VARIANT_INFO_STATIC_PROPERTIES_KEY: { - "ns": {"f1": ["v"], "f2": ["v"]} - }, } } ) diff --git a/variantlib/api.py b/variantlib/api.py index 2fdf628d..b3940610 100644 --- a/variantlib/api.py +++ b/variantlib/api.py @@ -234,16 +234,16 @@ def make_variant_dist_info( for config in configs: if config.namespace not in build_namespaces: continue - variant_json.static_properties[config.namespace] = {} + variant_json.providers[config.namespace].static_properties = {} for vfeat in config.configs: if vfeat.multi_value: raise ValidationError( f"Feature '{config.namespace} :: {vfeat.name}' is " "multi-value, which is invalid for ahead-of-time plugins" ) - variant_json.static_properties[config.namespace][vfeat.name] = ( - vfeat.values - ) + variant_json.providers[config.namespace].static_properties[ + vfeat.name + ] = vfeat.values # adjust feature priorities only if at least 2 features defined if len(config.configs) > 1: @@ -262,9 +262,9 @@ def make_variant_dist_info( for vprop in vdesc.properties: if vprop.namespace not in build_namespaces: continue - if vprop.value not in variant_json.static_properties[vprop.namespace].get( - vprop.feature, [] - ): + if vprop.value not in variant_json.providers[ + vprop.namespace + ].static_properties.get(vprop.feature, []): raise ValidationError( f"Property {vprop.to_str()!r} is not installable according to the " "respective provider plugin, which is invalid for ahead-of-time " diff --git a/variantlib/constants.py b/variantlib/constants.py index 06b2998b..6310b9b4 100644 --- a/variantlib/constants.py +++ b/variantlib/constants.py @@ -19,7 +19,9 @@ 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" -VARIANT_INFO_STATIC_PROPERTIES_KEY: Literal["static-properties"] = "static-properties" +VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: Literal["static-properties"] = ( + "static-properties" +) PYPROJECT_TOML_TOP_KEY = "variant" @@ -118,6 +120,7 @@ class PriorityJsonDict(TypedDict, total=False): "optional": bool, "plugin-api": str, "requires": list[str], + "static-properties": dict[str, list[str]], }, total=False, ) @@ -131,7 +134,6 @@ class PriorityJsonDict(TypedDict, total=False): "$schema": str, "default-priorities": PriorityJsonDict, "providers": dict[str, ProviderPluginJsonDict], - "static-properties": dict[str, dict[str, list[str]]], "variants": dict[str, VariantInfoJsonDict], }, total=False, diff --git a/variantlib/models/variant_info.py b/variantlib/models/variant_info.py index 19101a78..322df4f7 100644 --- a/variantlib/models/variant_info.py +++ b/variantlib/models/variant_info.py @@ -23,7 +23,7 @@ from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_REQUIRES_KEY -from variantlib.constants import VARIANT_INFO_STATIC_PROPERTIES_KEY +from variantlib.constants import VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY from variantlib.errors import ValidationError from variantlib.protocols import VariantFeatureName from variantlib.protocols import VariantFeatureValue @@ -40,12 +40,13 @@ class ProviderInfo: install_time: bool = True optional: bool = False requires: list[str] = field(default_factory=list) + static_properties: dict[VariantFeatureName, list[VariantFeatureValue]] = field( + default_factory=dict + ) def __post_init__(self) -> None: - if self.install_time and not self.requires: - raise ValidationError( - "requires need to be specified for install-time providers" - ) + # TODO: readd validation for requires/static-properties + pass @property def object_reference(self) -> str: @@ -68,9 +69,6 @@ class VariantInfo: ] = field(default_factory=dict) providers: dict[VariantNamespace, ProviderInfo] = field(default_factory=dict) - static_properties: dict[ - VariantNamespace, dict[VariantFeatureName, list[VariantFeatureValue]] - ] = field(default_factory=dict) def copy_as_kwargs(self) -> dict[str, Any]: """Return a "kwargs" dict suitable for instantiating a copy of itself""" @@ -95,15 +93,13 @@ def copy_as_kwargs(self) -> dict[str, Any]: optional=provider_data.optional, plugin_api=provider_data.plugin_api, requires=list(provider_data.requires), + static_properties={ + feature: list(values) + for feature, values in provider_data.static_properties.items() + }, ) for namespace, provider_data in self.providers.items() }, - "static_properties": { - namespace: { - feature: list(values) for feature, values in feature_dict.items() - } - for namespace, feature_dict in self.static_properties.items() - }, } def get_provider_requires( @@ -129,7 +125,8 @@ def get_provider_requires( requirements.update(provider.requires) return requirements - def _get_expected_aot_namespaces(self) -> set[VariantNamespace]: + @property + def _aot_providers_need_static_properties(self) -> bool: raise NotImplementedError def _process_common(self, validator: KeyTrackingValidator) -> None: @@ -208,57 +205,69 @@ def _process_common(self, validator: KeyTrackingValidator) -> None: VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY, bool, True ) as provider_install_time: pass + provider_static_properties = {} + with validator.get( + VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY, + dict[VariantFeatureName, list[VariantFeatureValue]], + {}, + ) as feature_dict: + validator.list_matches_re(VALIDATION_FEATURE_NAME_REGEX) + for feature_name in feature_dict: + with validator.get( + feature_name, list[VariantFeatureValue] + ) as feature_values: + validator.list_matches_re(VALIDATION_VALUE_REGEX) + provider_static_properties[feature_name] = ( + feature_values + ) + + if len(feature_dict) > 1: + feature_prios = set( + self.feature_priorities.get(namespace, []) + ) + missing_feature_prios = ( + set(feature_dict.keys()) - feature_prios + ) + if missing_feature_prios: + raise ValidationError( + f"{validator.key}: for AoT providers with multiple " + "features, priorities need to be specified via " + f"{VARIANT_INFO_DEFAULT_PRIO_KEY}." + f"{VARIANT_INFO_FEATURE_KEY}; missing: " + f"{missing_feature_prios}" + ) + + if provider_install_time: + if not provider_requires: + raise ValidationError( + f"{validator.key}: " + f"{VARIANT_INFO_PROVIDER_REQUIRES_KEY} must be " + "specified for install-time providers" + ) + elif not provider_static_properties: + if self._aot_providers_need_static_properties: + raise ValidationError( + f"{validator.key}: " + f"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY} " + "must be specified for AoT providers" + ) + if not provider_requires: + raise ValidationError( + f"{validator.key}: " + f"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY} or " + f"{VARIANT_INFO_PROVIDER_REQUIRES_KEY} must be " + "specified for AoT providers" + ) - if provider_install_time and not provider_requires: - raise ValidationError( - f"{validator.key}: " - f"{VARIANT_INFO_PROVIDER_REQUIRES_KEY} must be " - "specified for install-time plugins" - ) self.providers[namespace] = ProviderInfo( enable_if=provider_enable_if, install_time=provider_install_time, optional=provider_optional, plugin_api=provider_plugin_api, requires=list(provider_requires), + static_properties=provider_static_properties, ) - with validator.get( - VARIANT_INFO_STATIC_PROPERTIES_KEY, - dict[ - VariantNamespace, - dict[VariantFeatureName, list[VariantFeatureValue]], - ], - {}, - ) as static_properties: - validator.list_matches_re(VALIDATION_NAMESPACE_REGEX) - self.static_properties = {} - for namespace in static_properties: - with validator.get( - namespace, dict[VariantFeatureName, list[VariantFeatureValue]] - ) as feature_dict: - validator.list_matches_re(VALIDATION_FEATURE_NAME_REGEX) - for feature_name in feature_dict: - with validator.get( - feature_name, list[VariantFeatureValue] - ) as feature_values: - validator.list_matches_re(VALIDATION_VALUE_REGEX) - self.static_properties.setdefault(namespace, {})[ - feature_name - ] = feature_values - - if len(feature_dict) > 1: - feature_prios = set(self.feature_priorities.get(namespace, [])) - missing_feature_prios = set(feature_dict.keys()) - feature_prios - if missing_feature_prios: - raise ValidationError( - f"{validator.key}: for AoT providers with multiple " - "features, priorities need to be specified via " - f"{VARIANT_INFO_DEFAULT_PRIO_KEY}." - f"{VARIANT_INFO_FEATURE_KEY}; missing: " - f"{missing_feature_prios}" - ) - all_providers = set(self.providers.keys()) all_providers_key = ".".join([*validator.keys, VARIANT_INFO_PROVIDER_DATA_KEY]) namespace_prios_key = ".".join( @@ -275,19 +284,3 @@ def _process_common(self, validator: KeyTrackingValidator) -> None: f"as {all_providers_key} keys; currently: " f"{set(self.namespace_priorities)} vs. {all_providers}" ) - - provided_aot_namespaces = set(self.static_properties.keys()) - aot_namespaces = self._get_expected_aot_namespaces() - static_properties_key = ".".join( - [ - *validator.keys, - VARIANT_INFO_STATIC_PROPERTIES_KEY, - ] - ) - - if provided_aot_namespaces != aot_namespaces: - raise ValidationError( - f"{static_properties_key} must specify properties for all AoT " - f"providers; currently provided: {provided_aot_namespaces}; " - f"expected: {aot_namespaces}" - ) diff --git a/variantlib/plugins/loader.py b/variantlib/plugins/loader.py index b87987e1..781bb8c4 100644 --- a/variantlib/plugins/loader.py +++ b/variantlib/plugins/loader.py @@ -313,7 +313,10 @@ def __init__( self._include_aot_plugins = include_aot_plugins super().__init__( venv_python_executable=venv_python_executable, - package_defined_properties=variant_info.static_properties, + package_defined_properties={ + namespace: provider_info.static_properties + for namespace, provider_info in variant_info.providers.items() + }, ) def _use_static_properties_for_provider(self, provider_data: ProviderInfo) -> bool: diff --git a/variantlib/pyproject_toml.py b/variantlib/pyproject_toml.py index ce78d549..8f1bac8f 100644 --- a/variantlib/pyproject_toml.py +++ b/variantlib/pyproject_toml.py @@ -13,7 +13,6 @@ if TYPE_CHECKING: from pathlib import Path - from variantlib.protocols import VariantNamespace if sys.version_info >= (3, 11): from typing import Self @@ -41,12 +40,9 @@ def from_path(cls, path: Path) -> Self: with path.open("rb") as f: return cls(tomllib.load(f)) - def _get_expected_aot_namespaces(self) -> set[VariantNamespace]: - return { - namespace - for namespace, provider_info in self.providers.items() - if not provider_info.install_time and not provider_info.requires - } + @property + def _aot_providers_need_static_properties(self) -> bool: + return False def _process(self, variant_table: dict[str, VariantInfoJsonDict]) -> None: validator = KeyTrackingValidator(PYPROJECT_TOML_TOP_KEY, variant_table) diff --git a/variantlib/variants_json.py b/variantlib/variants_json.py index 52aabec9..82e8f8f6 100644 --- a/variantlib/variants_json.py +++ b/variantlib/variants_json.py @@ -19,7 +19,7 @@ from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_REQUIRES_KEY -from variantlib.constants import VARIANT_INFO_STATIC_PROPERTIES_KEY +from variantlib.constants import VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY from variantlib.constants import VARIANTS_JSON_SCHEMA_KEY from variantlib.constants import VARIANTS_JSON_SCHEMA_URL from variantlib.constants import VARIANTS_JSON_VARIANT_DATA_KEY @@ -34,8 +34,6 @@ if TYPE_CHECKING: from collections.abc import Generator - from variantlib.protocols import VariantNamespace - if sys.version_info >= (3, 11): from typing import Self @@ -61,7 +59,7 @@ def __init__(self, variants_json: VariantsJsonDict | VariantInfo) -> None: @staticmethod def _provider_info_to_json( provider_info: ProviderInfo, - ) -> Generator[tuple[str, str | list[str] | bool]]: + ) -> 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: @@ -72,6 +70,11 @@ def _provider_info_to_json( yield (VARIANT_INFO_PROVIDER_PLUGIN_API_KEY, provider_info.plugin_api) if not provider_info.install_time: yield (VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY, provider_info.install_time) + if provider_info.static_properties: + yield ( + VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY, + provider_info.static_properties, + ) def _priorities_to_json(self) -> Generator[tuple[str, Any]]: yield (VARIANT_INFO_NAMESPACE_KEY, self.namespace_priorities) @@ -80,7 +83,9 @@ def _priorities_to_json(self) -> Generator[tuple[str, Any]]: if self.property_priorities: yield (VARIANT_INFO_PROPERTY_KEY, self.property_priorities) - def providers_dict(self) -> dict[str, dict[str, str | list[str] | bool]]: + def providers_dict( + self, + ) -> dict[str, dict[str, str | list[str] | dict[str, list[str]] | bool]]: """Get a dictionary of providers in a format suitable for JSON serialization""" return { namespace: dict(self._provider_info_to_json(provider_info)) @@ -100,8 +105,6 @@ def to_str(self) -> str: vhash: vdesc.to_dict() for vhash, vdesc in self.variants.items() }, } - if self.static_properties: - data[VARIANT_INFO_STATIC_PROPERTIES_KEY] = self.static_properties return json.dumps(data, indent=4, sort_keys=True) @@ -156,12 +159,9 @@ def merge(self, variant_dist_info: Self) -> None: f"Expected: {old!r}, found: {new!r}" ) - def _get_expected_aot_namespaces(self) -> set[VariantNamespace]: - return { - namespace - for namespace, provider_info in self.providers.items() - if not provider_info.install_time - } + @property + def _aot_providers_need_static_properties(self) -> bool: + return True def _process(self, variant_table: VariantsJsonDict) -> None: validator = KeyTrackingValidator(None, variant_table) # type: ignore[arg-type] From cc4868c4a4a718f6c13d2fa3c7b3d5969689eb8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 31 Aug 2026 17:57:46 +0200 Subject: [PATCH 2/7] Implement `feature-order` key for provider information 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 | 8 +++----- tests/test_pyproject_toml.py | 19 +++++++++++-------- variantlib/api.py | 16 +++++++++------- variantlib/constants.py | 2 ++ variantlib/models/variant_info.py | 20 ++++++++++++++------ variantlib/variants_json.py | 6 ++++++ 6 files changed, 45 insertions(+), 26 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 05696bf1..39cba33d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -30,6 +30,7 @@ from variantlib.constants import VARIANT_INFO_PROPERTY_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_INSTALL_TIME_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY @@ -401,13 +402,13 @@ def test_make_variant_dist_info( "f1": ["v1", "v2"], "f2": ["v3", "v4"], }, + VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY: ["f2", "f1"], }, } ) expected[VARIANT_INFO_DEFAULT_PRIO_KEY].update( { VARIANT_INFO_NAMESPACE_KEY: ["ns1", "ns2", "ns3"], - VARIANT_INFO_FEATURE_KEY: {"ns3": ["f2", "f1"]}, }, ) @@ -417,7 +418,6 @@ def test_make_variant_dist_info( VARIANT_INFO_FEATURE_KEY: { "ns1": ["f2"], "ns2": ["f1", "f2"], - "ns3": ["f2", "f1"], }, VARIANT_INFO_PROPERTY_KEY: { "ns1": { @@ -701,13 +701,11 @@ def test_make_variant_dist_info_expand_aot_plugin_properties( if not install_time: provider_data[VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY] = False if requires and not install_time: - expected[VARIANT_INFO_DEFAULT_PRIO_KEY][VARIANT_INFO_FEATURE_KEY] = { - "aot_plugin": ["name1", "name2"], - } provider_data[VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY] = { "name1": ["val1a", "val1b"], "name2": ["val2a", "val2b", "val2c"], } + provider_data[VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY] = ["name1", "name2"] assert ( json.loads( diff --git a/tests/test_pyproject_toml.py b/tests/test_pyproject_toml.py index 9096f520..10d5b4fb 100644 --- a/tests/test_pyproject_toml.py +++ b/tests/test_pyproject_toml.py @@ -11,6 +11,7 @@ from variantlib.constants import VARIANT_INFO_PROPERTY_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_INSTALL_TIME_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY @@ -39,7 +40,6 @@ {VARIANT_INFO_NAMESPACE_KEY} = ["ns1", "ns2", "ns3"] {VARIANT_INFO_FEATURE_KEY}.ns1 = ["f2"] {VARIANT_INFO_FEATURE_KEY}.ns2 = ["f1", "f2"] -{VARIANT_INFO_FEATURE_KEY}.ns3 = ["f2", "f1"] {VARIANT_INFO_PROPERTY_KEY}.ns1.f2 = ["p1"] {VARIANT_INFO_PROPERTY_KEY}.ns2.f1 = ["p2"] @@ -59,6 +59,7 @@ [{PYPROJECT_TOML_TOP_KEY}.{VARIANT_INFO_PROVIDER_DATA_KEY}.ns3] {VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY} = false +{VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY} = ["f2", "f1"] [{PYPROJECT_TOML_TOP_KEY}.{VARIANT_INFO_PROVIDER_DATA_KEY}.ns3.{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY}] f1 = ["v1", "v2"] @@ -72,8 +73,9 @@ "\n".join( x for x in TOML_DATA.splitlines() - if not x.startswith((VARIANT_INFO_FEATURE_KEY, VARIANT_INFO_PROPERTY_KEY)) - or x.startswith(f"{VARIANT_INFO_FEATURE_KEY}.ns3") + if not x.startswith( + (f"{VARIANT_INFO_FEATURE_KEY}.", f"{VARIANT_INFO_PROPERTY_KEY}.") + ) ) ) @@ -84,7 +86,6 @@ def test_pyproject_toml() -> None: assert pyproj.feature_priorities == { "ns1": ["f2"], "ns2": ["f1", "f2"], - "ns3": ["f2", "f1"], } assert pyproj.property_priorities == { "ns1": {"f2": ["p1"]}, @@ -108,6 +109,7 @@ def test_pyproject_toml() -> None: "ns3": ProviderInfo( install_time=False, static_properties={"f1": ["v1", "v2"], "f2": ["v3", "v4"]}, + feature_order=["f2", "f1"], ), } @@ -115,7 +117,7 @@ def test_pyproject_toml() -> None: def test_pyproject_toml_minimal() -> None: pyproj = VariantPyProjectToml(PYPROJECT_TOML_MINIMAL) assert pyproj.namespace_priorities == ["ns1", "ns2", "ns3"] - assert pyproj.feature_priorities == {"ns3": ["f2", "f1"]} + assert pyproj.feature_priorities == {} assert pyproj.property_priorities == {} assert pyproj.providers == { "ns1": ProviderInfo( @@ -135,6 +137,7 @@ def test_pyproject_toml_minimal() -> None: "ns3": ProviderInfo( install_time=False, static_properties={"f1": ["v1", "v2"], "f2": ["v3", "v4"]}, + feature_order=["f2", "f1"], ), } @@ -432,7 +435,6 @@ def test_conversion(cls: type[VariantPyProjectToml | VariantsJson]) -> None: assert converted.feature_priorities == { "ns1": ["f2"], "ns2": ["f1", "f2"], - "ns3": ["f2", "f1"], } assert converted.property_priorities == { "ns1": {"f2": ["p1"]}, @@ -456,6 +458,7 @@ def test_conversion(cls: type[VariantPyProjectToml | VariantsJson]) -> None: "ns3": ProviderInfo( install_time=False, static_properties={"f1": ["v1", "v2"], "f2": ["v3", "v4"]}, + feature_order=["f2", "f1"], ), } @@ -546,7 +549,7 @@ def test_static_properties_missing_priorities() -> None: match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_PROVIDER_DATA_KEY}\.ns\." rf"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY}: " r"for AoT providers with multiple features, priorities need to be specified " - rf"via {VARIANT_INFO_DEFAULT_PRIO_KEY}\.{VARIANT_INFO_FEATURE_KEY}; missing: " + rf"via {VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY}; missing: " r"{'f2'}", ): VariantPyProjectToml( @@ -554,7 +557,6 @@ def test_static_properties_missing_priorities() -> None: PYPROJECT_TOML_TOP_KEY: { VARIANT_INFO_DEFAULT_PRIO_KEY: { VARIANT_INFO_NAMESPACE_KEY: ["ns"], - VARIANT_INFO_FEATURE_KEY: {"ns": ["f1"]}, }, VARIANT_INFO_PROVIDER_DATA_KEY: { "ns": { @@ -563,6 +565,7 @@ def test_static_properties_missing_priorities() -> None: "f1": ["v"], "f2": ["v"], }, + VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY: ["f1"], } }, } diff --git a/variantlib/api.py b/variantlib/api.py index b3940610..97a40f92 100644 --- a/variantlib/api.py +++ b/variantlib/api.py @@ -92,6 +92,10 @@ def get_variants_by_priority( feature_priorities=aggregate_feature_priorities( config.feature_priorities, variants_json.feature_priorities, + { + namespace: provider.feature_order + for namespace, provider in variants_json.providers.items() + }, ), property_priorities=aggregate_property_priorities( config.property_priorities, @@ -245,13 +249,11 @@ def make_variant_dist_info( vfeat.name ] = vfeat.values - # adjust feature priorities only if at least 2 features defined - if len(config.configs) > 1: - feature_prios = variant_json.feature_priorities.setdefault( - config.namespace, [] - ) - if vfeat.name not in feature_prios: - feature_prios.append(vfeat.name) + # set feature-order only if at least 2 features defined + if len(config.configs) > 1: + variant_json.providers[config.namespace].feature_order = [ + vfeat.name for vfeat in config.configs + ] # Validate that we did not end up using an unsupported property. # This could happen in two cases: diff --git a/variantlib/constants.py b/variantlib/constants.py index 6310b9b4..c1a453f0 100644 --- a/variantlib/constants.py +++ b/variantlib/constants.py @@ -22,6 +22,7 @@ VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: Literal["static-properties"] = ( "static-properties" ) +VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY: Literal["feature-order"] = "feature-order" PYPROJECT_TOML_TOP_KEY = "variant" @@ -121,6 +122,7 @@ class PriorityJsonDict(TypedDict, total=False): "plugin-api": str, "requires": list[str], "static-properties": dict[str, list[str]], + "feature-order": list[str], }, total=False, ) diff --git a/variantlib/models/variant_info.py b/variantlib/models/variant_info.py index 322df4f7..109ffe35 100644 --- a/variantlib/models/variant_info.py +++ b/variantlib/models/variant_info.py @@ -19,6 +19,7 @@ from variantlib.constants import VARIANT_INFO_PROPERTY_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_INSTALL_TIME_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY @@ -43,6 +44,7 @@ class ProviderInfo: static_properties: dict[VariantFeatureName, list[VariantFeatureValue]] = field( default_factory=dict ) + feature_order: list[VariantFeatureName] = field(default_factory=list) def __post_init__(self) -> None: # TODO: readd validation for requires/static-properties @@ -97,6 +99,7 @@ def copy_as_kwargs(self) -> dict[str, Any]: feature: list(values) for feature, values in provider_data.static_properties.items() }, + feature_order=list(provider_data.feature_order), ) for namespace, provider_data in self.providers.items() }, @@ -205,6 +208,12 @@ def _process_common(self, validator: KeyTrackingValidator) -> None: VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY, bool, True ) as provider_install_time: pass + with validator.get( + VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY, + list[VariantFeatureName], + [], + ) as provider_feature_order: + validator.list_matches_re(VALIDATION_FEATURE_NAME_REGEX) provider_static_properties = {} with validator.get( VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY, @@ -222,9 +231,7 @@ def _process_common(self, validator: KeyTrackingValidator) -> None: ) if len(feature_dict) > 1: - feature_prios = set( - self.feature_priorities.get(namespace, []) - ) + feature_prios = set(provider_feature_order) missing_feature_prios = ( set(feature_dict.keys()) - feature_prios ) @@ -232,11 +239,11 @@ def _process_common(self, validator: KeyTrackingValidator) -> None: raise ValidationError( f"{validator.key}: for AoT providers with multiple " "features, priorities need to be specified via " - f"{VARIANT_INFO_DEFAULT_PRIO_KEY}." - f"{VARIANT_INFO_FEATURE_KEY}; missing: " - f"{missing_feature_prios}" + f"{VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY}; " + f"missing: {missing_feature_prios}" ) + # TODO: check for exclusive elements properly if provider_install_time: if not provider_requires: raise ValidationError( @@ -266,6 +273,7 @@ def _process_common(self, validator: KeyTrackingValidator) -> None: plugin_api=provider_plugin_api, requires=list(provider_requires), static_properties=provider_static_properties, + feature_order=provider_feature_order, ) all_providers = set(self.providers.keys()) diff --git a/variantlib/variants_json.py b/variantlib/variants_json.py index 82e8f8f6..024ea2d4 100644 --- a/variantlib/variants_json.py +++ b/variantlib/variants_json.py @@ -15,6 +15,7 @@ from variantlib.constants import VARIANT_INFO_PROPERTY_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_INSTALL_TIME_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY @@ -75,6 +76,11 @@ def _provider_info_to_json( VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY, provider_info.static_properties, ) + if provider_info.feature_order: + yield ( + VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY, + provider_info.feature_order, + ) def _priorities_to_json(self) -> Generator[tuple[str, Any]]: yield (VARIANT_INFO_NAMESPACE_KEY, self.namespace_priorities) From d02ede03777e809819c4b078285c3f5fddc164e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 31 Aug 2026 18:17:02 +0200 Subject: [PATCH 3/7] Remove feature and property priorities from variant metadata 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 | 18 +---- .../sandbox_project-1.0.0-variants.json | 4 +- tests/commands/test_update_pyproject_toml.py | 4 - tests/test_api.py | 25 +------ tests/test_pyproject_toml.py | 75 ------------------- tests/test_variant_dist_info.py | 4 - tests/test_variants_json.py | 47 ++---------- variantlib/api.py | 2 - variantlib/constants.py | 4 - variantlib/models/variant_info.py | 69 ++--------------- variantlib/variants_json.py | 12 +-- 11 files changed, 17 insertions(+), 247 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 8f5c35bc..b5dc51f6 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 @@ -4,23 +4,7 @@ "namespace": [ "fictional_hw", "fictional_tech" - ], - "feature": { - "fictional_hw": [ - "humor", - "compute_accuracy" - ], - "fictional_tech": [ - "quantum" - ] - }, - "property": { - "fictional_tech": { - "technology": [ - "auto_chef" - ] - } - } + ] }, "providers": { "fictional_hw": { diff --git a/tests/artifacts/variant_json_files/sandbox_project-1.0.0-variants.json b/tests/artifacts/variant_json_files/sandbox_project-1.0.0-variants.json index 5c5e6591..6a5ad22e 100644 --- a/tests/artifacts/variant_json_files/sandbox_project-1.0.0-variants.json +++ b/tests/artifacts/variant_json_files/sandbox_project-1.0.0-variants.json @@ -4,9 +4,7 @@ "namespace": [ "fictional_tech", "fictional_hw" - ], - "feature": {}, - "property": {} + ] }, "providers": { "fictional_hw": { diff --git a/tests/commands/test_update_pyproject_toml.py b/tests/commands/test_update_pyproject_toml.py index 735f4f61..41eb7dcd 100644 --- a/tests/commands/test_update_pyproject_toml.py +++ b/tests/commands/test_update_pyproject_toml.py @@ -7,9 +7,7 @@ from variantlib.commands.main import main from variantlib.constants import PYPROJECT_TOML_TOP_KEY from variantlib.constants import VARIANT_INFO_DEFAULT_PRIO_KEY -from variantlib.constants import VARIANT_INFO_FEATURE_KEY from variantlib.constants import VARIANT_INFO_NAMESPACE_KEY -from variantlib.constants import VARIANT_INFO_PROPERTY_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 @@ -32,8 +30,6 @@ def test_update_pyproject_toml( "test_namespace", "foo", ], - VARIANT_INFO_FEATURE_KEY: ["foo::bar"], - VARIANT_INFO_PROPERTY_KEY: ["foo::bar::baz"], }, VARIANT_INFO_PROVIDER_DATA_KEY: { "test_namespace": { diff --git a/tests/test_api.py b/tests/test_api.py index 39cba33d..c490e9b0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -25,9 +25,7 @@ from variantlib.constants import VALIDATION_NAMESPACE_REGEX from variantlib.constants import VALIDATION_VALUE_REGEX from variantlib.constants import VARIANT_INFO_DEFAULT_PRIO_KEY -from variantlib.constants import VARIANT_INFO_FEATURE_KEY from variantlib.constants import VARIANT_INFO_NAMESPACE_KEY -from variantlib.constants import VARIANT_INFO_PROPERTY_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 @@ -57,7 +55,6 @@ from variantlib.variants_json import VariantsJson from tests.test_pyproject_toml import PYPROJECT_TOML -from tests.test_pyproject_toml import PYPROJECT_TOML_MINIMAL from tests.utils import get_combinations if TYPE_CHECKING: @@ -354,9 +351,7 @@ def test_validate_variant(optional: bool) -> None: assert not res.is_valid() -@pytest.mark.parametrize( - "pyproject_toml", [None, PYPROJECT_TOML, PYPROJECT_TOML_MINIMAL] -) +@pytest.mark.parametrize("pyproject_toml", [None, PYPROJECT_TOML]) @pytest.mark.parametrize("label", ["foo", "xy1.2"]) def test_make_variant_dist_info( pyproject_toml: VariantsJsonDict | None, @@ -412,24 +407,6 @@ def test_make_variant_dist_info( }, ) - if pyproject_toml is PYPROJECT_TOML: - expected[VARIANT_INFO_DEFAULT_PRIO_KEY].update( - { - VARIANT_INFO_FEATURE_KEY: { - "ns1": ["f2"], - "ns2": ["f1", "f2"], - }, - VARIANT_INFO_PROPERTY_KEY: { - "ns1": { - "f2": ["p1"], - }, - "ns2": { - "f1": ["p2"], - }, - }, - } - ) - assert ( json.loads( make_variant_dist_info( diff --git a/tests/test_pyproject_toml.py b/tests/test_pyproject_toml.py index 10d5b4fb..77697307 100644 --- a/tests/test_pyproject_toml.py +++ b/tests/test_pyproject_toml.py @@ -6,9 +6,7 @@ import pytest from variantlib.constants import PYPROJECT_TOML_TOP_KEY from variantlib.constants import VARIANT_INFO_DEFAULT_PRIO_KEY -from variantlib.constants import VARIANT_INFO_FEATURE_KEY from variantlib.constants import VARIANT_INFO_NAMESPACE_KEY -from variantlib.constants import VARIANT_INFO_PROPERTY_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 @@ -38,10 +36,6 @@ [{PYPROJECT_TOML_TOP_KEY}.{VARIANT_INFO_DEFAULT_PRIO_KEY}] {VARIANT_INFO_NAMESPACE_KEY} = ["ns1", "ns2", "ns3"] -{VARIANT_INFO_FEATURE_KEY}.ns1 = ["f2"] -{VARIANT_INFO_FEATURE_KEY}.ns2 = ["f1", "f2"] -{VARIANT_INFO_PROPERTY_KEY}.ns1.f2 = ["p1"] -{VARIANT_INFO_PROPERTY_KEY}.ns2.f1 = ["p2"] [{PYPROJECT_TOML_TOP_KEY}.{VARIANT_INFO_PROVIDER_DATA_KEY}.ns1] {VARIANT_INFO_PROVIDER_REQUIRES_KEY} = ["ns1-provider >= 1.2.3"] @@ -68,57 +62,10 @@ PYPROJECT_TOML = tomllib.loads(TOML_DATA) -PYPROJECT_TOML_MINIMAL = tomllib.loads( - # remove truly optional keys - "\n".join( - x - for x in TOML_DATA.splitlines() - if not x.startswith( - (f"{VARIANT_INFO_FEATURE_KEY}.", f"{VARIANT_INFO_PROPERTY_KEY}.") - ) - ) -) - def test_pyproject_toml() -> None: pyproj = VariantPyProjectToml(PYPROJECT_TOML) assert pyproj.namespace_priorities == ["ns1", "ns2", "ns3"] - assert pyproj.feature_priorities == { - "ns1": ["f2"], - "ns2": ["f1", "f2"], - } - assert pyproj.property_priorities == { - "ns1": {"f2": ["p1"]}, - "ns2": {"f1": ["p2"]}, - } - 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( - requires=[ - "ns2_provider; python_version >= '3.11'", - "old_ns2_provider; python_version < '3.11'", - ], - optional=True, - plugin_api="ns2_provider:Plugin", - install_time=False, - ), - "ns3": ProviderInfo( - install_time=False, - static_properties={"f1": ["v1", "v2"], "f2": ["v3", "v4"]}, - feature_order=["f2", "f1"], - ), - } - - -def test_pyproject_toml_minimal() -> None: - pyproj = VariantPyProjectToml(PYPROJECT_TOML_MINIMAL) - assert pyproj.namespace_priorities == ["ns1", "ns2", "ns3"] - assert pyproj.feature_priorities == {} - assert pyproj.property_priorities == {} assert pyproj.providers == { "ns1": ProviderInfo( requires=["ns1-provider >= 1.2.3"], @@ -167,8 +114,6 @@ def test_invalid_table_type(table: str) -> None: ("key", "expected"), [ (VARIANT_INFO_NAMESPACE_KEY, r"list\[str\]"), - (VARIANT_INFO_FEATURE_KEY, r"dict\[str, list\[str\]\]"), - (VARIANT_INFO_PROPERTY_KEY, r"dict\[str\, dict\[str, list\[str\]\]\]"), ], ) def test_invalid_priority_type(key: str, expected: str) -> None: @@ -194,16 +139,6 @@ def test_invalid_priority_type(key: str, expected: str) -> None: ["ns", "ns :: feature"], r"\[1\]: Value `ns :: feature`", ), - ( - VARIANT_INFO_FEATURE_KEY, - {"ns": ["feature", "feature :: property"]}, - r"\.ns\[1\]: Value `feature :: property`", - ), - ( - VARIANT_INFO_PROPERTY_KEY, - {"ns": {"feature": ["property", "not valid"]}}, - r".ns.feature\[1\]: Value `not valid`", - ), ], ) def test_invalid_priority_value(key: str, value: Any, expected: str) -> None: @@ -425,21 +360,11 @@ def test_conversion(cls: type[VariantPyProjectToml | VariantsJson]) -> None: # Mangle the original to ensure everything was copied pyproj.namespace_priorities.append("ns4") - pyproj.feature_priorities["ns4"] = ["foo"] - pyproj.property_priorities["ns2"]["foo"] = ["bar"] 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.feature_priorities == { - "ns1": ["f2"], - "ns2": ["f1", "f2"], - } - assert converted.property_priorities == { - "ns1": {"f2": ["p1"]}, - "ns2": {"f1": ["p2"]}, - } assert converted.providers == { "ns1": ProviderInfo( requires=["ns1-provider >= 1.2.3"], diff --git a/tests/test_variant_dist_info.py b/tests/test_variant_dist_info.py index c6c2850d..7df648f8 100644 --- a/tests/test_variant_dist_info.py +++ b/tests/test_variant_dist_info.py @@ -40,8 +40,6 @@ def test_variant_dist_info(json_type: type, expected_label: str | None) -> None: ) variant_dist_info = VariantDistInfo(vjson_str, expected_label=expected_label) assert variant_dist_info.namespace_priorities == ["ns"] - assert variant_dist_info.feature_priorities == {} - assert variant_dist_info.property_priorities == {} assert variant_dist_info.providers == {"ns": ProviderInfo(requires=["ns-pkg"])} vdesc = VariantDescription([VariantProperty("ns", "f", "v")], label="test") assert variant_dist_info.variants == {"test": vdesc} @@ -54,8 +52,6 @@ def test_variant_dist_info_custom_label(expected_label: str | None) -> None: vjson_str = json.dumps(VARIANT_JSON).replace("test", "fancy1") variant_dist_info = VariantDistInfo(vjson_str, expected_label=expected_label) assert variant_dist_info.namespace_priorities == ["ns"] - assert variant_dist_info.feature_priorities == {} - assert variant_dist_info.property_priorities == {} assert variant_dist_info.providers == {"ns": ProviderInfo(requires=["ns-pkg"])} vdesc = VariantDescription([VariantProperty("ns", "f", "v")], label="fancy1") assert variant_dist_info.variants == {"fancy1": vdesc} diff --git a/tests/test_variants_json.py b/tests/test_variants_json.py index 51d2ec7e..3ca5fded 100644 --- a/tests/test_variants_json.py +++ b/tests/test_variants_json.py @@ -8,9 +8,7 @@ import pytest from variantlib.constants import NULL_VARIANT_LABEL from variantlib.constants import VARIANT_INFO_DEFAULT_PRIO_KEY -from variantlib.constants import VARIANT_INFO_FEATURE_KEY from variantlib.constants import VARIANT_INFO_NAMESPACE_KEY -from variantlib.constants import VARIANT_INFO_PROPERTY_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 @@ -219,13 +217,6 @@ def test_validate_variants_json() -> None: ), } assert variants_json.namespace_priorities == ["fictional_hw", "fictional_tech"] - assert variants_json.feature_priorities == { - "fictional_hw": ["humor", "compute_accuracy"], - "fictional_tech": ["quantum"], - } - assert variants_json.property_priorities == { - "fictional_tech": {"technology": ["auto_chef"]} - } assert variants_json.providers == { "fictional_hw": ProviderInfo( requires=["provider-fictional-hw == 1.0.0"], @@ -260,20 +251,11 @@ def test_conversion(cls: type[VariantPyProjectToml | VariantsJson]) -> None: # Mangle variants_json to ensure everything was copied variants_json.namespace_priorities.append("ns") - variants_json.feature_priorities["ns"] = ["foo"] - variants_json.property_priorities["fictional_tech"]["foo"] = ["bar"] 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.feature_priorities == { - "fictional_hw": ["humor", "compute_accuracy"], - "fictional_tech": ["quantum"], - } - assert converted.property_priorities == { - "fictional_tech": {"technology": ["auto_chef"]} - } assert converted.providers == { "fictional_hw": ProviderInfo( requires=["provider-fictional-hw == 1.0.0"], @@ -295,14 +277,6 @@ def test_to_str() -> None: variants_json = VariantsJson( VariantInfo( namespace_priorities=["ns2", "ns1"], - feature_priorities={ - "ns1": ["f1"], - "ns2": ["f2"], - }, - property_priorities={ - "ns2": {"f2": ["v2"]}, - "ns1": {"f1": ["v1"]}, - }, providers={ "ns1": ProviderInfo( requires=["ns1-pkg >= 1.0.0", "ns1-dep"], @@ -334,8 +308,6 @@ def test_to_str() -> None: VARIANTS_JSON_SCHEMA_KEY: VARIANTS_JSON_SCHEMA_URL, VARIANT_INFO_DEFAULT_PRIO_KEY: { VARIANT_INFO_NAMESPACE_KEY: ["ns2", "ns1"], - VARIANT_INFO_FEATURE_KEY: {"ns1": ["f1"], "ns2": ["f2"]}, - VARIANT_INFO_PROPERTY_KEY: {"ns2": {"f2": ["v2"]}, "ns1": {"f1": ["v1"]}}, }, VARIANT_INFO_PROVIDER_DATA_KEY: { "ns1": { @@ -367,8 +339,6 @@ def test_roundtrip() -> None: def test_merge_variants() -> None: priority_data: PriorityJsonDict = { VARIANT_INFO_NAMESPACE_KEY: ["a", "b"], - VARIANT_INFO_FEATURE_KEY: {"a": ["a"], "b": ["b"]}, - VARIANT_INFO_PROPERTY_KEY: {"a": {"a": ["a"]}, "b": {"b": ["b"]}}, } provider_data: dict[str, ProviderPluginJsonDict] = { @@ -466,17 +436,12 @@ def test_merge_variants() -> None: assert v1 == merged # Test for mismatches in default priorities. - overrides = { - VARIANT_INFO_NAMESPACE_KEY: ["b", "a"], - VARIANT_INFO_FEATURE_KEY: {"b": ["b"]}, - VARIANT_INFO_PROPERTY_KEY: {"b": {"b": ["b"]}}, - } - - for key in json_a[VARIANT_INFO_DEFAULT_PRIO_KEY]: - _json_data = copy.deepcopy(json_b) - _json_data[VARIANT_INFO_DEFAULT_PRIO_KEY][key] = overrides[key] # type: ignore[literal-required] - with pytest.raises(ValidationError, match=rf"Inconsistency in '{key}"): - v1.merge(VariantsJson(_json_data)) + _json_data = copy.deepcopy(json_b) + _json_data[VARIANT_INFO_DEFAULT_PRIO_KEY][VARIANT_INFO_NAMESPACE_KEY] = ["b", "a"] + with pytest.raises( + ValidationError, match=rf"Inconsistency in '{VARIANT_INFO_NAMESPACE_KEY}" + ): + v1.merge(VariantsJson(_json_data)) # Test for mismatches in provider information. _json_data = copy.deepcopy(json_b) diff --git a/variantlib/api.py b/variantlib/api.py index 97a40f92..d6c3c491 100644 --- a/variantlib/api.py +++ b/variantlib/api.py @@ -91,7 +91,6 @@ def get_variants_by_priority( ), feature_priorities=aggregate_feature_priorities( config.feature_priorities, - variants_json.feature_priorities, { namespace: provider.feature_order for namespace, provider in variants_json.providers.items() @@ -99,7 +98,6 @@ def get_variants_by_priority( ), property_priorities=aggregate_property_priorities( config.property_priorities, - variants_json.property_priorities, ), ) ] diff --git a/variantlib/constants.py b/variantlib/constants.py index c1a453f0..7f92f12d 100644 --- a/variantlib/constants.py +++ b/variantlib/constants.py @@ -10,9 +10,7 @@ # Common variant info keys (used in pyproject.toml and variants.json) VARIANT_INFO_DEFAULT_PRIO_KEY: Literal["default-priorities"] = "default-priorities" -VARIANT_INFO_FEATURE_KEY: Literal["feature"] = "feature" VARIANT_INFO_NAMESPACE_KEY: Literal["namespace"] = "namespace" -VARIANT_INFO_PROPERTY_KEY: Literal["property"] = "property" VARIANT_INFO_PROVIDER_DATA_KEY: Literal["providers"] = "providers" VARIANT_INFO_PROVIDER_ENABLE_IF_KEY: Literal["enable-if"] = "enable-if" VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: Literal["install-time"] = "install-time" @@ -109,8 +107,6 @@ class PriorityJsonDict(TypedDict, total=False): namespace: list[str] - feature: dict[str, list[str]] - property: dict[str, dict[str, list[str]]] ProviderPluginJsonDict = TypedDict( diff --git a/variantlib/models/variant_info.py b/variantlib/models/variant_info.py index 109ffe35..f709f259 100644 --- a/variantlib/models/variant_info.py +++ b/variantlib/models/variant_info.py @@ -14,9 +14,7 @@ from variantlib.constants import VALIDATION_PROVIDER_REQUIRES_REGEX from variantlib.constants import VALIDATION_VALUE_REGEX from variantlib.constants import VARIANT_INFO_DEFAULT_PRIO_KEY -from variantlib.constants import VARIANT_INFO_FEATURE_KEY from variantlib.constants import VARIANT_INFO_NAMESPACE_KEY -from variantlib.constants import VARIANT_INFO_PROPERTY_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 @@ -63,12 +61,6 @@ def object_reference(self) -> str: @dataclass class VariantInfo: namespace_priorities: list[VariantNamespace] = field(default_factory=list) - feature_priorities: dict[VariantNamespace, list[VariantFeatureName]] = field( - default_factory=dict - ) - property_priorities: dict[ - VariantNamespace, dict[VariantFeatureName, list[VariantFeatureValue]] - ] = field(default_factory=dict) providers: dict[VariantNamespace, ProviderInfo] = field(default_factory=dict) @@ -77,17 +69,6 @@ def copy_as_kwargs(self) -> dict[str, Any]: return { "namespace_priorities": list(self.namespace_priorities), - "feature_priorities": { - namespace: list(feature_priorities) - for namespace, feature_priorities in self.feature_priorities.items() - }, - "property_priorities": { - namespace: { - feature: list(property_priorities) - for feature, property_priorities in feature_dict.items() - } - for namespace, feature_dict in self.property_priorities.items() - }, "providers": { namespace: ProviderInfo( enable_if=provider_data.enable_if, @@ -133,50 +114,14 @@ def _aot_providers_need_static_properties(self) -> bool: raise NotImplementedError def _process_common(self, validator: KeyTrackingValidator) -> None: - with validator.get(VARIANT_INFO_DEFAULT_PRIO_KEY, dict[str, Any], {}): - with validator.get( + with ( + validator.get(VARIANT_INFO_DEFAULT_PRIO_KEY, dict[str, Any], {}), + validator.get( VARIANT_INFO_NAMESPACE_KEY, list[VariantNamespace], [] - ) as namespace_priorities: - validator.list_matches_re(VALIDATION_NAMESPACE_REGEX) - self.namespace_priorities = list(namespace_priorities) - - with validator.get( - VARIANT_INFO_FEATURE_KEY, - dict[VariantNamespace, list[VariantFeatureName]], - {}, - ) as feature_priorities_dict: - validator.list_matches_re(VALIDATION_NAMESPACE_REGEX) - self.feature_priorities = {} - for namespace in feature_priorities_dict: - with validator.get( - namespace, list[VariantFeatureName] - ) as feature_priorities: - validator.list_matches_re(VALIDATION_FEATURE_NAME_REGEX) - self.feature_priorities[namespace] = feature_priorities - - with validator.get( - VARIANT_INFO_PROPERTY_KEY, - dict[ - VariantNamespace, - dict[VariantFeatureName, list[VariantFeatureValue]], - ], - {}, - ) as property_priorities_dict: - validator.list_matches_re(VALIDATION_NAMESPACE_REGEX) - self.property_priorities = {} - for namespace in property_priorities_dict: - with validator.get( - namespace, dict[VariantFeatureName, list[VariantFeatureValue]] - ) as feature_dict: - validator.list_matches_re(VALIDATION_FEATURE_NAME_REGEX) - for feature_name in feature_dict: - with validator.get( - feature_name, list[VariantFeatureValue] - ) as value_priorities: - validator.list_matches_re(VALIDATION_VALUE_REGEX) - self.property_priorities.setdefault(namespace, {})[ - feature_name - ] = value_priorities + ) as namespace_priorities, + ): + validator.list_matches_re(VALIDATION_NAMESPACE_REGEX) + self.namespace_priorities = list(namespace_priorities) with validator.get( VARIANT_INFO_PROVIDER_DATA_KEY, dict[str, Any], {} diff --git a/variantlib/variants_json.py b/variantlib/variants_json.py index 024ea2d4..7497ef9e 100644 --- a/variantlib/variants_json.py +++ b/variantlib/variants_json.py @@ -10,9 +10,7 @@ from variantlib.constants import NULL_VARIANT_LABEL from variantlib.constants import VALIDATION_VARIANT_LABEL_REGEX from variantlib.constants import VARIANT_INFO_DEFAULT_PRIO_KEY -from variantlib.constants import VARIANT_INFO_FEATURE_KEY from variantlib.constants import VARIANT_INFO_NAMESPACE_KEY -from variantlib.constants import VARIANT_INFO_PROPERTY_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 @@ -84,10 +82,6 @@ def _provider_info_to_json( def _priorities_to_json(self) -> Generator[tuple[str, Any]]: yield (VARIANT_INFO_NAMESPACE_KEY, self.namespace_priorities) - if self.feature_priorities: - yield (VARIANT_INFO_FEATURE_KEY, self.feature_priorities) - if self.property_priorities: - yield (VARIANT_INFO_PROPERTY_KEY, self.property_priorities) def providers_dict( self, @@ -127,11 +121,7 @@ def merge(self, variant_dist_info: Self) -> None: self.variants.update(variant_dist_info.variants) # Verify consistency of default priorities - for attribute in ( - "namespace_priorities", - "feature_priorities", - "property_priorities", - ): + for attribute in ("namespace_priorities",): new_value = getattr(variant_dist_info, attribute) old_value = getattr(self, attribute) if old_value != new_value: From d1142b551c1540ef744f15abc12c0bf358f1b879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 1 Sep 2026 16:36:58 +0200 Subject: [PATCH 4/7] Implement `build-requires`, remove `install-time` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync with the latest drafts of the providers and building PEPs. Add the `build-requires` key, remove `install-time`. Implement the checks for mutually exclusive fields. Signed-off-by: Michał Górny --- tests/plugins/test_loader.py | 4 +- tests/test_api.py | 35 ++---- tests/test_pyproject_toml.py | 172 ++++++++++++++++++++---------- tests/test_variants_json.py | 19 ++++ variantlib/api.py | 5 +- variantlib/constants.py | 4 +- variantlib/models/variant_info.py | 101 +++++++++++------- variantlib/plugins/_subprocess.py | 2 +- variantlib/plugins/loader.py | 4 +- variantlib/protocols.py | 4 +- variantlib/pyproject_toml.py | 4 +- variantlib/variants_json.py | 13 ++- 12 files changed, 228 insertions(+), 139 deletions(-) diff --git a/tests/plugins/test_loader.py b/tests/plugins/test_loader.py index 046488ec..c9e19183 100644 --- a/tests/plugins/test_loader.py +++ b/tests/plugins/test_loader.py @@ -571,12 +571,10 @@ def test_package_defined_properties(include_aot_plugins: bool) -> None: requires=["variantlib"], plugin_api="tests.mocked_plugins:MockedPluginA" ), "second_namespace": ProviderInfo( - requires=["variantlib"], + build_requires=["variantlib"], plugin_api="tests.mocked_plugins:MockedPluginB", - install_time=False, ), "private": ProviderInfo( - install_time=False, static_properties={ "baz": ["v5", "v6"], }, diff --git a/tests/test_api.py b/tests/test_api.py index c490e9b0..4d744e97 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -26,10 +26,10 @@ from variantlib.constants import VALIDATION_VALUE_REGEX from variantlib.constants import VARIANT_INFO_DEFAULT_PRIO_KEY 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_INSTALL_TIME_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_REQUIRES_KEY @@ -298,10 +298,9 @@ def test_validate_variant(optional: bool) -> None: optional=optional, ), "second_namespace": ProviderInfo( - requires=["variantlib"], + build_requires=["variantlib"], plugin_api="tests.mocked_plugins:MockedPluginB", optional=optional, - install_time=False, ), "incompatible_namespace": ProviderInfo( requires=["variantlib"], @@ -309,9 +308,7 @@ def test_validate_variant(optional: bool) -> None: optional=optional, ), "private": ProviderInfo( - plugin_api="donotuseme", optional=optional, - install_time=False, static_properties={"build_type": ["debug", "release"]}, ), }, @@ -383,16 +380,14 @@ def test_make_variant_dist_info( VARIANT_INFO_PROVIDER_PLUGIN_API_KEY: "ns1_provider.plugin:NS1Plugin", # noqa: E501 }, "ns2": { - VARIANT_INFO_PROVIDER_REQUIRES_KEY: [ + VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY: [ "ns2_provider; python_version >= '3.11'", "old_ns2_provider; python_version < '3.11'", ], VARIANT_INFO_PROVIDER_PLUGIN_API_KEY: "ns2_provider:Plugin", VARIANT_INFO_PROVIDER_OPTIONAL_KEY: True, - VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: False, }, "ns3": { - VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: False, VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: { "f1": ["v1", "v2"], "f2": ["v3", "v4"], @@ -626,12 +621,9 @@ def test_get_variant_label() -> None: ) -@pytest.mark.parametrize( - ("install_time", "requires"), [(False, False), (False, True), (True, True)] -) +@pytest.mark.parametrize("install_time", [True, False]) def test_make_variant_dist_info_expand_aot_plugin_properties( install_time: bool, - requires: bool, ) -> None: vdesc = VariantDescription( [ @@ -644,10 +636,10 @@ def test_make_variant_dist_info_expand_aot_plugin_properties( namespace_priorities=["aot_plugin"], providers={ "aot_plugin": ProviderInfo( - install_time=install_time, optional=True, plugin_api=plugin_api, - requires=["variantlib"] if requires else [], + requires=["variantlib"] if install_time else [], + build_requires=["variantlib"] if not install_time else [], ) }, ) @@ -673,11 +665,9 @@ def test_make_variant_dist_info_expand_aot_plugin_properties( } provider_data = expected[VARIANT_INFO_PROVIDER_DATA_KEY]["aot_plugin"] - if requires: + if install_time: provider_data[VARIANT_INFO_PROVIDER_REQUIRES_KEY] = ["variantlib"] - if not install_time: - provider_data[VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY] = False - if requires and not install_time: + else: provider_data[VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY] = { "name1": ["val1a", "val1b"], "name2": ["val2a", "val2b", "val2c"], @@ -709,10 +699,9 @@ def test_make_variant_dist_info_invalid_aot_plugin_property() -> None: namespace_priorities=["aot_plugin"], providers={ "aot_plugin": ProviderInfo( - requires=["variantlib"], + build_requires=["variantlib"], plugin_api=plugin_api, optional=True, - install_time=False, ) }, ) @@ -741,10 +730,9 @@ def test_make_variant_dist_info_invalid_aot_plugin_multi_value() -> None: namespace_priorities=["aot_plugin"], providers={ "aot_plugin": ProviderInfo( - requires=["variantlib"], + build_requires=["variantlib"], plugin_api=plugin_api, optional=True, - install_time=False, ) }, ) @@ -772,9 +760,8 @@ def test_make_variant_dist_info_really_invalid_build_plugin() -> None: namespace_priorities=["second_namespace"], providers={ "second_namespace": ProviderInfo( - requires=["variantlib"], + build_requires=["variantlib"], plugin_api=plugin_api, - install_time=False, ) }, ) diff --git a/tests/test_pyproject_toml.py b/tests/test_pyproject_toml.py index 77697307..4a4a3191 100644 --- a/tests/test_pyproject_toml.py +++ b/tests/test_pyproject_toml.py @@ -7,10 +7,10 @@ from variantlib.constants import PYPROJECT_TOML_TOP_KEY from variantlib.constants import VARIANT_INFO_DEFAULT_PRIO_KEY 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_INSTALL_TIME_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_REQUIRES_KEY @@ -43,8 +43,7 @@ {VARIANT_INFO_PROVIDER_PLUGIN_API_KEY} = "ns1_provider.plugin:NS1Plugin" [{PYPROJECT_TOML_TOP_KEY}.{VARIANT_INFO_PROVIDER_DATA_KEY}.ns2] -{VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY} = false -{VARIANT_INFO_PROVIDER_REQUIRES_KEY} = [ +{VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY} = [ "ns2_provider; python_version >= '3.11'", "old_ns2_provider; python_version < '3.11'", ] @@ -52,7 +51,6 @@ {VARIANT_INFO_PROVIDER_OPTIONAL_KEY} = true [{PYPROJECT_TOML_TOP_KEY}.{VARIANT_INFO_PROVIDER_DATA_KEY}.ns3] -{VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY} = false {VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY} = ["f2", "f1"] [{PYPROJECT_TOML_TOP_KEY}.{VARIANT_INFO_PROVIDER_DATA_KEY}.ns3.{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY}] @@ -73,16 +71,14 @@ def test_pyproject_toml() -> None: plugin_api="ns1_provider.plugin:NS1Plugin", ), "ns2": ProviderInfo( - requires=[ + build_requires=[ "ns2_provider; python_version >= '3.11'", "old_ns2_provider; python_version < '3.11'", ], optional=True, plugin_api="ns2_provider:Plugin", - install_time=False, ), "ns3": ProviderInfo( - install_time=False, static_properties={"f1": ["v1", "v2"], "f2": ["v3", "v4"]}, feature_order=["f2", "f1"], ), @@ -240,11 +236,13 @@ def test_invalid_provider_plugin_api() -> None: ) -def test_missing_provider_requires() -> None: +def test_missing_required_key() -> None: with pytest.raises( ValidationError, match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_PROVIDER_DATA_KEY}\.ns: " - rf"{VARIANT_INFO_PROVIDER_REQUIRES_KEY} must be specified", + rf"exactly one of {VARIANT_INFO_PROVIDER_REQUIRES_KEY}, " + rf"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY} or " + rf"{VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY} must be specified", ): VariantPyProjectToml( { @@ -255,7 +253,6 @@ def test_missing_provider_requires() -> None: VARIANT_INFO_PROVIDER_DATA_KEY: { "ns": { VARIANT_INFO_PROVIDER_REQUIRES_KEY: [], - VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: True, } }, } @@ -263,25 +260,6 @@ def test_missing_provider_requires() -> None: ) -def test_missing_provider_requires_aot() -> 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: [], - VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: False, - VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: {"test": ["val"]}, - } - }, - } - } - ) - - def test_missing_namespace_priority() -> None: with pytest.raises( ValidationError, @@ -372,16 +350,14 @@ def test_conversion(cls: type[VariantPyProjectToml | VariantsJson]) -> None: plugin_api="ns1_provider.plugin:NS1Plugin", ), "ns2": ProviderInfo( - requires=[ + build_requires=[ "ns2_provider; python_version >= '3.11'", "old_ns2_provider; python_version < '3.11'", ], optional=True, plugin_api="ns2_provider:Plugin", - install_time=False, ), "ns3": ProviderInfo( - install_time=False, static_properties={"f1": ["v1", "v2"], "f2": ["v3", "v4"]}, feature_order=["f2", "f1"], ), @@ -433,25 +409,6 @@ def test_no_plugin_api() -> None: assert pyproject_toml.providers["ns"].object_reference == "my_plugin" -def test_missing_static_properties() -> None: - with pytest.raises( - ValidationError, - match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_PROVIDER_DATA_KEY}\.ns: " - rf"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY} or " - rf"{VARIANT_INFO_PROVIDER_REQUIRES_KEY} must be specified for AoT providers", - ): - VariantPyProjectToml( - { - PYPROJECT_TOML_TOP_KEY: { - VARIANT_INFO_DEFAULT_PRIO_KEY: {VARIANT_INFO_NAMESPACE_KEY: ["ns"]}, - VARIANT_INFO_PROVIDER_DATA_KEY: { - "ns": {VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: False} - }, - } - } - ) - - def test_static_properties_one_feature() -> None: VariantPyProjectToml( { @@ -459,7 +416,6 @@ def test_static_properties_one_feature() -> None: VARIANT_INFO_DEFAULT_PRIO_KEY: {VARIANT_INFO_NAMESPACE_KEY: ["ns"]}, VARIANT_INFO_PROVIDER_DATA_KEY: { "ns": { - VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: False, VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: {"f": ["v"]}, } }, @@ -472,9 +428,8 @@ def test_static_properties_missing_priorities() -> None: with pytest.raises( ValidationError, match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_PROVIDER_DATA_KEY}\.ns\." - rf"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY}: " - r"for AoT providers with multiple features, priorities need to be specified " - rf"via {VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY}; missing: " + rf"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY}: multiple features require " + rf"specifying ordering via {VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY}; missing: " r"{'f2'}", ): VariantPyProjectToml( @@ -485,7 +440,6 @@ def test_static_properties_missing_priorities() -> None: }, VARIANT_INFO_PROVIDER_DATA_KEY: { "ns": { - VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: False, VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: { "f1": ["v"], "f2": ["v"], @@ -496,3 +450,109 @@ def test_static_properties_missing_priorities() -> None: } } ) + + +@pytest.mark.parametrize( + "requires_key", + [VARIANT_INFO_PROVIDER_REQUIRES_KEY, VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY], +) +def test_static_properties_and_requires(requires_key: str) -> None: + with pytest.raises( + ValidationError, + match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_PROVIDER_DATA_KEY}\.ns: " + rf"exactly one of {VARIANT_INFO_PROVIDER_REQUIRES_KEY}, " + rf"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY} or " + rf"{VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY} must be specified", + ): + VariantPyProjectToml( + { + PYPROJECT_TOML_TOP_KEY: { + VARIANT_INFO_DEFAULT_PRIO_KEY: { + VARIANT_INFO_NAMESPACE_KEY: ["ns"], + }, + VARIANT_INFO_PROVIDER_DATA_KEY: { + "ns": { + requires_key: ["example"], + VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: {"f": ["v"]}, + } + }, + } + } + ) + + +def test_double_requires() -> None: + with pytest.raises( + ValidationError, + match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_PROVIDER_DATA_KEY}\.ns: " + rf"exactly one of {VARIANT_INFO_PROVIDER_REQUIRES_KEY}, " + rf"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY} or " + rf"{VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY} must be specified", + ): + VariantPyProjectToml( + { + PYPROJECT_TOML_TOP_KEY: { + VARIANT_INFO_DEFAULT_PRIO_KEY: { + VARIANT_INFO_NAMESPACE_KEY: ["ns"], + }, + VARIANT_INFO_PROVIDER_DATA_KEY: { + "ns": { + VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY: ["example"], + VARIANT_INFO_PROVIDER_REQUIRES_KEY: ["example"], + } + }, + } + } + ) + + +def test_static_properties_and_plugin_api() -> None: + with pytest.raises( + ValidationError, + match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_PROVIDER_DATA_KEY}\.ns: " + rf"{VARIANT_INFO_PROVIDER_PLUGIN_API_KEY} is not valid with " + rf"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY}", + ): + 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: "example", + VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: {"f": ["v"]}, + } + }, + } + } + ) + + +@pytest.mark.parametrize( + "requires_key", + [VARIANT_INFO_PROVIDER_REQUIRES_KEY, VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY], +) +def test_requires_and_feature_order(requires_key: str) -> None: + with pytest.raises( + ValidationError, + match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_PROVIDER_DATA_KEY}\.ns: " + rf"{VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY} is valid only with " + rf"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY}", + ): + VariantPyProjectToml( + { + PYPROJECT_TOML_TOP_KEY: { + VARIANT_INFO_DEFAULT_PRIO_KEY: { + VARIANT_INFO_NAMESPACE_KEY: ["ns"], + }, + VARIANT_INFO_PROVIDER_DATA_KEY: { + "ns": { + requires_key: ["example"], + VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY: ["f1", "f2"], + } + }, + } + } + ) diff --git a/tests/test_variants_json.py b/tests/test_variants_json.py index 3ca5fded..fcdd638f 100644 --- a/tests/test_variants_json.py +++ b/tests/test_variants_json.py @@ -9,6 +9,7 @@ from variantlib.constants import NULL_VARIANT_LABEL from variantlib.constants import VARIANT_INFO_DEFAULT_PRIO_KEY 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 @@ -478,3 +479,21 @@ def test_null_variant_label(): match=rf"Null variant must always use {NULL_VARIANT_LABEL!r} label", ): VariantsJson({VARIANTS_JSON_VARIANT_DATA_KEY: {"zuul": {}}}) + + +def test_build_requires(): + with pytest.raises( + ValidationError, + match=rf"{VARIANT_INFO_PROVIDER_DATA_KEY}.x: " + rf"{VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY} is not allowed in this file", + ): + VariantsJson( + { + VARIANT_INFO_PROVIDER_DATA_KEY: { + "x": { + VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY: ["example"], + } + }, + VARIANTS_JSON_VARIANT_DATA_KEY: {"test": {"x": {"y": ["z"]}}}, + } + ) diff --git a/variantlib/api.py b/variantlib/api.py index d6c3c491..6e738553 100644 --- a/variantlib/api.py +++ b/variantlib/api.py @@ -212,8 +212,7 @@ def make_variant_dist_info( ns for ns in namespaces if ns in variant_info.providers - and not variant_info.providers[ns].install_time - and variant_info.providers[ns].requires + and variant_info.providers[ns].build_requires } if build_namespaces: venv_python_executable = ( @@ -236,6 +235,8 @@ def make_variant_dist_info( for config in configs: if config.namespace not in build_namespaces: continue + assert variant_json.providers[config.namespace].build_requires + variant_json.providers[config.namespace].build_requires = [] variant_json.providers[config.namespace].static_properties = {} for vfeat in config.configs: if vfeat.multi_value: diff --git a/variantlib/constants.py b/variantlib/constants.py index 7f92f12d..81d4a904 100644 --- a/variantlib/constants.py +++ b/variantlib/constants.py @@ -12,8 +12,8 @@ VARIANT_INFO_DEFAULT_PRIO_KEY: Literal["default-priorities"] = "default-priorities" 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_INSTALL_TIME_KEY: Literal["install-time"] = "install-time" 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" @@ -112,8 +112,8 @@ class PriorityJsonDict(TypedDict, total=False): ProviderPluginJsonDict = TypedDict( "ProviderPluginJsonDict", { + "build-requires": list[str], "enable-if": str, - "install-time": bool, "optional": bool, "plugin-api": str, "requires": list[str], diff --git a/variantlib/models/variant_info.py b/variantlib/models/variant_info.py index f709f259..8d0a8afb 100644 --- a/variantlib/models/variant_info.py +++ b/variantlib/models/variant_info.py @@ -15,10 +15,10 @@ from variantlib.constants import VALIDATION_VALUE_REGEX from variantlib.constants import VARIANT_INFO_DEFAULT_PRIO_KEY 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_INSTALL_TIME_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_REQUIRES_KEY @@ -36,26 +36,37 @@ class ProviderInfo: plugin_api: str | None = None enable_if: str | None = None - install_time: bool = True optional: bool = False requires: list[str] = field(default_factory=list) static_properties: dict[VariantFeatureName, list[VariantFeatureValue]] = field( default_factory=dict ) feature_order: list[VariantFeatureName] = field(default_factory=list) + build_requires: list[str] = field(default_factory=list) def __post_init__(self) -> None: - # TODO: readd validation for requires/static-properties - pass + if ( + bool(self.build_requires), + bool(self.requires), + bool(self.static_properties), + ).count(True) != 1: + raise ValidationError( + "Exactly one of build_requires, requires and static_properties " + "must be provided" + ) + if self.static_properties and self.plugin_api: + raise ValidationError("plugin_api is invalid with static_properties") + if not self.static_properties and self.feature_order: + raise ValidationError("feature_order requires static_properties") @property def object_reference(self) -> str: """Get effective object reference from plugin-api or requires""" - assert self.requires + requires = self.requires or self.build_requires + assert requires if self.plugin_api is not None: return self.plugin_api - # TODO: how far should we normalize it? - return Requirement(self.requires[0]).name.replace("-", "_") + return Requirement(requires[0]).name.replace("-", "_") @dataclass @@ -72,7 +83,6 @@ def copy_as_kwargs(self) -> dict[str, Any]: "providers": { namespace: ProviderInfo( enable_if=provider_data.enable_if, - install_time=provider_data.install_time, optional=provider_data.optional, plugin_api=provider_data.plugin_api, requires=list(provider_data.requires), @@ -81,6 +91,7 @@ def copy_as_kwargs(self) -> dict[str, Any]: for feature, values in provider_data.static_properties.items() }, feature_order=list(provider_data.feature_order), + build_requires=list(provider_data.build_requires), ) for namespace, provider_data in self.providers.items() }, @@ -104,13 +115,15 @@ def get_provider_requires( requirements = set() for namespace in namespaces: provider = self.providers[namespace] - if not provider.install_time and not include_aot_plugins: - continue + # requires and build_requires are mutually exclusive, + # one of them will always be empty requirements.update(provider.requires) + if include_aot_plugins: + requirements.update(provider.build_requires) return requirements @property - def _aot_providers_need_static_properties(self) -> bool: + def _build_requires_allowed(self) -> bool: raise NotImplementedError def _process_common(self, validator: KeyTrackingValidator) -> None: @@ -149,16 +162,16 @@ def _process_common(self, validator: KeyTrackingValidator) -> 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_INSTALL_TIME_KEY, bool, True - ) as provider_install_time: - pass with validator.get( VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY, list[VariantFeatureName], [], ) as provider_feature_order: validator.list_matches_re(VALIDATION_FEATURE_NAME_REGEX) + with validator.get( + VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY, list[str], [] + ) as provider_build_requires: + validator.list_matches_re(VALIDATION_PROVIDER_REQUIRES_REGEX) provider_static_properties = {} with validator.get( VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY, @@ -182,43 +195,51 @@ def _process_common(self, validator: KeyTrackingValidator) -> None: ) if missing_feature_prios: raise ValidationError( - f"{validator.key}: for AoT providers with multiple " - "features, priorities need to be specified via " + f"{validator.key}: multiple features require " + "specifying ordering via " f"{VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY}; " f"missing: {missing_feature_prios}" ) - # TODO: check for exclusive elements properly - if provider_install_time: - if not provider_requires: - raise ValidationError( - f"{validator.key}: " - f"{VARIANT_INFO_PROVIDER_REQUIRES_KEY} must be " - "specified for install-time providers" - ) - elif not provider_static_properties: - if self._aot_providers_need_static_properties: - raise ValidationError( - f"{validator.key}: " - f"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY} " - "must be specified for AoT providers" - ) - if not provider_requires: - raise ValidationError( - f"{validator.key}: " - f"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY} or " - f"{VARIANT_INFO_PROVIDER_REQUIRES_KEY} must be " - "specified for AoT providers" - ) + if provider_build_requires and not self._build_requires_allowed: + raise ValidationError( + f"{validator.key}: " + f"{VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY} is not " + f"allowed in this file" + ) + if ( + bool(provider_build_requires), + bool(provider_requires), + bool(provider_static_properties), + ).count(True) != 1: + raise ValidationError( + f"{validator.key}: exactly one of " + f"{VARIANT_INFO_PROVIDER_REQUIRES_KEY}, " + f"{VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY} " + f"or {VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY} " + "must be specified" + ) + if provider_static_properties and provider_plugin_api: + raise ValidationError( + f"{validator.key}: " + f"{VARIANT_INFO_PROVIDER_PLUGIN_API_KEY} is not valid " + f"with {VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY}" + ) + if not provider_static_properties and provider_feature_order: + raise ValidationError( + f"{validator.key}: " + f"{VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY} is valid " + f"only with {VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY}" + ) self.providers[namespace] = ProviderInfo( enable_if=provider_enable_if, - install_time=provider_install_time, optional=provider_optional, plugin_api=provider_plugin_api, requires=list(provider_requires), static_properties=provider_static_properties, feature_order=provider_feature_order, + build_requires=provider_build_requires, ) all_providers = set(self.providers.keys()) diff --git a/variantlib/plugins/_subprocess.py b/variantlib/plugins/_subprocess.py index f8e91d06..0d5b5bd3 100644 --- a/variantlib/plugins/_subprocess.py +++ b/variantlib/plugins/_subprocess.py @@ -99,7 +99,7 @@ def main() -> int: if non_fixed_plugins: raise TypeError( f"Providers for namespaces {non_fixed_plugins} are not AoT plugins, " - f"they cannot be used with install-time = false" + f"they cannot be used with build-requires" ) retval: dict[str, Any] = {} diff --git a/variantlib/plugins/loader.py b/variantlib/plugins/loader.py index 781bb8c4..3ea32e56 100644 --- a/variantlib/plugins/loader.py +++ b/variantlib/plugins/loader.py @@ -322,10 +322,10 @@ def __init__( def _use_static_properties_for_provider(self, provider_data: ProviderInfo) -> bool: """Returns True if we should read properties from metadata""" # for install-time providers, we always query the plugin - if provider_data.install_time: + if provider_data.requires: return False # when there is no plugin, we always use metadata - if not provider_data.requires: + if not provider_data.build_requires: return True # otherwise, query the plugin if build-time querying is enabled return not self._include_aot_plugins diff --git a/variantlib/protocols.py b/variantlib/protocols.py index 73cf0eb6..70f61c3a 100644 --- a/variantlib/protocols.py +++ b/variantlib/protocols.py @@ -79,11 +79,11 @@ def namespace(self) -> VariantNamespace: @property def is_aot_plugin(self) -> bool: """ - Is this plugin valid for use with `install-time = false`? + Is this plugin valid for use with `build-requires`? 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 `install-time = false`, + This permits the plugin to be used via `build-requires`, where the supported properties are recorded at build time. If the value of `get_supported_configs()` may change in any way diff --git a/variantlib/pyproject_toml.py b/variantlib/pyproject_toml.py index 8f1bac8f..11c75f3a 100644 --- a/variantlib/pyproject_toml.py +++ b/variantlib/pyproject_toml.py @@ -41,8 +41,8 @@ def from_path(cls, path: Path) -> Self: return cls(tomllib.load(f)) @property - def _aot_providers_need_static_properties(self) -> bool: - return False + def _build_requires_allowed(self) -> bool: + return True def _process(self, variant_table: dict[str, VariantInfoJsonDict]) -> None: validator = KeyTrackingValidator(PYPROJECT_TOML_TOP_KEY, variant_table) diff --git a/variantlib/variants_json.py b/variantlib/variants_json.py index 7497ef9e..1891221f 100644 --- a/variantlib/variants_json.py +++ b/variantlib/variants_json.py @@ -11,10 +11,10 @@ from variantlib.constants import VALIDATION_VARIANT_LABEL_REGEX from variantlib.constants import VARIANT_INFO_DEFAULT_PRIO_KEY 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_INSTALL_TIME_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_REQUIRES_KEY @@ -67,8 +67,6 @@ def _provider_info_to_json( yield (VARIANT_INFO_PROVIDER_OPTIONAL_KEY, provider_info.optional) if provider_info.plugin_api is not None: yield (VARIANT_INFO_PROVIDER_PLUGIN_API_KEY, provider_info.plugin_api) - if not provider_info.install_time: - yield (VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY, provider_info.install_time) if provider_info.static_properties: yield ( VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY, @@ -79,6 +77,11 @@ def _provider_info_to_json( VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY, provider_info.feature_order, ) + if provider_info.build_requires: + yield ( + VARIANT_INFO_PROVIDER_BUILD_REQUIRES_KEY, + provider_info.build_requires, + ) def _priorities_to_json(self) -> Generator[tuple[str, Any]]: yield (VARIANT_INFO_NAMESPACE_KEY, self.namespace_priorities) @@ -156,8 +159,8 @@ def merge(self, variant_dist_info: Self) -> None: ) @property - def _aot_providers_need_static_properties(self) -> bool: - return True + def _build_requires_allowed(self) -> bool: + return False def _process(self, variant_table: VariantsJsonDict) -> None: validator = KeyTrackingValidator(None, variant_table) # type: ignore[arg-type] From 62b78c26b60d8d9e98cb1f71d40749450bf1aaec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 1 Sep 2026 18:44:41 +0200 Subject: [PATCH 5/7] Fix deprecations in commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- variantlib/commands/analyze_wheel.py | 3 +-- variantlib/commands/make_variant.py | 9 +++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/variantlib/commands/analyze_wheel.py b/variantlib/commands/analyze_wheel.py index c00065ca..30b38e43 100644 --- a/variantlib/commands/analyze_wheel.py +++ b/variantlib/commands/analyze_wheel.py @@ -8,7 +8,6 @@ from typing import TYPE_CHECKING from variantlib import __package_name__ -from variantlib.api import get_variant_label from variantlib.constants import VALIDATION_WHEEL_NAME_REGEX from variantlib.constants import VARIANT_DIST_INFO_FILENAME from variantlib.variant_dist_info import VariantDistInfo @@ -20,7 +19,7 @@ def pretty_print(vdesc: VariantDescription) -> str: - result_str = f"{'#' * 30} Variant: `{get_variant_label(vdesc)}` {'#' * 29}" + result_str = f"{'#' * 30} Variant: `{vdesc.label}` {'#' * 29}" for vprop in vdesc.properties: result_str += f"\n{vprop.to_str()}" result_str += f"\n{'#' * 80}\n" diff --git a/variantlib/commands/make_variant.py b/variantlib/commands/make_variant.py index cf424a03..cdd90023 100644 --- a/variantlib/commands/make_variant.py +++ b/variantlib/commands/make_variant.py @@ -16,7 +16,6 @@ from variantlib import __package_name__ from variantlib.api import VariantDescription from variantlib.api import VariantProperty -from variantlib.api import get_variant_label from variantlib.api import make_variant_dist_info from variantlib.api import validate_variant from variantlib.constants import VALIDATION_VARIANT_LABEL_REGEX @@ -169,7 +168,7 @@ def _make_variant( if not is_null_variant: # Transform properties into a VariantDescription - vdesc = VariantDescription(properties=properties) + vdesc = VariantDescription(properties=properties, label=variant_label or "") if validate_properties: env_factory: DefaultIsolatedEnv | nullcontext[None] @@ -230,11 +229,9 @@ def _make_variant( # Create a null variant vdesc = VariantDescription() - variant_label = get_variant_label(vdesc, variant_label) - # Determine output wheel filename output_filepath = ( - output_directory / f"{wheel_info.group('base_wheel_name')}-{variant_label}.whl" + output_directory / f"{wheel_info.group('base_wheel_name')}-{vdesc.label}.whl" ) with ( @@ -256,7 +253,7 @@ def _make_variant( # required, but a nice convention). dist_info_path = f"{components[0]}/{VARIANT_DIST_INFO_FILENAME}" dist_info_data = make_variant_dist_info( - vdesc, variant_info=variant_info, variant_label=variant_label + vdesc, variant_info=variant_info ) output_zip.writestr(dist_info_path, dist_info_data) From ba37f225c4b9e1b9bf1513ad423d2051975477eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 1 Sep 2026 19:33:43 +0200 Subject: [PATCH 6/7] Align metadata merging with the specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permit namespace_priorities to be different, as long as they can be consistently merged. Require provider information to be always the same. Signed-off-by: Michał Górny --- tests/test_variants_json.py | 20 +++++++++----- variantlib/variants_json.py | 53 ++++++++++++++----------------------- 2 files changed, 33 insertions(+), 40 deletions(-) diff --git a/tests/test_variants_json.py b/tests/test_variants_json.py index fcdd638f..1ea4ad1e 100644 --- a/tests/test_variants_json.py +++ b/tests/test_variants_json.py @@ -369,8 +369,12 @@ def test_merge_variants() -> None: }, } json_b: VariantsJsonDict = { - VARIANT_INFO_DEFAULT_PRIO_KEY: priority_data, - VARIANT_INFO_PROVIDER_DATA_KEY: provider_data, + VARIANT_INFO_DEFAULT_PRIO_KEY: { + VARIANT_INFO_NAMESPACE_KEY: ["a"], + }, + VARIANT_INFO_PROVIDER_DATA_KEY: { + "a": provider_data["a"], + }, VARIANTS_JSON_VARIANT_DATA_KEY: { "48b561bc": { "a": { @@ -437,21 +441,23 @@ def test_merge_variants() -> None: assert v1 == merged # Test for mismatches in default priorities. - _json_data = copy.deepcopy(json_b) + _json_data = copy.deepcopy(json_a) _json_data[VARIANT_INFO_DEFAULT_PRIO_KEY][VARIANT_INFO_NAMESPACE_KEY] = ["b", "a"] with pytest.raises( - ValidationError, match=rf"Inconsistency in '{VARIANT_INFO_NAMESPACE_KEY}" + ValidationError, + match=rf"Inconsistency in {VARIANT_INFO_DEFAULT_PRIO_KEY}\." + rf"{VARIANT_INFO_NAMESPACE_KEY}", ): v1.merge(VariantsJson(_json_data)) # Test for mismatches in provider information. - _json_data = copy.deepcopy(json_b) + _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="Inconsistency in providers when merging variants", + match=r"Inconsistency in providers\.b", ): v1.merge(VariantsJson(_json_data)) @@ -461,7 +467,7 @@ def test_merge_variants() -> None: ] = "test:Test" with pytest.raises( ValidationError, - match="Inconsistency in providers when merging variants", + match=r"Inconsistency in providers\.a", ): v1.merge(VariantsJson(_json_data)) diff --git a/variantlib/variants_json.py b/variantlib/variants_json.py index 1891221f..4bf9c868 100644 --- a/variantlib/variants_json.py +++ b/variantlib/variants_json.py @@ -111,52 +111,39 @@ def to_str(self) -> str: return json.dumps(data, indent=4, sort_keys=True) - @property - def provider_hash(self) -> int: - encoded_dict = json.dumps(self.providers_dict(), sort_keys=True).encode("utf-8") - - return hash(encoded_dict) - def merge(self, variant_dist_info: Self) -> None: """Merge info from another wheel (VariantsJson instance)""" # Merge the variant properties self.variants.update(variant_dist_info.variants) - # Verify consistency of default priorities - for attribute in ("namespace_priorities",): - new_value = getattr(variant_dist_info, attribute) - old_value = getattr(self, attribute) - if old_value != new_value: - raise ValidationError( - f"Inconsistency in {attribute!r} when merging variants. " - f"Expected: {old_value!r}, found {new_value!r}" - ) - - if self.provider_hash != variant_dist_info.provider_hash: + # Merge namespace priorities + # Both lists should start with the same values, the longer one + # is the result + namespace_priorities = sorted( + (self.namespace_priorities, variant_dist_info.namespace_priorities), key=len + ) + if ( + namespace_priorities[0] + != namespace_priorities[1][: len(namespace_priorities[0])] + ): raise ValidationError( - f"Inconsistency in providers when merging variants:\n" - f"Before:\n{self.providers}.\n\nAfter:\n{variant_dist_info.providers}." + f"Inconsistency in {VARIANT_INFO_DEFAULT_PRIO_KEY}." + f"{VARIANT_INFO_NAMESPACE_KEY} when merging variants. " + f"Unable to merge: {namespace_priorities!r}" ) + variant_dist_info.namespace_priorities = namespace_priorities[1] for namespace, provider_info in variant_dist_info.providers.items(): if (old_provider_info := self.providers.get(namespace)) is None: # If provider not yet specified, just copy it self.providers[namespace] = provider_info - - else: - # Otherwise, merge requirements and verify consistency - for req_str in provider_info.requires: - if req_str not in old_provider_info.requires: - old_provider_info.requires.append(req_str) - for attribute in ("enable_if", "optional", "plugin_api"): - new = getattr(provider_info, attribute) - old = getattr(old_provider_info, attribute) - if new != old: - raise ValidationError( - f"Inconsistency in providers[{namespace!r}].{attribute}. " - f"Expected: {old!r}, found: {new!r}" - ) + # Otherwise, verify consistency + elif provider_info != old_provider_info: + raise ValidationError( + f"Inconsistency in providers.{namespace}. " + f"Expected: {old_provider_info!r}, found: {provider_info!r}" + ) @property def _build_requires_allowed(self) -> bool: From f48cfba1b6b0a29b7221d9088b3ce6351b0fad19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 1 Sep 2026 19:43:29 +0200 Subject: [PATCH 7/7] Verify variant data consistency when merging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensure that variant data is consistent across labels. This was likely missing because labels used to be hashes, so consistency was implied. Signed-off-by: Michał Górny --- tests/test_variants_json.py | 28 +++++++++++++++++----------- variantlib/variants_json.py | 13 +++++++++++-- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/tests/test_variants_json.py b/tests/test_variants_json.py index 1ea4ad1e..e9a6faec 100644 --- a/tests/test_variants_json.py +++ b/tests/test_variants_json.py @@ -358,7 +358,7 @@ def test_merge_variants() -> None: VARIANT_INFO_DEFAULT_PRIO_KEY: priority_data, VARIANT_INFO_PROVIDER_DATA_KEY: provider_data, VARIANTS_JSON_VARIANT_DATA_KEY: { - "54357fe4": { + "foo": { "a": { "a": ["a"], }, @@ -376,13 +376,10 @@ def test_merge_variants() -> None: "a": provider_data["a"], }, VARIANTS_JSON_VARIANT_DATA_KEY: { - "48b561bc": { + "bar": { "a": { "a": ["c"], }, - "b": { - "b": ["b"], - }, } }, } @@ -391,15 +388,12 @@ def test_merge_variants() -> None: VARIANT_INFO_DEFAULT_PRIO_KEY: priority_data, VARIANT_INFO_PROVIDER_DATA_KEY: provider_data, VARIANTS_JSON_VARIANT_DATA_KEY: { - "48b561bc": { + "bar": { "a": { "a": ["c"], }, - "b": { - "b": ["b"], - }, }, - "54357fe4": { + "foo": { "a": { "a": ["a"], }, @@ -467,7 +461,19 @@ def test_merge_variants() -> None: ] = "test:Test" with pytest.raises( ValidationError, - match=r"Inconsistency in providers\.a", + match=rf"Inconsistency in {VARIANT_INFO_PROVIDER_DATA_KEY}\.a", + ): + v1.merge(VariantsJson(_json_data)) + + _json_data = copy.deepcopy(json_a) + _json_data[VARIANTS_JSON_VARIANT_DATA_KEY]["foo"] = { + "a": { + "a": ["a"], + }, + } + with pytest.raises( + ValidationError, + match=rf"Inconsistency in {VARIANTS_JSON_VARIANT_DATA_KEY}\.foo", ): v1.merge(VariantsJson(_json_data)) diff --git a/variantlib/variants_json.py b/variantlib/variants_json.py index 4bf9c868..8898234e 100644 --- a/variantlib/variants_json.py +++ b/variantlib/variants_json.py @@ -115,6 +115,15 @@ def merge(self, variant_dist_info: Self) -> None: """Merge info from another wheel (VariantsJson instance)""" # Merge the variant properties + for label, properties in variant_dist_info.variants.items(): + if (old_properties := self.variants.get(label)) is None: + self.variants[label] = properties + elif old_properties != properties: + raise ValidationError( + f"Inconsistency in {VARIANTS_JSON_VARIANT_DATA_KEY}.{label}. " + f"Expected: { {x.to_str() for x in old_properties.properties}!r} , " + f"found: { {x.to_str() for x in properties.properties}!r}" + ) self.variants.update(variant_dist_info.variants) # Merge namespace priorities @@ -132,7 +141,7 @@ def merge(self, variant_dist_info: Self) -> None: f"{VARIANT_INFO_NAMESPACE_KEY} when merging variants. " f"Unable to merge: {namespace_priorities!r}" ) - variant_dist_info.namespace_priorities = namespace_priorities[1] + self.namespace_priorities = namespace_priorities[1] for namespace, provider_info in variant_dist_info.providers.items(): if (old_provider_info := self.providers.get(namespace)) is None: @@ -141,7 +150,7 @@ def merge(self, variant_dist_info: Self) -> None: # Otherwise, verify consistency elif provider_info != old_provider_info: raise ValidationError( - f"Inconsistency in providers.{namespace}. " + f"Inconsistency in {VARIANT_INFO_PROVIDER_DATA_KEY}.{namespace}. " f"Expected: {old_provider_info!r}, found: {provider_info!r}" )