From 502fb389570b82ce5caa0a2ea73af8e8e5957147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Fri, 10 Apr 2026 17:32:29 +0200 Subject: [PATCH 01/19] Remove unused `serialize` / `deserialize` model methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- tests/models/test_variant.py | 54 ------------------------------------ variantlib/models/variant.py | 19 ------------- 2 files changed, 73 deletions(-) diff --git a/tests/models/test_variant.py b/tests/models/test_variant.py index f0812e58..912ef4cd 100644 --- a/tests/models/test_variant.py +++ b/tests/models/test_variant.py @@ -177,29 +177,6 @@ def test_from_str_invalid_format() -> None: VariantProperty.from_str(input_str) -def test_variantprop_serialization() -> None: - vprop = VariantProperty(namespace="provider", feature="feature", value="value") - assert vprop.serialize() == { - "namespace": "provider", - "feature": "feature", - "value": "value", - } - - -def test_variantprop_deserialization() -> None: - data = { - "namespace": "provider", - "feature": "feature", - "value": "value", - } - - vprop = VariantProperty.deserialize(data) - - assert vprop.namespace == data["namespace"] - assert vprop.feature == data["feature"] - assert vprop.value == data["value"] - - def test_variantprop_sorting() -> None: data = [ VariantProperty("z", "a", "a"), @@ -348,37 +325,6 @@ def test_variantdescription_hexdigest_adjacent_strings() -> None: ) -def test_variantdescription_serialization() -> None: - vprop = VariantProperty(namespace="provider", feature="feature", value="value") - vdesc = VariantDescription(properties=[vprop]) - - assert vdesc.serialize() == [ - { - "namespace": "provider", - "feature": "feature", - "value": "value", - } - ] - - -def test_variantdescription_deserialization() -> None: - data = [ - { - "namespace": "provider", - "feature": "feature", - "value": "value", - } - ] - - vdesc = VariantDescription.deserialize(data) - - assert len(vdesc.properties) == 1 - assert vdesc.properties[0].namespace == "provider" - assert vdesc.properties[0].feature == "feature" - assert vdesc.properties[0].value == "value" - assert vdesc.hexdigest == "c44d3adf" - - # ----------------------------------------------- # Fuzzy Testing # ----------------------------------------------- diff --git a/variantlib/models/variant.py b/variantlib/models/variant.py index ed79e9e6..9fc6d360 100644 --- a/variantlib/models/variant.py +++ b/variantlib/models/variant.py @@ -69,16 +69,6 @@ def to_str(self) -> str: # Variant-Property: :: :: return f"{self.namespace} :: {self.feature}" - def serialize(self) -> dict[str, str]: - return asdict(self) - - @classmethod - def deserialize(cls, data: dict[str, str]) -> Self: - for field_name in cls.__dataclass_fields__: - if field_name not in data: - raise ValidationError(f"Extra field not known: `{field_name}`") - return cls(**data) - @classmethod def from_str(cls, input_str: str) -> Self: # Try matching the input string with the regex pattern @@ -213,15 +203,6 @@ def hexdigest(self) -> str: # Source: https://docs.python.org/3/library/hashlib.html#hashlib.hash.hexdigest return hash_object.hexdigest()[:VARIANT_HASH_LENGTH] - @classmethod - def deserialize(cls, properties: list[dict[str, str]]) -> Self: - return cls( - properties=[VariantProperty.deserialize(vdata) for vdata in properties] - ) - - def serialize(self) -> list[dict[str, str]]: - return [vprop.serialize() for vprop in self.properties] - def to_dict(self) -> VariantInfoJsonDict: data = asdict(self) From 2abd8ff146061f66251e9c2c004cf33f6cd2f5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Fri, 10 Apr 2026 17:41:36 +0200 Subject: [PATCH 02/19] Add a (not used yet) label field to `VariantDescription` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- variantlib/models/variant.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/variantlib/models/variant.py b/variantlib/models/variant.py index 9fc6d360..53586af8 100644 --- a/variantlib/models/variant.py +++ b/variantlib/models/variant.py @@ -9,11 +9,13 @@ from dataclasses import field from functools import cached_property +from variantlib.constants import NULL_VARIANT_LABEL from variantlib.constants import VALIDATION_FEATURE_NAME_REGEX from variantlib.constants import VALIDATION_FEATURE_REGEX from variantlib.constants import VALIDATION_NAMESPACE_REGEX from variantlib.constants import VALIDATION_PROPERTY_REGEX from variantlib.constants import VALIDATION_VALUE_REGEX +from variantlib.constants import VALIDATION_VARIANT_LABEL_REGEX from variantlib.constants import VariantInfoJsonDict from variantlib.errors import ValidationError from variantlib.models.base import BaseModel @@ -160,6 +162,19 @@ class VariantDescription(BaseModel): default_factory=list, ) + label: str = field( + metadata={ + "validator": lambda val: validate_and( + [ + lambda v: validate_type(v, str), + lambda v: validate_matches_re(v, VALIDATION_VARIANT_LABEL_REGEX), # pyright: ignore[reportArgumentType] + ], + value=val, + ), + }, + default=NULL_VARIANT_LABEL, + ) + def __post_init__(self) -> None: # We sort the data so that they always get displayed/hashed # in a consistent manner. @@ -172,6 +187,19 @@ def __post_init__(self) -> None: # Execute the validator super().__post_init__() + # TODO: enable once we're done porting + if False: + if self.is_null_variant(): + if self.label != NULL_VARIANT_LABEL: + raise ValidationError( + f"Null variant must always use {NULL_VARIANT_LABEL!r} label" + ) + elif self.label == NULL_VARIANT_LABEL: + raise ValidationError( + f"{NULL_VARIANT_LABEL!r} label can be used only for the null " + "variant" + ) + def is_null_variant(self) -> bool: """ Check if the variant is a null variant. From 5f133d9fdf25b3f5a36bb188097d64c8085f5ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Fri, 10 Apr 2026 17:56:11 +0200 Subject: [PATCH 03/19] Populate label in `from_dict()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- tests/test_variant_dist_info.py | 14 +++++++------- tests/test_variants_json.py | 15 ++++++++++++--- variantlib/models/variant.py | 4 ++-- variantlib/variants_json.py | 4 +++- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/tests/test_variant_dist_info.py b/tests/test_variant_dist_info.py index 1a9c2814..cd5c5845 100644 --- a/tests/test_variant_dist_info.py +++ b/tests/test_variant_dist_info.py @@ -26,12 +26,12 @@ VARIANT_INFO_PROVIDER_DATA_KEY: { "ns": {VARIANT_INFO_PROVIDER_REQUIRES_KEY: ["ns-pkg"]} }, - VARIANTS_JSON_VARIANT_DATA_KEY: {"bdbc6ca0": {"ns": {"f": ["v"]}}}, + VARIANTS_JSON_VARIANT_DATA_KEY: {"test": {"ns": {"f": ["v"]}}}, } @pytest.mark.parametrize("json_type", [str, bytes]) -@pytest.mark.parametrize("expected_label", [None, "bdbc6ca0"]) +@pytest.mark.parametrize("expected_label", [None, "test"]) def test_variant_dist_info(json_type: type, expected_label: str | None) -> None: vjson_str = ( json.dumps(VARIANT_JSON) @@ -43,21 +43,21 @@ def test_variant_dist_info(json_type: type, expected_label: str | None) -> None: 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")]) - assert variant_dist_info.variants == {vdesc.hexdigest: vdesc} + vdesc = VariantDescription([VariantProperty("ns", "f", "v")], label="test") + assert variant_dist_info.variants == {"test": vdesc} assert variant_dist_info.variant_desc == vdesc - assert variant_dist_info.variant_label == vdesc.hexdigest + assert variant_dist_info.variant_label == "test" @pytest.mark.parametrize("expected_label", [None, "fancy1"]) def test_variant_dist_info_custom_label(expected_label: str | None) -> None: - vjson_str = json.dumps(VARIANT_JSON).replace("bdbc6ca0", "fancy1") + 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")]) + vdesc = VariantDescription([VariantProperty("ns", "f", "v")], label="fancy1") assert variant_dist_info.variants == {"fancy1": vdesc} assert variant_dist_info.variant_desc == vdesc assert variant_dist_info.variant_label == "fancy1" diff --git a/tests/test_variants_json.py b/tests/test_variants_json.py index 4805c9fa..a8bc9822 100644 --- a/tests/test_variants_json.py +++ b/tests/test_variants_json.py @@ -46,6 +46,7 @@ def test_validate_variants_json() -> None: assert variants_json.variants == { NULL_VARIANT_LABEL: VariantDescription(), "03e04d5e": VariantDescription( + label="03e04d5e", properties=[ VariantProperty( namespace="fictional_hw", @@ -60,6 +61,7 @@ def test_validate_variants_json() -> None: ], ), "36028aca": VariantDescription( + label="36028aca", properties=[ VariantProperty( namespace="fictional_hw", @@ -89,6 +91,7 @@ def test_validate_variants_json() -> None: ], ), "3f7188c1": VariantDescription( + label="3f7188c1", properties=[ VariantProperty( namespace="fictional_hw", @@ -113,6 +116,7 @@ def test_validate_variants_json() -> None: ], ), "7db6d39f": VariantDescription( + label="7db6d39f", properties=[ VariantProperty( namespace="fictional_tech", @@ -132,6 +136,7 @@ def test_validate_variants_json() -> None: ], ), "808c7f9d": VariantDescription( + label="808c7f9d", properties=[ VariantProperty( namespace="fictional_tech", @@ -151,6 +156,7 @@ def test_validate_variants_json() -> None: ], ), "80fa16ff": VariantDescription( + label="80fa16ff", properties=[ VariantProperty( namespace="fictional_hw", @@ -175,6 +181,7 @@ def test_validate_variants_json() -> None: ], ), "3351fc6a": VariantDescription( + label="3351fc6a", properties=[ VariantProperty( namespace="fictional_hw", @@ -182,9 +189,10 @@ def test_validate_variants_json() -> None: value=str(value), ) for value in range(4, 6) - ] + ], ), "181830db": VariantDescription( + label="181830db", properties=[ VariantProperty( namespace="fictional_hw", @@ -192,9 +200,10 @@ def test_validate_variants_json() -> None: value=str(value), ) for value in range(4, 8) - ] + ], ), "72c47fce": VariantDescription( + label="72c47fce", properties=[ VariantProperty( namespace="fictional_hw", @@ -206,7 +215,7 @@ def test_validate_variants_json() -> None: feature="compute_capability", value="8", ), - ] + ], ), } assert variants_json.namespace_priorities == ["fictional_hw", "fictional_tech"] diff --git a/variantlib/models/variant.py b/variantlib/models/variant.py index 53586af8..ea443c13 100644 --- a/variantlib/models/variant.py +++ b/variantlib/models/variant.py @@ -247,7 +247,7 @@ def to_dict(self) -> VariantInfoJsonDict: return dict(result) @classmethod - def from_dict(cls, data: VariantInfoJsonDict) -> Self: + def from_dict(cls, data: VariantInfoJsonDict, label: str) -> Self: vprops = [ VariantProperty(namespace=namespace, feature=key, value=vprop_val) for namespace, vdata in data.items() @@ -255,7 +255,7 @@ def from_dict(cls, data: VariantInfoJsonDict) -> Self: for vprop_val in vprop_values ] - return cls(vprops) + return cls(properties=vprops, label=label) @dataclass(frozen=True) diff --git a/variantlib/variants_json.py b/variantlib/variants_json.py index 363eeb5f..325956a1 100644 --- a/variantlib/variants_json.py +++ b/variantlib/variants_json.py @@ -179,7 +179,9 @@ def _process(self, variant_table: VariantsJsonDict) -> None: VariantInfoJsonDict, ignore_subkeys=True, ) as packed_vdesc: - vdesc = VariantDescription.from_dict(packed_vdesc) + vdesc = VariantDescription.from_dict( + packed_vdesc, label=variant_label + ) if vdesc.is_null_variant() and variant_label != NULL_VARIANT_LABEL: raise ValidationError( f"Null variant must use {NULL_VARIANT_LABEL!r} label" From f0e76317ab99ca8879c3853399bdc3a2c3870089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Fri, 10 Apr 2026 18:14:47 +0200 Subject: [PATCH 04/19] Add fallback sort on variant label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note that we do not guarantee passing variant label to the function yet. Signed-off-by: Michał Górny --- tests/resolver/test_sorting.py | 64 ++++++++++++++++++++++++++++++++++ variantlib/resolver/sorting.py | 5 +-- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/tests/resolver/test_sorting.py b/tests/resolver/test_sorting.py index 3966aebb..c3bcd40b 100644 --- a/tests/resolver/test_sorting.py +++ b/tests/resolver/test_sorting.py @@ -366,3 +366,67 @@ def test_sort_variants_descriptions_validation_error( vdescs=vdescs, property_priorities=property_priorities, ) + + +def test_sort_variants_descriptions_by_label() -> None: + vprops_proprioty_list = [ + VariantProperty(namespace="omnicorp", feature="feat_a", value="value"), + VariantProperty(namespace="omnicorp", feature="feat_b", value="value1"), + ] + + vdesc1a = VariantDescription( + [ + VariantProperty(namespace="omnicorp", feature="feat_a", value="value"), + ], + label="a", + ) + vdesc1b = VariantDescription( + [ + VariantProperty(namespace="omnicorp", feature="feat_a", value="value"), + ], + label="b", + ) + vdesc1c = VariantDescription( + [ + VariantProperty(namespace="omnicorp", feature="feat_a", value="value"), + ], + label="c", + ) + + vdesc2a = VariantDescription( + [ + VariantProperty(namespace="omnicorp", feature="feat_b", value="value1"), + ], + label="a", + ) + vdesc2b = VariantDescription( + [ + VariantProperty(namespace="omnicorp", feature="feat_b", value="value1"), + ], + label="b", + ) + vdesc2c = VariantDescription( + [ + VariantProperty(namespace="omnicorp", feature="feat_b", value="value1"), + ], + label="c", + ) + + assert sort_variants_descriptions( + vdescs=[vdesc1a, vdesc1b, vdesc1c], property_priorities=vprops_proprioty_list + ) == [vdesc1a, vdesc1b, vdesc1c] + assert sort_variants_descriptions( + vdescs=[vdesc1b, vdesc1c, vdesc1a], property_priorities=vprops_proprioty_list + ) == [vdesc1a, vdesc1b, vdesc1c] + assert sort_variants_descriptions( + vdescs=[vdesc1c, vdesc1b, vdesc1a], property_priorities=vprops_proprioty_list + ) == [vdesc1a, vdesc1b, vdesc1c] + + assert sort_variants_descriptions( + vdescs=[vdesc1a, vdesc1b, vdesc1c, vdesc2a, vdesc2b], + property_priorities=vprops_proprioty_list, + ) == [vdesc1a, vdesc1b, vdesc1c, vdesc2a, vdesc2b] + assert sort_variants_descriptions( + vdescs=[vdesc2a, vdesc2c, vdesc1a, vdesc1b], + property_priorities=vprops_proprioty_list, + ) == [vdesc1a, vdesc1b, vdesc2a, vdesc2c] diff --git a/variantlib/resolver/sorting.py b/variantlib/resolver/sorting.py index 1de41d8c..a4ddf5a7 100644 --- a/variantlib/resolver/sorting.py +++ b/variantlib/resolver/sorting.py @@ -195,7 +195,7 @@ def sort_variants_descriptions( property_lookup_table = dict(property_lookup_table) lookup_table_size = len(property_lookup_table) - def _get_rank_tuple(vdesc: VariantDescription) -> tuple[int, ...]: + def _get_rank_tuple(vdesc: VariantDescription) -> tuple[tuple[int, ...], str]: """ Get the rank tuple of a `VariantDescription` object. @@ -232,6 +232,7 @@ def _get_rank_tuple(vdesc: VariantDescription) -> tuple[int, ...]: if any(ranking_array[idx] == sys.maxsize for idx in vdesc_feature_indexes): raise ValidationError("Filtering should be applied first.") - return tuple(ranking_array) + # As a fallback, sort on variant label. + return (tuple(ranking_array), vdesc.label) return sorted(vdescs, key=_get_rank_tuple) From bdb34f77e5ef1bc5caa9b304ed918d3e86d4e7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Fri, 10 Apr 2026 19:01:58 +0200 Subject: [PATCH 05/19] Update variant model tests for labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- tests/models/test_variant.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/tests/models/test_variant.py b/tests/models/test_variant.py index 912ef4cd..3485418d 100644 --- a/tests/models/test_variant.py +++ b/tests/models/test_variant.py @@ -6,9 +6,11 @@ import pytest from hypothesis import given from hypothesis import strategies as st +from variantlib.constants import NULL_VARIANT_LABEL from variantlib.constants import VALIDATION_FEATURE_NAME_REGEX from variantlib.constants import VALIDATION_NAMESPACE_REGEX from variantlib.constants import VALIDATION_VALUE_REGEX +from variantlib.constants import VALIDATION_VARIANT_LABEL_REGEX from variantlib.errors import ValidationError from variantlib.models.variant import VARIANT_HASH_LENGTH from variantlib.models.variant import VariantDescription @@ -214,6 +216,7 @@ def test_null_variant() -> None: vdesc = VariantDescription() assert vdesc.properties == [] assert vdesc.hexdigest == hashlib.sha256(b"").hexdigest()[:VARIANT_HASH_LENGTH] + assert vdesc.label == NULL_VARIANT_LABEL def test_variantdescription_initialization() -> None: @@ -224,7 +227,8 @@ def test_variantdescription_initialization() -> None: vprop2 = VariantProperty( namespace="tyrell_corporation", feature="client_id", value="secret_pass" ) - vdesc = VariantDescription([vprop1, vprop2]) + vdesc = VariantDescription([vprop1, vprop2], label="test") + assert vdesc.label == "test" # Check that the _data property is a list assert isinstance(vdesc.properties, list) @@ -257,14 +261,13 @@ def test_variantdescription_duplicate_data() -> None: def test_variantdescription_partial_duplicate_data() -> None: - # Test that duplicate VariantProperty instances are removed vprop1 = VariantProperty( namespace="omnicorp", feature="custom_feat", value="secret_value" ) vprop2 = VariantProperty( namespace="omnicorp", feature="custom_feat", value="another_value" ) - VariantDescription([vprop1, vprop2]) + VariantDescription([vprop1, vprop2], label="test") def test_variantdescription_sorted_data() -> None: @@ -278,7 +281,7 @@ def test_variantdescription_sorted_data() -> None: vprop3 = VariantProperty( namespace="omnicorp", feature="secret_pass", value="client_value" ) - vdesc = VariantDescription([vprop1, vprop2, vprop3]) + vdesc = VariantDescription([vprop1, vprop2, vprop3], label="test") # Check that data is sorted by namespace, feature, and value sorted_vprops = sorted( @@ -296,7 +299,7 @@ def test_variantdescription_hexdigest() -> None: namespace="tyrell_corporation", feature="client_id", value="secret_pass" ) vprops = [vprop1, vprop2] - vdesc = VariantDescription(vprops) + vdesc = VariantDescription(vprops, label="test") # Compute the expected hash using shake_128 (mock the hash output for testing) hash_object = hashlib.sha256( @@ -314,13 +317,15 @@ def test_variantdescription_hexdigest_adjacent_strings() -> None: [ VariantProperty("a", "b", "cx"), VariantProperty("d", "e", "f"), - ] + ], + label="test", ).hexdigest != VariantDescription( [ VariantProperty("a", "b", "c"), VariantProperty("xd", "e", "f"), - ] + ], + label="another", ).hexdigest ) @@ -390,7 +395,7 @@ def test_fuzzy_variantprop(namespace: str, feature: str, value: str) -> None: ) def test_fuzzy_variantdescription(vprop: list[VariantProperty]) -> None: # Fuzzy test for random combinations of VariantDescription - vdesc = VariantDescription(vprop) + vdesc = VariantDescription(vprop, label="test") assert isinstance(vdesc.properties, list) assert len(vdesc.properties) >= 1 @@ -414,8 +419,23 @@ def test_fuzzy_variantdescription(vprop: list[VariantProperty]) -> None: value=st.from_regex(VALIDATION_NAMESPACE_REGEX, fullmatch=True), ), ), + label=st.from_regex(VALIDATION_VARIANT_LABEL_REGEX, fullmatch=True), ) ) def test_random_hexdigest(vdesc: VariantDescription) -> None: assert isinstance(vdesc.hexdigest, str) assert len(vdesc.hexdigest) == VARIANT_HASH_LENGTH + + +@pytest.mark.xfail(reason="Validation is disabled for porting") +def test_null_variant_label(): + with pytest.raises( + ValidationError, + match=rf"{NULL_VARIANT_LABEL!r} label can be used only for the null variant", + ): + VariantDescription([VariantProperty("a", "b", "c")], label=NULL_VARIANT_LABEL) + with pytest.raises( + ValidationError, + match=rf"Null variant must always use {NULL_VARIANT_LABEL!r} label", + ): + VariantDescription(label="zuul") From 54593fef6ef05007184ec05c3830ed0b75ebd2c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Fri, 10 Apr 2026 19:31:20 +0200 Subject: [PATCH 06/19] Update resolver tests to define labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- tests/resolver/test_filtering.py | 22 ++-- tests/resolver/test_lib.py | 194 +++++++++++++++---------------- tests/resolver/test_sorting.py | 22 ++-- 3 files changed, 123 insertions(+), 115 deletions(-) diff --git a/tests/resolver/test_filtering.py b/tests/resolver/test_filtering.py index b6efe8c7..32838184 100644 --- a/tests/resolver/test_filtering.py +++ b/tests/resolver/test_filtering.py @@ -32,9 +32,9 @@ def vdescs(vprops: list[VariantProperty]) -> list[VariantDescription]: vprop1, vprop2 = vprops return [ - VariantDescription([vprop1]), - VariantDescription([vprop2]), - VariantDescription([vprop1, vprop2]), + VariantDescription([vprop1], label="a"), + VariantDescription([vprop2], label="b"), + VariantDescription([vprop1, vprop2], label="c"), ] @@ -139,11 +139,11 @@ def test_filter_variants_by_namespaces(vdescs: list[VariantDescription]) -> None ("vdescs", "forbidden_namespaces"), [ ( - [VariantDescription([VariantProperty("a", "b", "c")])], + [VariantDescription([VariantProperty("a", "b", "c")], label="test")], "not a list", ), ( - [VariantDescription([VariantProperty("a", "b", "c")])], + [VariantDescription([VariantProperty("a", "b", "c")], label="test")], [VariantProperty("not", "a", "str")], ), ("not a list", ["omnicorp"]), @@ -245,11 +245,11 @@ def test_filter_variants_by_features( ("vdescs", "forbidden_features"), [ ( - [VariantDescription([VariantProperty("a", "b", "c")])], + [VariantDescription([VariantProperty("a", "b", "c")], label="test")], "not a list", ), ( - [VariantDescription([VariantProperty("a", "b", "c")])], + [VariantDescription([VariantProperty("a", "b", "c")], label="test")], ["not a `VariantFeature`"], ), ("not a list", VariantFeature("a", "b")), @@ -453,22 +453,22 @@ def test_filter_variants_by_property( [VariantProperty("a", "b", "c")], ), ( - [VariantDescription([VariantProperty("a", "b", "c")])], + [VariantDescription([VariantProperty("a", "b", "c")], label="test")], "not a list", [VariantProperty("a", "b", "c")], ), ( - [VariantDescription([VariantProperty("a", "b", "c")])], + [VariantDescription([VariantProperty("a", "b", "c")], label="test")], ["not a `VariantFeature`"], [VariantProperty("a", "b", "c")], ), ( - [VariantDescription([VariantProperty("a", "b", "c")])], + [VariantDescription([VariantProperty("a", "b", "c")], label="test")], [VariantProperty("a", "b", "c")], "not a list", ), ( - [VariantDescription([VariantProperty("a", "b", "c")])], + [VariantDescription([VariantProperty("a", "b", "c")], label="test")], [VariantProperty("a", "b", "c")], ["not a `VariantFeature`"], ), diff --git a/tests/resolver/test_lib.py b/tests/resolver/test_lib.py index 2d32449b..3a5656d6 100644 --- a/tests/resolver/test_lib.py +++ b/tests/resolver/test_lib.py @@ -100,82 +100,82 @@ def vdescs(vprops: list[VariantProperty]) -> list[VariantDescription]: # Important: vprop4 and vprop5 are mutually exclusive return [ # variants with 5 properties - VariantDescription([vprop1, vprop2, vprop3, vprop4, vprop6]), - VariantDescription([vprop1, vprop2, vprop3, vprop5, vprop6]), + VariantDescription([vprop1, vprop2, vprop3, vprop4, vprop6], label="a"), + VariantDescription([vprop1, vprop2, vprop3, vprop5, vprop6], label="b"), # variants with 4 properties - VariantDescription([vprop1, vprop2, vprop3, vprop4]), # - vprop6 - VariantDescription([vprop1, vprop2, vprop3, vprop5]), # - vprop6 + VariantDescription([vprop1, vprop2, vprop3, vprop4], label="c"), # - vprop6 + VariantDescription([vprop1, vprop2, vprop3, vprop5], label="d"), # - vprop6 - VariantDescription([vprop1, vprop2, vprop3, vprop6]), # - vprop4/5 + VariantDescription([vprop1, vprop2, vprop3, vprop6], label="c"), # - vprop4/5 - VariantDescription([vprop1, vprop2, vprop4, vprop6]), # - vprop3 - VariantDescription([vprop1, vprop2, vprop5, vprop6]), # - vprop3 + VariantDescription([vprop1, vprop2, vprop4, vprop6], label="d"), # - vprop3 + VariantDescription([vprop1, vprop2, vprop5, vprop6], label="e"), # - vprop3 - VariantDescription([vprop1, vprop3, vprop4, vprop6]), # - vprop2 - VariantDescription([vprop1, vprop3, vprop5, vprop6]), # - vprop2 + VariantDescription([vprop1, vprop3, vprop4, vprop6], label="f"), # - vprop2 + VariantDescription([vprop1, vprop3, vprop5, vprop6], label="g"), # - vprop2 - VariantDescription([vprop2, vprop3, vprop5, vprop6]), # - vprop1 - VariantDescription([vprop2, vprop3, vprop5, vprop6]), # - vprop1 + VariantDescription([vprop2, vprop3, vprop5, vprop6], label="h"), # - vprop1 + VariantDescription([vprop2, vprop3, vprop5, vprop6], label="i"), # - vprop1 # variants with 3 properties # --- vprop1 --- # - VariantDescription([vprop1, vprop2, vprop3]), - VariantDescription([vprop1, vprop2, vprop4]), - VariantDescription([vprop1, vprop2, vprop5]), - VariantDescription([vprop1, vprop2, vprop6]), + VariantDescription([vprop1, vprop2, vprop3], label="j"), + VariantDescription([vprop1, vprop2, vprop4], label="k"), + VariantDescription([vprop1, vprop2, vprop5], label="l"), + VariantDescription([vprop1, vprop2, vprop6], label="m"), - VariantDescription([vprop1, vprop3, vprop4]), - VariantDescription([vprop1, vprop3, vprop5]), - VariantDescription([vprop1, vprop3, vprop6]), + VariantDescription([vprop1, vprop3, vprop4], label="n"), + VariantDescription([vprop1, vprop3, vprop5], label="o"), + VariantDescription([vprop1, vprop3, vprop6], label="p"), - VariantDescription([vprop1, vprop4, vprop6]), - VariantDescription([vprop1, vprop5, vprop6]), + VariantDescription([vprop1, vprop4, vprop6], label="q"), + VariantDescription([vprop1, vprop5, vprop6], label="r"), # --- vprop2 --- # - VariantDescription([vprop2, vprop3, vprop4]), - VariantDescription([vprop2, vprop3, vprop5]), - VariantDescription([vprop2, vprop3, vprop6]), + VariantDescription([vprop2, vprop3, vprop4], label="s"), + VariantDescription([vprop2, vprop3, vprop5], label="t"), + VariantDescription([vprop2, vprop3, vprop6], label="u"), - VariantDescription([vprop2, vprop4, vprop6]), - VariantDescription([vprop2, vprop5, vprop6]), + VariantDescription([vprop2, vprop4, vprop6], label="v"), + VariantDescription([vprop2, vprop5, vprop6], label="w"), # --- vprop3 --- # - VariantDescription([vprop3, vprop4, vprop6]), - VariantDescription([vprop3, vprop5, vprop6]), + VariantDescription([vprop3, vprop4, vprop6], label="x"), + VariantDescription([vprop3, vprop5, vprop6], label="y"), # variants with 2 properties # --- vprop1 --- # - VariantDescription([vprop1, vprop2]), - VariantDescription([vprop1, vprop3]), - VariantDescription([vprop1, vprop4]), - VariantDescription([vprop1, vprop5]), - VariantDescription([vprop1, vprop6]), + VariantDescription([vprop1, vprop2], label="z"), + VariantDescription([vprop1, vprop3], label="aa"), + VariantDescription([vprop1, vprop4], label="ab"), + VariantDescription([vprop1, vprop5], label="ac"), + VariantDescription([vprop1, vprop6], label="ad"), # --- vprop2 --- # - VariantDescription([vprop2, vprop3]), - VariantDescription([vprop2, vprop4]), - VariantDescription([vprop2, vprop5]), - VariantDescription([vprop2, vprop6]), + VariantDescription([vprop2, vprop3], label="ae"), + VariantDescription([vprop2, vprop4], label="af"), + VariantDescription([vprop2, vprop5], label="ag"), + VariantDescription([vprop2, vprop6], label="ah"), # --- vprop3 --- # - VariantDescription([vprop3, vprop4]), - VariantDescription([vprop3, vprop5]), - VariantDescription([vprop3, vprop6]), + VariantDescription([vprop3, vprop4], label="ai"), + VariantDescription([vprop3, vprop5], label="aj"), + VariantDescription([vprop3, vprop6], label="ak"), # --- vprop4 --- # - VariantDescription([vprop4, vprop6]), + VariantDescription([vprop4, vprop6], label="al"), # --- vprop5 --- # - VariantDescription([vprop5, vprop6]), + VariantDescription([vprop5, vprop6], label="am"), # variants with 1 property - VariantDescription([vprop1]), - VariantDescription([vprop2]), - VariantDescription([vprop3]), - VariantDescription([vprop4]), - VariantDescription([vprop5]), - VariantDescription([vprop6]), + VariantDescription([vprop1], label="an"), + VariantDescription([vprop2], label="ao"), + VariantDescription([vprop3], label="ap"), + VariantDescription([vprop4], label="aq"), + VariantDescription([vprop5], label="ar"), + VariantDescription([vprop6], label="as"), ] # fmt: on @@ -207,7 +207,7 @@ def test_filter_variants_only_one_prop_allowed( vdescs=inputs_vdescs, allowed_properties=[vprop4], ) - ) == [VariantDescription([vprop4])] + ) == [VariantDescription([vprop4], label="aq")] assert ( list( @@ -258,7 +258,7 @@ def test_filter_variants_forbidden_feature_allowed_prop( allowed_properties=[vprop4], forbidden_features=[vprop2.feature_object], ) - ) == [VariantDescription([vprop4])] + ) == [VariantDescription([vprop4], label="aq")] def test_filter_variants_forbidden_namespace_allowed_prop( @@ -276,7 +276,7 @@ def test_filter_variants_forbidden_namespace_allowed_prop( allowed_properties=[vprop4], forbidden_namespaces=["NotExisting"], ) - ) == [VariantDescription([vprop4])] + ) == [VariantDescription([vprop4], label="aq")] assert list( filter_variants( @@ -284,7 +284,7 @@ def test_filter_variants_forbidden_namespace_allowed_prop( allowed_properties=[vprop4], forbidden_namespaces=[vprop1.namespace], ) - ) == [VariantDescription([vprop4])] + ) == [VariantDescription([vprop4], label="aq")] def test_filter_variants_only_remove_duplicates( @@ -320,22 +320,22 @@ def test_filter_variants_remove_duplicates_and_namespaces( expected_vdescs = [ # --- vprop3 --- # - VariantDescription([vprop3, vprop4, vprop6]), - VariantDescription([vprop3, vprop5, vprop6]), + VariantDescription([vprop3, vprop4, vprop6], label="test"), + VariantDescription([vprop3, vprop5, vprop6], label="test"), # variants with 2 properties # --- vprop3 --- # - VariantDescription([vprop3, vprop4]), - VariantDescription([vprop3, vprop5]), - VariantDescription([vprop3, vprop6]), + VariantDescription([vprop3, vprop4], label="test"), + VariantDescription([vprop3, vprop5], label="test"), + VariantDescription([vprop3, vprop6], label="test"), # --- vprop4 --- # - VariantDescription([vprop4, vprop6]), + VariantDescription([vprop4, vprop6], label="test"), # --- vprop5 --- # - VariantDescription([vprop5, vprop6]), + VariantDescription([vprop5, vprop6], label="test"), # variants with 1 property - VariantDescription([vprop3]), - VariantDescription([vprop4]), - VariantDescription([vprop5]), - VariantDescription([vprop6]), + VariantDescription([vprop3], label="test"), + VariantDescription([vprop4], label="test"), + VariantDescription([vprop5], label="test"), + VariantDescription([vprop6], label="test"), # Null-Variant is never removed and last VariantDescription(), ] @@ -379,12 +379,12 @@ def test_filter_variants_remove_duplicates_and_features( expected_vdescs = [ # variants with 2 properties # --- vprop1 --- # - VariantDescription([vprop1, vprop4]), - VariantDescription([vprop1, vprop5]), + VariantDescription([vprop1, vprop4], label="test"), + VariantDescription([vprop1, vprop5], label="test"), # variants with 1 property - VariantDescription([vprop1]), - VariantDescription([vprop4]), - VariantDescription([vprop5]), + VariantDescription([vprop1], label="test"), + VariantDescription([vprop4], label="test"), + VariantDescription([vprop5], label="test"), # Null-Variant is never removed and last VariantDescription(), ] @@ -434,12 +434,12 @@ def test_filter_variants_remove_duplicates_and_properties( expected_vdescs = [ # variants with 2 properties # --- vprop1 --- # - VariantDescription([vprop1, vprop4]), - VariantDescription([vprop1, vprop5]), + VariantDescription([vprop1, vprop4], label="test"), + VariantDescription([vprop1, vprop5], label="test"), # variants with 1 property - VariantDescription([vprop1]), - VariantDescription([vprop4]), - VariantDescription([vprop5]), + VariantDescription([vprop1], label="test"), + VariantDescription([vprop4], label="test"), + VariantDescription([vprop5], label="test"), # Null-Variant is never removed and last VariantDescription(), ] @@ -520,48 +520,48 @@ def test_sort_and_filter_supported_variants( # 1. Everything with vprop6 # 1.1. + vprop3 # 1.1.1. + vprop5 - VariantDescription([vprop1, vprop3, vprop5, vprop6]), - VariantDescription([vprop3, vprop5, vprop6]), + VariantDescription([vprop1, vprop3, vprop5, vprop6], label="g"), + VariantDescription([vprop3, vprop5, vprop6], label="y"), # 1.1.2. + vprop4 - VariantDescription([vprop1, vprop3, vprop4, vprop6]), - VariantDescription([vprop3, vprop4, vprop6]), + VariantDescription([vprop1, vprop3, vprop4, vprop6], label="f"), + VariantDescription([vprop3, vprop4, vprop6], label="x"), # 1.1.3. + vprop1 - VariantDescription([vprop1, vprop3, vprop6]), + VariantDescription([vprop1, vprop3, vprop6], label="p"), # 1.1.4. vprop6 + vprop3 - VariantDescription([vprop3, vprop6]), + VariantDescription([vprop3, vprop6], label="ak"), # 1.2. + vprop5 - VariantDescription([vprop1, vprop5, vprop6]), - VariantDescription([vprop5, vprop6]), + VariantDescription([vprop1, vprop5, vprop6], label="r"), + VariantDescription([vprop5, vprop6], label="am"), # 1.3. + vprop4 - VariantDescription([vprop1, vprop4, vprop6]), - VariantDescription([vprop4, vprop6]), + VariantDescription([vprop1, vprop4, vprop6], label="q"), + VariantDescription([vprop4, vprop6], label="al"), # 1.4. + vprop1 - VariantDescription([vprop1, vprop6]), + VariantDescription([vprop1, vprop6], label="ad"), # 1. sole vprop6 - VariantDescription([vprop6]), + VariantDescription([vprop6], label="as"), # 2. Everything with vprop3 # 2.1. + vprop5 - VariantDescription([vprop1, vprop3, vprop5]), - VariantDescription([vprop3, vprop5]), + VariantDescription([vprop1, vprop3, vprop5], label="o"), + VariantDescription([vprop3, vprop5], label="aj"), # 2.2. + vprop4 - VariantDescription([vprop1, vprop3, vprop4]), - VariantDescription([vprop3, vprop4]), + VariantDescription([vprop1, vprop3, vprop4], label="n"), + VariantDescription([vprop3, vprop4], label="ai"), # 2.3. + vprop1 - VariantDescription([vprop1, vprop3]), + VariantDescription([vprop1, vprop3], label="aa"), # 2. sole vprop3 - VariantDescription([vprop3]), + VariantDescription([vprop3], label="ap"), # 3. vprop5 - VariantDescription([vprop1, vprop5]), - VariantDescription([vprop5]), + VariantDescription([vprop1, vprop5], label="ac"), + VariantDescription([vprop5], label="ar"), # 4. vprop4 - VariantDescription([vprop1, vprop4]), - VariantDescription([vprop4]), + VariantDescription([vprop1, vprop4], label="ab"), + VariantDescription([vprop4], label="aq"), # 5. sole vprop1 - VariantDescription([vprop1]), + VariantDescription([vprop1], label="an"), # Null-Variant is never removed and last - Implicitly added VariantDescription(), @@ -590,11 +590,11 @@ def test_sort_and_filter_supported_variants( ("vdescs", "feature_priorities"), [ ( - [VariantDescription([VariantProperty("a", "b", "c")])], + [VariantDescription([VariantProperty("a", "b", "c")], label="test")], "not a list", ), ( - [VariantDescription([VariantProperty("a", "b", "c")])], + [VariantDescription([VariantProperty("a", "b", "c")], label="test")], {"a": [VariantFeature("not_a", "variantproperty")]}, ), ("not a list", {"a": ["a"]}), diff --git a/tests/resolver/test_sorting.py b/tests/resolver/test_sorting.py index c3bcd40b..d730e352 100644 --- a/tests/resolver/test_sorting.py +++ b/tests/resolver/test_sorting.py @@ -239,13 +239,16 @@ def test_sort_variants_descriptions() -> None: VariantProperty(namespace="omnicorp", feature="feat_b", value="value1"), VariantProperty(namespace="omnicorp", feature="feat_c", value="value"), VariantProperty(namespace="other_corp", feature="feat_c", value="value"), - ] + ], + label="a", ) vdesc2 = VariantDescription( - [VariantProperty(namespace="other_corp", feature="feat_a", value="value")] + [VariantProperty(namespace="other_corp", feature="feat_a", value="value")], + label="b", ) vdesc3 = VariantDescription( - [VariantProperty(namespace="omnicorp", feature="feat_a", value="value")] + [VariantProperty(namespace="omnicorp", feature="feat_a", value="value")], + label="c", ) assert sort_variants_descriptions( @@ -317,7 +320,8 @@ def test_sort_variants_descriptions() -> None: VariantProperty( namespace="omnicorp", feature="feat", value="other_value" ) - ] + ], + label="a", ), VariantDescription( properties=[ @@ -326,6 +330,7 @@ def test_sort_variants_descriptions() -> None: namespace="omnicorp", feature="other_feat", value="other_value" ), ], + label="b", ), ], ) @@ -347,13 +352,16 @@ def test_sort_variants_descriptions_ranking_validation_error( [ ("not a list", [VariantProperty("a", "b", "c")]), (["not a VariantDescription"], [VariantProperty("a", "b", "c")]), - (VariantDescription([VariantProperty("a", "b", "c")]), "not a list or None"), ( - VariantDescription([VariantProperty("a", "b", "c")]), + VariantDescription([VariantProperty("a", "b", "c")], label="test"), + "not a list or None", + ), + ( + VariantDescription([VariantProperty("a", "b", "c")], label="test"), VariantProperty("not", "a", "list"), ), ( - VariantDescription([VariantProperty("a", "b", "c")]), + VariantDescription([VariantProperty("a", "b", "c")], label="test"), [{"not a VariantProperty": True}], ), ], From 98d9cba062684d11c10d6d932f2c4ae263bed6a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Fri, 10 Apr 2026 20:19:39 +0200 Subject: [PATCH 07/19] Update more tests to pass variant label 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 | 94 ++++++++++++++++------------------- tests/test_utils.py | 117 ++++++++++++++++++++++---------------------- tests/utils.py | 4 +- 3 files changed, 102 insertions(+), 113 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index d6ed1d35..dab8da74 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -import re import string from collections.abc import Generator from typing import TYPE_CHECKING @@ -336,7 +335,7 @@ def test_validate_variant(optional: bool) -> None: # Verify whether the variant properties are valid res = validate_variant( - VariantDescription(list(expected.keys())), + VariantDescription(list(expected.keys()), label="test"), variant_info=variant_info, ) @@ -357,10 +356,10 @@ def test_validate_variant(optional: bool) -> None: @pytest.mark.parametrize( "pyproject_toml", [None, PYPROJECT_TOML, PYPROJECT_TOML_MINIMAL] ) -@pytest.mark.parametrize("label", [None, "foo", "xy1.2"]) +@pytest.mark.parametrize("label", ["foo", "xy1.2"]) def test_make_variant_dist_info( pyproject_toml: VariantsJsonDict | None, - label: str | None, + label: str, ) -> None: expected: VariantsJsonDict = { VARIANTS_JSON_SCHEMA_KEY: VARIANTS_JSON_SCHEMA_URL, @@ -441,7 +440,8 @@ def test_make_variant_dist_info( VariantProperty("ns1", "f1", "p1"), VariantProperty("ns1", "f2", "p2"), VariantProperty("ns2", "f1", "p1"), - ] + ], + label=label, ), variant_info=VariantPyProjectToml(pyproject_toml) # type: ignore[arg-type] if pyproject_toml is not None @@ -478,7 +478,8 @@ def common_variant_info() -> VariantInfo: [ VariantProperty("test_namespace", "name2", "val2c"), VariantProperty("second_namespace", "name3", "val3a"), - ] + ], + label="test", ), True, ), @@ -486,7 +487,8 @@ def common_variant_info() -> VariantInfo: VariantDescription( [ VariantProperty("test_namespace", "name1", "val1c"), - ] + ], + label="test", ), False, ), @@ -526,27 +528,30 @@ def test_check_variant_supported_generic() -> None: [ VariantProperty("test_namespace", "name2", "val2c"), VariantProperty("second_namespace", "name3", "val3a"), - ] + ], + label="test", ), variant_info=variant_info, ) # test an usupported variant assert not check_variant_supported( - vdesc=VariantDescription([VariantProperty("test_namespace", "name1", "val1c")]), + vdesc=VariantDescription( + [VariantProperty("test_namespace", "name1", "val1c")], label="test" + ), variant_info=variant_info, ) -@pytest.mark.parametrize("label", [None, "foo"]) -def test_get_variant_environment_dict(label: str | None) -> None: +def test_get_variant_environment_dict() -> None: vdesc = VariantDescription( [ VariantProperty("ns1", "feat1", "val1"), VariantProperty("ns1", "feat2", "val2"), VariantProperty("ns2", "feat1", "val1"), VariantProperty("ns3", "feat2", "val2"), - ] + ], + label="foo", ) expected: dict[str, set[str] | str] = { "variant_features": { @@ -566,10 +571,9 @@ def test_get_variant_environment_dict(label: str | None) -> None: "ns2 :: feat1 :: val1", "ns3 :: feat2 :: val2", }, + "variant_label": "foo", } - if label is not None: - expected["variant_label"] = label - assert get_variant_environment_dict(vdesc, label) == expected + assert get_variant_environment_dict(vdesc, "foo") == expected def test_make_variant_dist_info_invalid_label(): @@ -583,18 +587,17 @@ def test_make_variant_dist_info_invalid_label(): match=rf"{NULL_VARIANT_LABEL!r} label can be used only for the null variant", ): make_variant_dist_info( - VariantDescription([VariantProperty("a", "b", "c")]), + VariantDescription( + [VariantProperty("a", "b", "c")], label=NULL_VARIANT_LABEL + ), variant_label=NULL_VARIANT_LABEL, ) with pytest.raises( ValidationError, - match=re.escape( - "Invalid variant label: 'foo/bar' " - "(must consist only of alphanumeric characters, underscores and dots)" - ), + match=("Value `foo/bar` must match regex"), ): make_variant_dist_info( - VariantDescription([VariantProperty("a", "b", "c")]), + VariantDescription([VariantProperty("a", "b", "c")], label="foo/bar"), variant_label="foo/bar", ) @@ -606,35 +609,17 @@ def test_get_variant_label() -> None: == NULL_VARIANT_LABEL ) - assert ( - get_variant_label(VariantDescription([VariantProperty("a", "b", "c")])) - == "01a9783a" - ) - assert ( - get_variant_label( - VariantDescription( - [VariantProperty("a", "b", "c"), VariantProperty("d", "e", "f")] - ) - ) - == "eb9a66a7" - ) assert ( get_variant_label( - VariantDescription( - [VariantProperty("d", "e", "f"), VariantProperty("a", "b", "c")] - ) + VariantDescription([VariantProperty("a", "b", "c")], label="foo"), "foo" ) - == "eb9a66a7" - ) - - assert ( - get_variant_label(VariantDescription([VariantProperty("a", "b", "c")]), "foo") == "foo" ) assert ( get_variant_label( VariantDescription( - [VariantProperty("a", "b", "c"), VariantProperty("d", "e", "f")] + [VariantProperty("a", "b", "c"), VariantProperty("d", "e", "f")], + label="foo", ), "foo", ) @@ -645,24 +630,23 @@ def test_get_variant_label() -> None: ValidationError, match=rf"Null variant must always use {NULL_VARIANT_LABEL!r} label", ): - get_variant_label(VariantDescription([]), "foo") + get_variant_label(VariantDescription([], label="foo"), "foo") with pytest.raises( ValidationError, match=rf"{NULL_VARIANT_LABEL!r} label can be used only for the null variant", ): get_variant_label( - VariantDescription([VariantProperty("a", "b", "c")]), + VariantDescription( + [VariantProperty("a", "b", "c")], label=NULL_VARIANT_LABEL + ), NULL_VARIANT_LABEL, ) with pytest.raises( ValidationError, - match=re.escape( - "Invalid variant label: 'foo/bar' " - "(must consist only of alphanumeric characters, underscores and dots)" - ), + match=("Value `foo/bar` must match regex"), ): get_variant_label( - VariantDescription([VariantProperty("a", "b", "c")]), + VariantDescription([VariantProperty("a", "b", "c")], label="foo/bar"), "foo/bar", ) @@ -677,7 +661,8 @@ def test_make_variant_dist_info_expand_aot_plugin_properties( vdesc = VariantDescription( [ VariantProperty("aot_plugin", "name1", "val1a"), - ] + ], + label="test", ) plugin_api = "tests.mocked_plugins:MockedAoTPlugin" vinfo = VariantInfo( @@ -745,7 +730,8 @@ def test_make_variant_dist_info_invalid_aot_plugin_property() -> None: vdesc = VariantDescription( [ VariantProperty("aot_plugin", "name1", "val1d"), - ] + ], + label="test", ) plugin_api = "tests.mocked_plugins:MockedAoTPlugin" vinfo = VariantInfo( @@ -776,7 +762,8 @@ def test_make_variant_dist_info_invalid_aot_plugin_multi_value() -> None: vdesc = VariantDescription( [ VariantProperty("aot_plugin", "name1", "val1a"), - ] + ], + label="test", ) plugin_api = "tests.mocked_plugins:MultiValueAoTPlugin" vinfo = VariantInfo( @@ -806,7 +793,8 @@ def test_make_variant_dist_info_really_invalid_build_plugin() -> None: vdesc = VariantDescription( [ VariantProperty("second_namespace", "name3", "val3a"), - ] + ], + label="test", ) plugin_api = "tests.mocked_plugins:MockedPluginB" vinfo = VariantInfo( diff --git a/tests/test_utils.py b/tests/test_utils.py index c809073b..f6298aa5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -39,29 +39,29 @@ def test_get_combinations(configs: list[ProviderConfig]) -> None: ] assert list(get_combinations(configs, namespace_priorities)) == [ - VariantDescription([val1a, val2a, val3a]), - VariantDescription([val1a, val2a]), - VariantDescription([val1a, val2b, val3a]), - VariantDescription([val1a, val2b]), - VariantDescription([val1a, val2c, val3a]), - VariantDescription([val1a, val2c]), - VariantDescription([val1a, val3a]), - VariantDescription([val1a]), - VariantDescription([val1b, val2a, val3a]), - VariantDescription([val1b, val2a]), - VariantDescription([val1b, val2b, val3a]), - VariantDescription([val1b, val2b]), - VariantDescription([val1b, val2c, val3a]), - VariantDescription([val1b, val2c]), - VariantDescription([val1b, val3a]), - VariantDescription([val1b]), - VariantDescription([val2a, val3a]), - VariantDescription([val2a]), - VariantDescription([val2b, val3a]), - VariantDescription([val2b]), - VariantDescription([val2c, val3a]), - VariantDescription([val2c]), - VariantDescription([val3a]), + VariantDescription([val1a, val2a, val3a], label="0_0"), + VariantDescription([val1a, val2a], label="0_1"), + VariantDescription([val1a, val2b, val3a], label="0_2"), + VariantDescription([val1a, val2b], label="0_3"), + VariantDescription([val1a, val2c, val3a], label="0_4"), + VariantDescription([val1a, val2c], label="0_5"), + VariantDescription([val1a, val3a], label="0_6"), + VariantDescription([val1a], label="0_7"), + VariantDescription([val1b, val2a, val3a], label="0_8"), + VariantDescription([val1b, val2a], label="0_9"), + VariantDescription([val1b, val2b, val3a], label="0_10"), + VariantDescription([val1b, val2b], label="0_11"), + VariantDescription([val1b, val2c, val3a], label="0_12"), + VariantDescription([val1b, val2c], label="0_13"), + VariantDescription([val1b, val3a], label="0_14"), + VariantDescription([val1b], label="0_15"), + VariantDescription([val2a, val3a], label="1_0"), + VariantDescription([val2a], label="1_1"), + VariantDescription([val2b, val3a], label="1_2"), + VariantDescription([val2b], label="1_3"), + VariantDescription([val2c, val3a], label="1_4"), + VariantDescription([val2c], label="1_5"), + VariantDescription([val3a], label="2_0"), VariantDescription(), ] @@ -81,29 +81,29 @@ def test_get_combinations_flipped_order(configs: list[ProviderConfig]) -> None: ] assert list(get_combinations(configs, namespace_priorities)) == [ - VariantDescription([val1a, val2a, val3a]), - VariantDescription([val1a, val2b, val3a]), - VariantDescription([val1a, val2c, val3a]), - VariantDescription([val1a, val3a]), - VariantDescription([val1b, val2a, val3a]), - VariantDescription([val1b, val2b, val3a]), - VariantDescription([val1b, val2c, val3a]), - VariantDescription([val1b, val3a]), - VariantDescription([val2a, val3a]), - VariantDescription([val2b, val3a]), - VariantDescription([val2c, val3a]), - VariantDescription([val3a]), - VariantDescription([val1a, val2a]), - VariantDescription([val1a, val2b]), - VariantDescription([val1a, val2c]), - VariantDescription([val1a]), - VariantDescription([val1b, val2a]), - VariantDescription([val1b, val2b]), - VariantDescription([val1b, val2c]), - VariantDescription([val1b]), - VariantDescription([val2a]), - VariantDescription([val2b]), - VariantDescription([val2c]), + VariantDescription([val1a, val2a, val3a], label="0_0"), + VariantDescription([val1a, val2b, val3a], label="0_1"), + VariantDescription([val1a, val2c, val3a], label="0_2"), + VariantDescription([val1a, val3a], label="0_3"), + VariantDescription([val1b, val2a, val3a], label="0_4"), + VariantDescription([val1b, val2b, val3a], label="0_5"), + VariantDescription([val1b, val2c, val3a], label="0_6"), + VariantDescription([val1b, val3a], label="0_7"), + VariantDescription([val2a, val3a], label="0_8"), + VariantDescription([val2b, val3a], label="0_9"), + VariantDescription([val2c, val3a], label="0_10"), + VariantDescription([val3a], label="0_11"), + VariantDescription([val1a, val2a], label="1_0"), + VariantDescription([val1a, val2b], label="1_1"), + VariantDescription([val1a, val2c], label="1_2"), + VariantDescription([val1a], label="1_3"), + VariantDescription([val1b, val2a], label="1_4"), + VariantDescription([val1b, val2b], label="1_5"), + VariantDescription([val1b, val2c], label="1_6"), + VariantDescription([val1b], label="1_7"), + VariantDescription([val2a], label="2_0"), + VariantDescription([val2b], label="2_1"), + VariantDescription([val2c], label="2_2"), VariantDescription(), ] @@ -117,7 +117,8 @@ def test_get_combinations_one_one_namespace_one(configs: list[ProviderConfig]) - VariantDescription( [ VariantProperty("second_namespace", "name3", "val3a"), - ] + ], + label="0_0", ), VariantDescription(), ] @@ -136,17 +137,17 @@ def test_get_combinations_one_one_namespace_two(configs: list[ProviderConfig]) - ] assert list(get_combinations(configs, namespace_priorities)) == [ - VariantDescription([val1a, val2a]), - VariantDescription([val1a, val2b]), - VariantDescription([val1a, val2c]), - VariantDescription([val1a]), - VariantDescription([val1b, val2a]), - VariantDescription([val1b, val2b]), - VariantDescription([val1b, val2c]), - VariantDescription([val1b]), - VariantDescription([val2a]), - VariantDescription([val2b]), - VariantDescription([val2c]), + VariantDescription([val1a, val2a], label="0_0"), + VariantDescription([val1a, val2b], label="0_1"), + VariantDescription([val1a, val2c], label="0_2"), + VariantDescription([val1a], label="0_3"), + VariantDescription([val1b, val2a], label="0_4"), + VariantDescription([val1b, val2b], label="0_5"), + VariantDescription([val1b, val2c], label="0_6"), + VariantDescription([val1b], label="0_7"), + VariantDescription([val2a], label="1_0"), + VariantDescription([val2b], label="1_1"), + VariantDescription([val2c], label="1_2"), VariantDescription(), ] diff --git a/tests/utils.py b/tests/utils.py index ea05b9fb..4d894b4b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -51,8 +51,8 @@ def yield_all_values( yield [VariantProperty(namespace, feature, value)] for start in range(len(all_properties)): - for properties in yield_all_values(all_properties[start:]): - yield VariantDescription(properties) + for j, properties in enumerate(yield_all_values(all_properties[start:])): + yield VariantDescription(properties, label=f"{start}_{j}") # Finish by the null variant yield VariantDescription() From 71602f81b0497b49d07a24c5d5f8873cc8b5379c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 13 Apr 2026 17:43:46 +0200 Subject: [PATCH 08/19] VariantDistInfo: use the label from `VariantDescription` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- tests/test_variant_dist_info.py | 10 +++++----- variantlib/variant_dist_info.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_variant_dist_info.py b/tests/test_variant_dist_info.py index cd5c5845..1e78a4a8 100644 --- a/tests/test_variant_dist_info.py +++ b/tests/test_variant_dist_info.py @@ -85,17 +85,17 @@ def test_new_variant_dist_info() -> None: namespace_priorities=["ns"], providers={"ns": ProviderInfo(requires=["ns-pkg"])} ) variant_dist_info = VariantDistInfo(variant_info) - vdesc = VariantDescription([VariantProperty("ns", "f", "v")]) + vdesc = VariantDescription([VariantProperty("ns", "f", "v")], label="a") variant_dist_info.variant_desc = vdesc assert variant_dist_info.namespace_priorities == variant_info.namespace_priorities assert variant_dist_info.providers == variant_info.providers - assert variant_dist_info.variant_label == vdesc.hexdigest + assert variant_dist_info.variant_label == "a" assert variant_dist_info.variant_desc == vdesc # changing vdesc should update the label - vdesc2 = VariantDescription([VariantProperty("ns2", "f2", "v2")]) + vdesc2 = VariantDescription([VariantProperty("ns2", "f2", "v2")], label="b") variant_dist_info.variant_desc = vdesc2 - assert variant_dist_info.variant_label == vdesc2.hexdigest + assert variant_dist_info.variant_label == "b" assert variant_dist_info.variant_desc == vdesc2 # set a custom label @@ -105,5 +105,5 @@ def test_new_variant_dist_info() -> None: # changing vdesc should reset the label variant_dist_info.variant_desc = vdesc2 - assert variant_dist_info.variant_label == vdesc2.hexdigest + assert variant_dist_info.variant_label == "b" assert variant_dist_info.variant_desc == vdesc2 diff --git a/variantlib/variant_dist_info.py b/variantlib/variant_dist_info.py index 79f6cecc..bc3fd87f 100644 --- a/variantlib/variant_dist_info.py +++ b/variantlib/variant_dist_info.py @@ -56,4 +56,4 @@ def variant_desc(self) -> VariantDescription: @variant_desc.setter def variant_desc(self, new_desc: VariantDescription) -> None: - self.variants = {new_desc.hexdigest: new_desc} + self.variants = {new_desc.label: new_desc} From 17282d13c965a18b61ecb5ffdadbb9a663cd64c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 13 Apr 2026 17:59:20 +0200 Subject: [PATCH 09/19] Fix remaining test failures and enable label validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- tests/models/test_variant.py | 1 - tests/test_variants_json.py | 19 +++++++++++-------- variantlib/commands/get_variant_hash.py | 5 ++++- variantlib/commands/make_variant.py | 2 +- variantlib/models/variant.py | 17 +++++++---------- 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/tests/models/test_variant.py b/tests/models/test_variant.py index 3485418d..62a107d1 100644 --- a/tests/models/test_variant.py +++ b/tests/models/test_variant.py @@ -427,7 +427,6 @@ def test_random_hexdigest(vdesc: VariantDescription) -> None: assert len(vdesc.hexdigest) == VARIANT_HASH_LENGTH -@pytest.mark.xfail(reason="Validation is disabled for porting") def test_null_variant_label(): with pytest.raises( ValidationError, diff --git a/tests/test_variants_json.py b/tests/test_variants_json.py index a8bc9822..51d2ec7e 100644 --- a/tests/test_variants_json.py +++ b/tests/test_variants_json.py @@ -317,16 +317,18 @@ def test_to_str() -> None: [ VariantProperty("ns1", "f1", "v1"), VariantProperty("ns2", "f2", "v1"), - ] + ], + label="a", ) vdesc2 = VariantDescription( [ VariantProperty("ns2", "f2", "v2"), - ] + ], + label="b", ) variants_json.variants = { - vdesc1.hexdigest: vdesc1, - vdesc2.hexdigest: vdesc2, + "a": vdesc1, + "b": vdesc2, } assert json.loads(variants_json.to_str()) == { VARIANTS_JSON_SCHEMA_KEY: VARIANTS_JSON_SCHEMA_URL, @@ -347,8 +349,8 @@ def test_to_str() -> None: }, }, VARIANTS_JSON_VARIANT_DATA_KEY: { - "b3b0305c": {"ns1": {"f1": ["v1"]}, "ns2": {"f2": ["v1"]}}, - "9177ff3f": {"ns2": {"f2": ["v2"]}}, + "a": {"ns1": {"f1": ["v1"]}, "ns2": {"f2": ["v1"]}}, + "b": {"ns2": {"f2": ["v2"]}}, }, } @@ -501,12 +503,13 @@ def test_merge_variants() -> None: def test_null_variant_label(): with pytest.raises( ValidationError, - match=rf"{NULL_VARIANT_LABEL!r} label can only be used for the null variant", + match=rf"{NULL_VARIANT_LABEL!r} label can be used only for the null variant", ): VariantsJson( {VARIANTS_JSON_VARIANT_DATA_KEY: {NULL_VARIANT_LABEL: {"x": {"y": ["z"]}}}} ) with pytest.raises( - ValidationError, match=rf"Null variant must use {NULL_VARIANT_LABEL!r} label" + ValidationError, + match=rf"Null variant must always use {NULL_VARIANT_LABEL!r} label", ): VariantsJson({VARIANTS_JSON_VARIANT_DATA_KEY: {"zuul": {}}}) diff --git a/variantlib/commands/get_variant_hash.py b/variantlib/commands/get_variant_hash.py index ee145cb2..c8fb3b34 100644 --- a/variantlib/commands/get_variant_hash.py +++ b/variantlib/commands/get_variant_hash.py @@ -6,6 +6,7 @@ from variantlib import __package_name__ from variantlib.api import VariantDescription from variantlib.api import VariantProperty +from variantlib.constants import NULL_VARIANT_LABEL def get_variant_hash(args: list[str]) -> None: @@ -35,6 +36,8 @@ def get_variant_hash(args: list[str]) -> None: def _print_variant_hash(properties: list[VariantProperty]) -> None: # Transform properties into a VariantDescription - vdesc = VariantDescription(properties=properties) + vdesc = VariantDescription( + properties=properties, label="na" if properties else NULL_VARIANT_LABEL + ) sys.stdout.write(f"{vdesc.hexdigest}\n") diff --git a/variantlib/commands/make_variant.py b/variantlib/commands/make_variant.py index cf424a03..793ed013 100644 --- a/variantlib/commands/make_variant.py +++ b/variantlib/commands/make_variant.py @@ -169,7 +169,7 @@ def _make_variant( if not is_null_variant: # Transform properties into a VariantDescription - vdesc = VariantDescription(properties=properties) + vdesc = VariantDescription(properties=properties, label="na") if validate_properties: env_factory: DefaultIsolatedEnv | nullcontext[None] diff --git a/variantlib/models/variant.py b/variantlib/models/variant.py index ea443c13..3787720f 100644 --- a/variantlib/models/variant.py +++ b/variantlib/models/variant.py @@ -187,18 +187,15 @@ def __post_init__(self) -> None: # Execute the validator super().__post_init__() - # TODO: enable once we're done porting - if False: - if self.is_null_variant(): - if self.label != NULL_VARIANT_LABEL: - raise ValidationError( - f"Null variant must always use {NULL_VARIANT_LABEL!r} label" - ) - elif self.label == NULL_VARIANT_LABEL: + if self.is_null_variant(): + if self.label != NULL_VARIANT_LABEL: raise ValidationError( - f"{NULL_VARIANT_LABEL!r} label can be used only for the null " - "variant" + f"Null variant must always use {NULL_VARIANT_LABEL!r} label" ) + elif self.label == NULL_VARIANT_LABEL: + raise ValidationError( + f"{NULL_VARIANT_LABEL!r} label can be used only for the null variant" + ) def is_null_variant(self) -> bool: """ From 29bfe17ffdb5f221d9dabaee754f502fdfc09ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 13 Apr 2026 18:09:59 +0200 Subject: [PATCH 10/19] Default the non-null variant label to hexdigest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- variantlib/models/variant.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/variantlib/models/variant.py b/variantlib/models/variant.py index 3787720f..18300482 100644 --- a/variantlib/models/variant.py +++ b/variantlib/models/variant.py @@ -162,7 +162,7 @@ class VariantDescription(BaseModel): default_factory=list, ) - label: str = field( + label: str | None = field( metadata={ "validator": lambda val: validate_and( [ @@ -172,7 +172,7 @@ class VariantDescription(BaseModel): value=val, ), }, - default=NULL_VARIANT_LABEL, + default=None, ) def __post_init__(self) -> None: @@ -184,10 +184,15 @@ def __post_init__(self) -> None: # Only "legal way" to modify a frozen dataclass attribute post init. object.__setattr__(self, "properties", sorted(self.properties)) - # Execute the validator - super().__post_init__() - - if self.is_null_variant(): + # default the label to "null" or hexdigest + if self.label is None: + with contextlib.suppress(AttributeError): + object.__setattr__( + self, + "label", + NULL_VARIANT_LABEL if self.is_null_variant() else self.hexdigest, + ) + elif self.is_null_variant(): if self.label != NULL_VARIANT_LABEL: raise ValidationError( f"Null variant must always use {NULL_VARIANT_LABEL!r} label" @@ -197,6 +202,9 @@ def __post_init__(self) -> None: f"{NULL_VARIANT_LABEL!r} label can be used only for the null variant" ) + # Execute the validator + super().__post_init__() + def is_null_variant(self) -> bool: """ Check if the variant is a null variant. From 11b44b3e7ff65b73388c7efe16455da8d88ade2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 13 Apr 2026 18:28:15 +0200 Subject: [PATCH 11/19] VariantDistInfo: sync label in VariantDescription MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- tests/test_variant_dist_info.py | 4 +++- variantlib/variant_dist_info.py | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_variant_dist_info.py b/tests/test_variant_dist_info.py index 1e78a4a8..c6c2850d 100644 --- a/tests/test_variant_dist_info.py +++ b/tests/test_variant_dist_info.py @@ -101,7 +101,9 @@ def test_new_variant_dist_info() -> None: # set a custom label variant_dist_info.variant_label = "fancy2" assert variant_dist_info.variant_label == "fancy2" - assert variant_dist_info.variant_desc == vdesc2 + assert variant_dist_info.variant_desc == VariantDescription( + vdesc2.properties, label="fancy2" + ) # changing vdesc should reset the label variant_dist_info.variant_desc = vdesc2 diff --git a/variantlib/variant_dist_info.py b/variantlib/variant_dist_info.py index bc3fd87f..7f3b99be 100644 --- a/variantlib/variant_dist_info.py +++ b/variantlib/variant_dist_info.py @@ -2,16 +2,13 @@ import json from dataclasses import dataclass -from typing import TYPE_CHECKING from variantlib.constants import VARIANT_DIST_INFO_FILENAME from variantlib.errors import ValidationError +from variantlib.models.variant import VariantDescription from variantlib.models.variant_info import VariantInfo from variantlib.variants_json import VariantsJson -if TYPE_CHECKING: - from variantlib.models.variant import VariantDescription - @dataclass(init=False) class VariantDistInfo(VariantsJson): @@ -47,6 +44,9 @@ def variant_label(self) -> str: @variant_label.setter def variant_label(self, new_label: str) -> None: + self.variant_desc = VariantDescription( + self.variant_desc.properties, label=new_label + ) self.variants = {new_label: self.variant_desc} @property @@ -56,4 +56,5 @@ def variant_desc(self) -> VariantDescription: @variant_desc.setter def variant_desc(self, new_desc: VariantDescription) -> None: + assert new_desc.label is not None self.variants = {new_desc.label: new_desc} From 0abf7de808e4b36a4c6650829e0050b444f6363c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 13 Apr 2026 18:32:13 +0200 Subject: [PATCH 12/19] Remove the no-longer-necessary command hack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- variantlib/commands/get_variant_hash.py | 5 +---- variantlib/commands/make_variant.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/variantlib/commands/get_variant_hash.py b/variantlib/commands/get_variant_hash.py index c8fb3b34..ee145cb2 100644 --- a/variantlib/commands/get_variant_hash.py +++ b/variantlib/commands/get_variant_hash.py @@ -6,7 +6,6 @@ from variantlib import __package_name__ from variantlib.api import VariantDescription from variantlib.api import VariantProperty -from variantlib.constants import NULL_VARIANT_LABEL def get_variant_hash(args: list[str]) -> None: @@ -36,8 +35,6 @@ def get_variant_hash(args: list[str]) -> None: def _print_variant_hash(properties: list[VariantProperty]) -> None: # Transform properties into a VariantDescription - vdesc = VariantDescription( - properties=properties, label="na" if properties else NULL_VARIANT_LABEL - ) + vdesc = VariantDescription(properties=properties) sys.stdout.write(f"{vdesc.hexdigest}\n") diff --git a/variantlib/commands/make_variant.py b/variantlib/commands/make_variant.py index 793ed013..cf424a03 100644 --- a/variantlib/commands/make_variant.py +++ b/variantlib/commands/make_variant.py @@ -169,7 +169,7 @@ def _make_variant( if not is_null_variant: # Transform properties into a VariantDescription - vdesc = VariantDescription(properties=properties, label="na") + vdesc = VariantDescription(properties=properties) if validate_properties: env_factory: DefaultIsolatedEnv | nullcontext[None] From 9612488f1655d91c251f17714430fdf735199440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 13 Apr 2026 18:40:28 +0200 Subject: [PATCH 13/19] api: Update `get_variant_label()` to use `vdesc.label`, deprecate it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- tests/commands/test_analyze_wheel.py | 2 +- variantlib/api.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/commands/test_analyze_wheel.py b/tests/commands/test_analyze_wheel.py index 889e8452..61c0e510 100644 --- a/tests/commands/test_analyze_wheel.py +++ b/tests/commands/test_analyze_wheel.py @@ -83,7 +83,7 @@ def test_analyze_wheel_variant_custom_label( assert ( capsys.readouterr().out == """\ -############################## Variant: `60567bd9` ############################# +############################## Variant: `foo` ############################# installable_plugin :: feat1 :: val1c ################################################################################ """ diff --git a/variantlib/api.py b/variantlib/api.py index e79c497a..924e4abd 100644 --- a/variantlib/api.py +++ b/variantlib/api.py @@ -352,14 +352,13 @@ def get_variant_label( Get the label corresponding to `variant_desc`. If `custom_label` is provided, validate it and use it. If `custom_label` is invalid, raises a `ValidationError`. + + This function is deprecated: use VariantDescription.label instead. """ if custom_label is None: - return ( - NULL_VARIANT_LABEL - if variant_desc.is_null_variant() - else variant_desc.hexdigest - ) + assert variant_desc.label is not None + return variant_desc.label if variant_desc.is_null_variant(): if custom_label != NULL_VARIANT_LABEL: From b177035a1031a7de7f2cbfda799cb865ac0a2df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 13 Apr 2026 18:46:52 +0200 Subject: [PATCH 14/19] api: Deprecate remaining variant_label arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- variantlib/api.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/variantlib/api.py b/variantlib/api.py index 924e4abd..bc83b76b 100644 --- a/variantlib/api.py +++ b/variantlib/api.py @@ -5,6 +5,7 @@ import itertools import logging import pathlib +import warnings from typing import TYPE_CHECKING from variantlib.configuration import VariantConfiguration @@ -185,7 +186,7 @@ def make_variant_dist_info( instead. variant_label specifies the variant label to use. If not specified, - the default of variant hash is used. + the default of variant hash is used. Deprecated: set vdesc.label instead. If expand_aot_plugin_properties is True, then default-priorities for ahead-of-time plugins will be filled with the current list @@ -198,6 +199,12 @@ def make_variant_dist_info( variant_info = VariantInfo() variant_json = VariantDistInfo(variant_info) variant_json.variant_desc = vdesc + if variant_label is not None: + warnings.warn( + "Passing variant_label is deprecated, provide VariantDescription() " + "with label instead", + stacklevel=2, + ) variant_json.variant_label = get_variant_label(vdesc, variant_label) if expand_aot_plugin_properties: @@ -330,14 +337,21 @@ def get_variant_environment_dict( ) -> dict[str, set[str] | str]: """Get the dict for packaging Marker.evaluate()""" + assert variant_desc.label is not None ret: dict[str, set[str] | str] = { "variant_namespaces": {vprop.namespace for vprop in variant_desc.properties}, "variant_features": { vprop.feature_object.to_str() for vprop in variant_desc.properties }, "variant_properties": {vprop.to_str() for vprop in variant_desc.properties}, + "variant_label": variant_desc.label, } if variant_label is not None: + warnings.warn( + "Passing variant_label is deprecated, provide VariantDescription() " + "with label instead", + stacklevel=2, + ) ret["variant_label"] = variant_label return ret From 568e1db0a4581e751abff41caaff4a3e8e36a51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 13 Apr 2026 18:48:14 +0200 Subject: [PATCH 15/19] Also emit DeprecationWarning from `get_variant_label()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- variantlib/api.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/variantlib/api.py b/variantlib/api.py index bc83b76b..a0df89f4 100644 --- a/variantlib/api.py +++ b/variantlib/api.py @@ -374,6 +374,11 @@ def get_variant_label( assert variant_desc.label is not None return variant_desc.label + warnings.warn( + "get_variant_label() is deprecated, use VariantDescription.label instead", + stacklevel=2, + ) + if variant_desc.is_null_variant(): if custom_label != NULL_VARIANT_LABEL: raise ValidationError( From 75d9ab860309a4e686b0b2feeb6c86c50bf0ae8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 13 Apr 2026 19:34:03 +0200 Subject: [PATCH 16/19] Use a hack-default of "" instead of None to make typing easier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- variantlib/api.py | 2 -- variantlib/models/variant.py | 6 +++--- variantlib/variant_dist_info.py | 1 - 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/variantlib/api.py b/variantlib/api.py index a0df89f4..d1d26f9b 100644 --- a/variantlib/api.py +++ b/variantlib/api.py @@ -337,7 +337,6 @@ def get_variant_environment_dict( ) -> dict[str, set[str] | str]: """Get the dict for packaging Marker.evaluate()""" - assert variant_desc.label is not None ret: dict[str, set[str] | str] = { "variant_namespaces": {vprop.namespace for vprop in variant_desc.properties}, "variant_features": { @@ -371,7 +370,6 @@ def get_variant_label( """ if custom_label is None: - assert variant_desc.label is not None return variant_desc.label warnings.warn( diff --git a/variantlib/models/variant.py b/variantlib/models/variant.py index 18300482..c7369eb2 100644 --- a/variantlib/models/variant.py +++ b/variantlib/models/variant.py @@ -162,7 +162,7 @@ class VariantDescription(BaseModel): default_factory=list, ) - label: str | None = field( + label: str = field( metadata={ "validator": lambda val: validate_and( [ @@ -172,7 +172,7 @@ class VariantDescription(BaseModel): value=val, ), }, - default=None, + default="", ) def __post_init__(self) -> None: @@ -185,7 +185,7 @@ def __post_init__(self) -> None: object.__setattr__(self, "properties", sorted(self.properties)) # default the label to "null" or hexdigest - if self.label is None: + if not self.label: with contextlib.suppress(AttributeError): object.__setattr__( self, diff --git a/variantlib/variant_dist_info.py b/variantlib/variant_dist_info.py index 7f3b99be..9c2d3160 100644 --- a/variantlib/variant_dist_info.py +++ b/variantlib/variant_dist_info.py @@ -56,5 +56,4 @@ def variant_desc(self) -> VariantDescription: @variant_desc.setter def variant_desc(self, new_desc: VariantDescription) -> None: - assert new_desc.label is not None self.variants = {new_desc.label: new_desc} From c4f948673cb15648934101d0a6b6eadefb5a1ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 13 Apr 2026 19:35:54 +0200 Subject: [PATCH 17/19] api: Simplify `get_variants_by_priority()` to use vdesc labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- variantlib/api.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/variantlib/api.py b/variantlib/api.py index d1d26f9b..2fdf628d 100644 --- a/variantlib/api.py +++ b/variantlib/api.py @@ -58,6 +58,8 @@ def get_variants_by_priority( if not isinstance(variants_json, VariantsJson): variants_json = VariantsJson(variants_json) + assert all(vdesc.label == label for label, vdesc in variants_json.variants.items()) + venv_python_executable = ( venv_python_executable if venv_python_executable is None @@ -77,14 +79,9 @@ def get_variants_by_priority( ) config = VariantConfiguration.get_config() - label_map = { - vdesc.hexdigest: label for label, vdesc in variants_json.variants.items() - } - # handle the implicit null variant - label_map.setdefault(VariantDescription([]).hexdigest, NULL_VARIANT_LABEL) return [ - label_map[vdesc.hexdigest] + vdesc.label for vdesc in sort_and_filter_supported_variants( list(variants_json.variants.values()), supported_vprops, From bed4a86ace11ed21ee9bf103c508aef758f2c473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 14 Apr 2026 16:08:27 +0200 Subject: [PATCH 18/19] Check that labels are synced when dumping variants.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- variantlib/variants_json.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variantlib/variants_json.py b/variantlib/variants_json.py index 325956a1..52aabec9 100644 --- a/variantlib/variants_json.py +++ b/variantlib/variants_json.py @@ -90,6 +90,8 @@ def providers_dict(self) -> dict[str, dict[str, str | list[str] | bool]]: def to_str(self) -> str: """Serialize variants.json as a JSON string""" + assert all(label == vdesc.label for label, vdesc in self.variants.items()) + data: dict[str, Any] = { VARIANTS_JSON_SCHEMA_KEY: VARIANTS_JSON_SCHEMA_URL, VARIANT_INFO_DEFAULT_PRIO_KEY: dict(self._priorities_to_json()), From 59d644544bbcd707a7d80241fadeddcc58e4aa2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 31 Aug 2026 14:59:39 +0200 Subject: [PATCH 19/19] Remove duplicate removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This breaks the valid case of having two wheels with the same properties but different labels. Either way, the code was apparently only added "to avoid potential bugs", so let's remove it altogether. Signed-off-by: Michał Górny --- tests/resolver/test_filtering.py | 32 ---------------- tests/resolver/test_lib.py | 63 ++++++++++---------------------- variantlib/resolver/filtering.py | 27 -------------- variantlib/resolver/lib.py | 16 ++------ 4 files changed, 24 insertions(+), 114 deletions(-) diff --git a/tests/resolver/test_filtering.py b/tests/resolver/test_filtering.py index 32838184..0b9ea39e 100644 --- a/tests/resolver/test_filtering.py +++ b/tests/resolver/test_filtering.py @@ -1,7 +1,5 @@ from __future__ import annotations -import copy -import random from collections import deque import pytest @@ -12,7 +10,6 @@ from variantlib.resolver.filtering import filter_variants_by_features from variantlib.resolver.filtering import filter_variants_by_namespaces from variantlib.resolver.filtering import filter_variants_by_property -from variantlib.resolver.filtering import remove_duplicates @pytest.fixture(scope="session") @@ -38,35 +35,6 @@ def vdescs(vprops: list[VariantProperty]) -> list[VariantDescription]: ] -# =========================== `remove_duplicates` =========================== # - - -def test_remove_duplicates(vdescs: list[VariantDescription]) -> None: - assert len(vdescs) == 3 - - # using `copy.deepcopy` to ensure that all objects are actually unique - input_vdescs = [copy.deepcopy(random.choice(vdescs)) for _ in range(100)] - filtered_vdescs = list(remove_duplicates(input_vdescs)) - - assert len(filtered_vdescs) == 3 - - for vdesc in vdescs: - assert vdesc in filtered_vdescs - - -def test_remove_duplicates_empty() -> None: - assert list(remove_duplicates([])) == [] - - -@pytest.mark.parametrize( - "vdescs", - ["not a list", ["not a VariantDescription"]], -) -def test_remove_duplicates_validation_error(vdescs: list[VariantDescription]) -> None: - with pytest.raises(ValidationError): - deque(remove_duplicates(vdescs=vdescs), maxlen=0) - - # ===================== `filter_variants_by_namespaces` ===================== # diff --git a/tests/resolver/test_lib.py b/tests/resolver/test_lib.py index 3a5656d6..a1e0290a 100644 --- a/tests/resolver/test_lib.py +++ b/tests/resolver/test_lib.py @@ -14,7 +14,6 @@ from variantlib.resolver.filtering import filter_variants_by_features from variantlib.resolver.filtering import filter_variants_by_namespaces from variantlib.resolver.filtering import filter_variants_by_property -from variantlib.resolver.filtering import remove_duplicates from variantlib.resolver.lib import filter_variants from variantlib.resolver.lib import sort_and_filter_supported_variants from variantlib.resolver.sorting import sort_variant_properties @@ -55,11 +54,10 @@ def normalize_value_for_hashing( ) -def shuffle_vdescs_with_duplicates( +def shuffle_vdescs( vdescs: list[VariantDescription], ) -> list[VariantDescription]: - inputs_vdescs = [vdesc for vdesc in vdescs for _ in range(5)] - assert len(inputs_vdescs) == len(vdescs) * 5 + inputs_vdescs = list(vdescs) random.shuffle(inputs_vdescs) return inputs_vdescs @@ -189,8 +187,8 @@ def test_filter_variants_only_one_prop_allowed( assert len(vprops) == 6 _, _, _, vprop4, _, _ = vprops - # Shuffling the list & creating duplicates - inputs_vdescs = shuffle_vdescs_with_duplicates(vdescs=vdescs) + # Shuffling the list + inputs_vdescs = shuffle_vdescs(vdescs=vdescs) assert ( list( @@ -249,8 +247,8 @@ def test_filter_variants_forbidden_feature_allowed_prop( assert len(vprops) == 6 _, vprop2, _, vprop4, _, _ = vprops - # Shuffling the list & creating duplicates - inputs_vdescs = shuffle_vdescs_with_duplicates(vdescs=vdescs) + # Shuffling the list + inputs_vdescs = shuffle_vdescs(vdescs=vdescs) assert list( filter_variants( @@ -267,8 +265,8 @@ def test_filter_variants_forbidden_namespace_allowed_prop( assert len(vprops) == 6 vprop1, _, _, vprop4, _, _ = vprops - # Shuffling the list & creating duplicates - inputs_vdescs = shuffle_vdescs_with_duplicates(vdescs=vdescs) + # Shuffling the list + inputs_vdescs = shuffle_vdescs(vdescs=vdescs) assert list( filter_variants( @@ -287,36 +285,15 @@ def test_filter_variants_forbidden_namespace_allowed_prop( ) == [VariantDescription([vprop4], label="aq")] -def test_filter_variants_only_remove_duplicates( - vdescs: list[VariantDescription], vprops: list[VariantProperty] -) -> None: - assert len(vprops) == 6 - - # Shuffling the list & creating duplicates - inputs_vdescs = shuffle_vdescs_with_duplicates(vdescs=vdescs) - - ddiff = deep_diff( - list(filter_variants(vdescs=inputs_vdescs, allowed_properties=vprops)), - vdescs, - ignore_ordering=True, - ) - assert ddiff == {}, ddiff - - ddiff = deep_diff( - list(remove_duplicates(vdescs=inputs_vdescs)), vdescs, ignore_ordering=True - ) - assert ddiff == {}, ddiff - - -def test_filter_variants_remove_duplicates_and_namespaces( +def test_filter_variants_remove_namespaces( vdescs: list[VariantDescription], vprops: list[VariantProperty] ) -> None: assert len(vprops) == 6 _, _, vprop3, vprop4, vprop5, vprop6 = vprops - # Adding the `null-Variant` and shuffling the list & creating duplicates + # Adding the `null-Variant` and shuffling the list vdescs = [*vdescs, VariantDescription()] - inputs_vdescs = shuffle_vdescs_with_duplicates(vdescs=vdescs) + inputs_vdescs = shuffle_vdescs(vdescs=vdescs) expected_vdescs = [ # --- vprop3 --- # @@ -356,7 +333,7 @@ def test_filter_variants_remove_duplicates_and_namespaces( ddiff = deep_diff( list( filter_variants_by_namespaces( - remove_duplicates(vdescs=inputs_vdescs), + vdescs=inputs_vdescs, forbidden_namespaces=["omnicorp"], ) ), @@ -366,15 +343,15 @@ def test_filter_variants_remove_duplicates_and_namespaces( assert ddiff == {}, ddiff -def test_filter_variants_remove_duplicates_and_features( +def test_filter_variants_remove_features( vdescs: list[VariantDescription], vprops: list[VariantProperty] ) -> None: assert len(vprops) == 6 vprop1, vprop2, vprop3, vprop4, vprop5, vprop6 = vprops - # Adding the `null-Variant` and shuffling the list & creating duplicates + # Adding the `null-Variant` and shuffling the list vdescs = [*vdescs, VariantDescription()] - inputs_vdescs = shuffle_vdescs_with_duplicates(vdescs=vdescs) + inputs_vdescs = shuffle_vdescs(vdescs=vdescs) expected_vdescs = [ # variants with 2 properties @@ -411,7 +388,7 @@ def test_filter_variants_remove_duplicates_and_features( ddiff = deep_diff( list( filter_variants_by_features( - remove_duplicates(vdescs=inputs_vdescs), + vdescs=inputs_vdescs, forbidden_features=forbidden_features, ) ), @@ -421,7 +398,7 @@ def test_filter_variants_remove_duplicates_and_features( assert ddiff == {}, ddiff -def test_filter_variants_remove_duplicates_and_properties( +def test_filter_variants_remove_properties( vdescs: list[VariantDescription], vprops: list[VariantProperty] ) -> None: assert len(vprops) == 6 @@ -429,7 +406,7 @@ def test_filter_variants_remove_duplicates_and_properties( # Adding the `null-Variant` and shuffling the list & creating duplicates vdescs = [*vdescs, VariantDescription()] - inputs_vdescs = shuffle_vdescs_with_duplicates(vdescs=vdescs) + inputs_vdescs = shuffle_vdescs(vdescs=vdescs) expected_vdescs = [ # variants with 2 properties @@ -462,7 +439,7 @@ def test_filter_variants_remove_duplicates_and_properties( ddiff = deep_diff( list( filter_variants_by_property( - remove_duplicates(vdescs=inputs_vdescs), + vdescs=inputs_vdescs, allowed_properties=allowed_properties, forbidden_properties=[vprop2], ) @@ -569,7 +546,7 @@ def test_sort_and_filter_supported_variants( # fmt: on # Shuffling the list & creating duplicates - inputs_vdescs = shuffle_vdescs_with_duplicates(vdescs=vdescs) + inputs_vdescs = shuffle_vdescs(vdescs=vdescs) assert ( sort_and_filter_supported_variants( diff --git a/variantlib/resolver/filtering.py b/variantlib/resolver/filtering.py index da77def8..f8c0aa0d 100644 --- a/variantlib/resolver/filtering.py +++ b/variantlib/resolver/filtering.py @@ -20,33 +20,6 @@ logger = logging.getLogger(__name__) -def remove_duplicates( - vdescs: Iterable[VariantDescription], -) -> Generator[VariantDescription]: - # Input validation - validate_type(vdescs, Iterable) - - seen = set() - - def _should_include(vdesc: VariantDescription) -> bool: - """ - Check if any of the namespaces in the variant description are not allowed. - """ - validate_type(vdesc, VariantDescription) - - if vdesc.hexdigest in seen: - logger.info( - "Variant `%(vhash)s` has been removed because it is a duplicate", - {"vhash": vdesc.hexdigest}, - ) - return False - - seen.add(vdesc.hexdigest) - return True - - yield from filter(_should_include, vdescs) - - def filter_variants_by_namespaces( vdescs: Iterable[VariantDescription], forbidden_namespaces: list[str], diff --git a/variantlib/resolver/lib.py b/variantlib/resolver/lib.py index f966d22e..e06a04f7 100644 --- a/variantlib/resolver/lib.py +++ b/variantlib/resolver/lib.py @@ -15,7 +15,6 @@ from variantlib.resolver.filtering import filter_variants_by_features from variantlib.resolver.filtering import filter_variants_by_namespaces from variantlib.resolver.filtering import filter_variants_by_property -from variantlib.resolver.filtering import remove_duplicates from variantlib.resolver.sorting import sort_variant_properties from variantlib.resolver.sorting import sort_variants_descriptions from variantlib.validators.base import validate_type @@ -80,16 +79,9 @@ def filter_variants( if forbidden_properties is not None: validate_type(forbidden_properties, list[VariantProperty]) - # Step 1 - # Remove duplicates - There should never be any duplicates on the index - # - filename collision (same filename & same hash) - # - hash collision inside `variants.json` - # => Added for safety and to avoid any potential bugs - # (Note: In all fairness, even if it was to happen, it would most - # likely not be a problem given that we just pick the best match) - result = remove_duplicates(vdescs) + result = iter(vdescs) - # Step 2 [Optional] + # Step 1 [Optional] # Remove any `VariantDescription` which declares any `VariantProperty` with # a variant namespace explicitly forbidden by the user. if forbidden_namespaces is not None: @@ -98,7 +90,7 @@ def filter_variants( forbidden_namespaces=forbidden_namespaces, ) - # Step 3 [Optional] + # Step 2 [Optional] # Remove any `VariantDescription` which declares any `VariantProperty` with # `namespace :: feature` (aka. `VariantFeature`) explicitly forbidden by the user. if forbidden_features is not None: @@ -107,7 +99,7 @@ def filter_variants( forbidden_features=forbidden_features, ) - # Step 4 [Optional] + # Step 3 [Optional] # Remove any `VariantDescription` which declare any `VariantProperty` # `namespace :: feature :: value` unsupported on this platform or explicitly # forbidden by the user.