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/plugins/test_loader.py b/tests/plugins/test_loader.py index 3e9df671..c9e19183 100644 --- a/tests/plugins/test_loader.py +++ b/tests/plugins/test_loader.py @@ -566,22 +566,18 @@ 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" ), "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 dab8da74..4d744e97 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -25,16 +25,15 @@ 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_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_INSTALL_TIME_KEY +from variantlib.constants import VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY 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 @@ -56,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: @@ -300,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"], @@ -311,12 +308,10 @@ 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"]}, ), }, - static_properties={"private": {"build_type": ["debug", "release"]}}, ) expected = { @@ -353,9 +348,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, @@ -387,50 +380,27 @@ 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"], + }, + 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"]}, }, ) - 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( - { - VARIANT_INFO_FEATURE_KEY: { - "ns1": ["f2"], - "ns2": ["f1", "f2"], - "ns3": ["f2", "f1"], - }, - VARIANT_INFO_PROPERTY_KEY: { - "ns1": { - "f2": ["p1"], - }, - "ns2": { - "f1": ["p2"], - }, - }, - } - ) assert ( json.loads( @@ -651,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( [ @@ -669,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 [], ) }, ) @@ -698,20 +665,14 @@ 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: - 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"], - }, + else: + 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( @@ -738,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, ) }, ) @@ -770,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, ) }, ) @@ -801,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 304f1baa..4a4a3191 100644 --- a/tests/test_pyproject_toml.py +++ b/tests/test_pyproject_toml.py @@ -6,16 +6,15 @@ 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_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_INSTALL_TIME_KEY +from variantlib.constants import VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY 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 @@ -37,11 +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_FEATURE_KEY}.ns3 = ["f2", "f1"] -{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"] @@ -49,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'", ] @@ -58,38 +51,19 @@ {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_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"] """ 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((VARIANT_INFO_FEATURE_KEY, VARIANT_INFO_PROPERTY_KEY)) - or x.startswith(f"{VARIANT_INFO_FEATURE_KEY}.ns3") - ) -) - 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"], - "ns3": ["f2", "f1"], - } - assert pyproj.property_priorities == { - "ns1": {"f2": ["p1"]}, - "ns2": {"f1": ["p2"]}, - } assert pyproj.providers == { "ns1": ProviderInfo( requires=["ns1-provider >= 1.2.3"], @@ -97,46 +71,18 @@ 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"], ), } - assert pyproj.static_properties == {"ns3": {"f1": ["v1", "v2"], "f2": ["v3", "v4"]}} - - -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.property_priorities == {} - 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, - ), - } - assert pyproj.static_properties == {"ns3": {"f1": ["v1", "v2"], "f2": ["v3", "v4"]}} def test_invalid_top_type() -> None: @@ -164,8 +110,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: @@ -191,16 +135,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: @@ -302,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( { @@ -317,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, } }, } @@ -325,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_STATIC_PROPERTIES_KEY: {"ns": {"test": ["val"]}}, - } - } - ) - - def test_missing_namespace_priority() -> None: with pytest.raises( ValidationError, @@ -422,22 +338,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"], - "ns3": ["f2", "f1"], - } - assert converted.property_priorities == { - "ns1": {"f2": ["p1"]}, - "ns2": {"f1": ["p2"]}, - } assert converted.providers == { "ns1": ProviderInfo( requires=["ns1-provider >= 1.2.3"], @@ -445,19 +350,18 @@ 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"], ), } - assert pyproj.static_properties == {"ns3": {"f1": ["v1", "v2"], "f2": ["v3", "v4"]}} # Non-common fields should be reset to defaults if isinstance(converted, VariantsJson): @@ -505,87 +409,149 @@ def test_no_plugin_api() -> None: assert pyproject_toml.providers["ns"].object_reference == "my_plugin" -def test_missing_static_properties() -> None: +def test_static_properties_one_feature() -> 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_STATIC_PROPERTIES_KEY: {"f": ["v"]}, + } + }, + } + } + ) + + +def test_static_properties_missing_priorities() -> 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}: multiple features require " + rf"specifying ordering via {VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY}; missing: " + r"{'f2'}", ): VariantPyProjectToml( { PYPROJECT_TOML_TOP_KEY: { - VARIANT_INFO_DEFAULT_PRIO_KEY: {VARIANT_INFO_NAMESPACE_KEY: ["ns"]}, + VARIANT_INFO_DEFAULT_PRIO_KEY: { + VARIANT_INFO_NAMESPACE_KEY: ["ns"], + }, VARIANT_INFO_PROVIDER_DATA_KEY: { - "ns": {VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: False} + "ns": { + VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: { + "f1": ["v"], + "f2": ["v"], + }, + VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY: ["f1"], + } }, } } ) -@pytest.mark.parametrize("install_time", [False, True]) -def test_extraneous_static_properties(install_time: bool) -> 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_STATIC_PROPERTIES_KEY} " - r"must specify properties for all AoT providers; currently provided: {'ns'}; " - r"expected: set\(\)", + 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_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"], + requires_key: ["example"], + VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: {"f": ["v"]}, } }, - VARIANT_INFO_STATIC_PROPERTIES_KEY: {"ns": {"f": ["v"]}}, } } ) -def test_static_properties_one_feature() -> 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_INSTALL_TIME_KEY: False, - } - }, - VARIANT_INFO_STATIC_PROPERTIES_KEY: {"ns": {"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_missing_priorities() -> None: +def test_static_properties_and_plugin_api() -> None: with pytest.raises( ValidationError, - match=rf"{PYPROJECT_TOML_TOP_KEY}\.{VARIANT_INFO_STATIC_PROPERTIES_KEY}\.ns: " - 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'}", + 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_FEATURE_KEY: {"ns": ["f1"]}, }, VARIANT_INFO_PROVIDER_DATA_KEY: { "ns": { - VARIANT_INFO_PROVIDER_INSTALL_TIME_KEY: False, + VARIANT_INFO_PROVIDER_PLUGIN_API_KEY: "example", + VARIANT_INFO_PROVIDER_STATIC_PROPERTIES_KEY: {"f": ["v"]}, } }, - VARIANT_INFO_STATIC_PROPERTIES_KEY: { - "ns": {"f1": ["v"], "f2": ["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_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..e9a6faec 100644 --- a/tests/test_variants_json.py +++ b/tests/test_variants_json.py @@ -8,9 +8,8 @@ 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_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 @@ -219,13 +218,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 +252,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 +278,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 +309,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 +340,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] = { @@ -387,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"], }, @@ -398,16 +369,17 @@ 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": { + "bar": { "a": { "a": ["c"], }, - "b": { - "b": ["b"], - }, } }, } @@ -416,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"], }, @@ -466,26 +435,23 @@ 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_a) + _json_data[VARIANT_INFO_DEFAULT_PRIO_KEY][VARIANT_INFO_NAMESPACE_KEY] = ["b", "a"] + with pytest.raises( + 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)) @@ -495,7 +461,19 @@ def test_merge_variants() -> None: ] = "test:Test" with pytest.raises( ValidationError, - match="Inconsistency in providers when merging variants", + 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)) @@ -513,3 +491,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 2fdf628d..6e738553 100644 --- a/variantlib/api.py +++ b/variantlib/api.py @@ -91,11 +91,13 @@ 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, - variants_json.property_priorities, ), ) ] @@ -210,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 = ( @@ -234,24 +235,24 @@ def make_variant_dist_info( for config in configs: if config.namespace not in build_namespaces: continue - variant_json.static_properties[config.namespace] = {} + 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: 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 - ) - - # 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) + variant_json.providers[config.namespace].static_properties[ + vfeat.name + ] = vfeat.values + + # 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: @@ -262,9 +263,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/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) diff --git a/variantlib/constants.py b/variantlib/constants.py index 06b2998b..81d4a904 100644 --- a/variantlib/constants.py +++ b/variantlib/constants.py @@ -10,16 +10,17 @@ # 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_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" -VARIANT_INFO_STATIC_PROPERTIES_KEY: Literal["static-properties"] = "static-properties" +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" @@ -106,18 +107,18 @@ class PriorityJsonDict(TypedDict, total=False): namespace: list[str] - feature: dict[str, list[str]] - property: dict[str, dict[str, list[str]]] ProviderPluginJsonDict = TypedDict( "ProviderPluginJsonDict", { + "build-requires": list[str], "enable-if": str, - "install-time": bool, "optional": bool, "plugin-api": str, "requires": list[str], + "static-properties": dict[str, list[str]], + "feature-order": list[str], }, total=False, ) @@ -131,7 +132,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..8d0a8afb 100644 --- a/variantlib/models/variant_info.py +++ b/variantlib/models/variant_info.py @@ -14,16 +14,15 @@ 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_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_INSTALL_TIME_KEY +from variantlib.constants import VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY 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 @@ -37,73 +36,65 @@ 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: - if self.install_time and not self.requires: + if ( + bool(self.build_requires), + bool(self.requires), + bool(self.static_properties), + ).count(True) != 1: raise ValidationError( - "requires need to be specified for install-time providers" + "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 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) - 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""" 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, - install_time=provider_data.install_time, 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() + }, + feature_order=list(provider_data.feature_order), + build_requires=list(provider_data.build_requires), ) 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( @@ -124,59 +115,26 @@ 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 - def _get_expected_aot_namespaces(self) -> set[VariantNamespace]: + @property + def _build_requires_allowed(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], {} @@ -205,60 +163,85 @@ def _process_common(self, validator: KeyTrackingValidator) -> None: 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 + 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, + 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 provider_install_time and not provider_requires: + if len(feature_dict) > 1: + feature_prios = set(provider_feature_order) + missing_feature_prios = ( + set(feature_dict.keys()) - feature_prios + ) + if missing_feature_prios: + raise ValidationError( + f"{validator.key}: multiple features require " + "specifying ordering via " + f"{VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY}; " + f"missing: {missing_feature_prios}" + ) + + 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_REQUIRES_KEY} must be " - "specified for install-time plugins" + 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, ) - 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 +258,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/_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 b87987e1..3ea32e56 100644 --- a/variantlib/plugins/loader.py +++ b/variantlib/plugins/loader.py @@ -313,16 +313,19 @@ 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: """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 ce78d549..11c75f3a 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 _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 52aabec9..8898234e 100644 --- a/variantlib/variants_json.py +++ b/variantlib/variants_json.py @@ -10,16 +10,15 @@ 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_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_INSTALL_TIME_KEY +from variantlib.constants import VARIANT_INFO_PROVIDER_FEATURE_ORDER_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY 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 +33,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 +58,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: @@ -70,17 +67,28 @@ 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, + provider_info.static_properties, + ) + if provider_info.feature_order: + yield ( + 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) - 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) -> 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,68 +108,55 @@ 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) - @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", - "feature_priorities", - "property_priorities", - ): - new_value = getattr(variant_dist_info, attribute) - old_value = getattr(self, attribute) - if old_value != new_value: + 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 {attribute!r} when merging variants. " - f"Expected: {old_value!r}, found {new_value!r}" + 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) - 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}" ) + 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: # If provider not yet specified, just copy it self.providers[namespace] = provider_info + # Otherwise, verify consistency + elif provider_info != old_provider_info: + raise ValidationError( + f"Inconsistency in {VARIANT_INFO_PROVIDER_DATA_KEY}.{namespace}. " + f"Expected: {old_provider_info!r}, found: {provider_info!r}" + ) - 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}" - ) - - 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 _build_requires_allowed(self) -> bool: + return False def _process(self, variant_table: VariantsJsonDict) -> None: validator = KeyTrackingValidator(None, variant_table) # type: ignore[arg-type]