Skip to content

Commit 08a8cc9

Browse files
[Transition to dataclass] meta.py file
1 parent a5c6973 commit 08a8cc9

File tree

6 files changed

+118
-66
lines changed

6 files changed

+118
-66
lines changed

tests/test_combinations.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import json
2-
from pathlib import Path
32
import random
43
import string
4+
from collections.abc import Generator
5+
from pathlib import Path
56

7+
import jsondiff
8+
import pytest
69
from hypothesis import assume
710
from hypothesis import example
811
from hypothesis import given
912
from hypothesis import strategies as st
10-
import jsondiff
11-
import pytest
13+
1214
from variantlib.combination import filtered_sorted_variants
1315
from variantlib.combination import get_combinations
1416
from variantlib.config import KeyConfig
@@ -51,7 +53,7 @@ def test_get_combinations(configs):
5153
assert not differences, f"Serialization altered JSON: {differences}"
5254

5355

54-
def desc_to_json(desc_list: list[VariantDescription]) -> dict:
56+
def desc_to_json(desc_list: list[VariantDescription]) -> Generator:
5557
shuffled_desc_list = list(desc_list)
5658
random.shuffle(shuffled_desc_list)
5759
for desc in shuffled_desc_list:
@@ -63,9 +65,10 @@ def desc_to_json(desc_list: list[VariantDescription]) -> dict:
6365

6466

6567
def test_filtered_sorted_variants_roundtrip(configs):
66-
"""Test that we can round-trip all combinations via variants.json and get the same result."""
68+
"""Test that we can round-trip all combinations via variants.json and get the same
69+
result."""
6770
combinations = list(get_combinations(configs))
68-
variants_from_json = {k: v for k, v in desc_to_json(combinations)}
71+
variants_from_json = dict(desc_to_json(combinations))
6972
assert filtered_sorted_variants(variants_from_json, configs) == combinations
7073

7174

@@ -125,5 +128,5 @@ def filter_long_combinations():
125128
yield x
126129

127130
combinations = list(filter_long_combinations())
128-
variants_from_json = {k: v for k, v in desc_to_json(combinations)}
131+
variants_from_json = dict(desc_to_json(combinations))
129132
assert filtered_sorted_variants(variants_from_json, configs) == combinations

tests/test_config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
23
from variantlib.config import KeyConfig
34
from variantlib.config import ProviderConfig
45

@@ -7,7 +8,7 @@ def test_key_config_creation_valid():
78
"""Test valid creation of KeyConfig."""
89
key_config = KeyConfig(key="attr_nameA", values=["7", "4", "8", "12"])
910
assert key_config.key == "attr_nameA"
10-
assert key_config.values == ["7", "4", "8", "12"] # noqa: PD011
11+
assert key_config.values == ["7", "4", "8", "12"]
1112

1213

1314
def test_provider_config_creation_valid():

tests/test_meta.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import string
44

55
import pytest
6+
67
from variantlib import VARIANT_HASH_LEN
78
from variantlib.meta import VariantDescription
89
from variantlib.meta import VariantMeta
@@ -193,7 +194,7 @@ def test_variantdescription_invalid_data():
193194
"key": "access_key",
194195
"value": "secret_value",
195196
}
196-
with pytest.raises(AssertionError):
197+
with pytest.raises(TypeError):
197198
VariantDescription([invalid_meta])
198199

199200

variantlib/combination.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def get_combinations(data: list[ProviderConfig]) -> Generator[VariantDescription
2020
data = [
2121
[
2222
VariantMeta(provider=provider_cnf.provider, key=key_config.key, value=val)
23-
for val in key_config.values # noqa: PD011
23+
for val in key_config.values
2424
]
2525
for provider_cnf in data
2626
for key_config in provider_cnf.configs
@@ -30,27 +30,26 @@ def get_combinations(data: list[ProviderConfig]) -> Generator[VariantDescription
3030
for r in range(len(data), 0, -1):
3131
for combo in itertools.combinations(data, r):
3232
for vmetas in itertools.product(*combo):
33-
yield VariantDescription(data=vmetas)
33+
yield VariantDescription(data=list(vmetas))
3434

3535

36-
def unpack_variants_from_json(variants_from_json: dict
37-
) -> Generator[VariantDescription]:
36+
def unpack_variants_from_json(
37+
variants_from_json: dict,
38+
) -> Generator[VariantDescription]:
3839
def variant_to_metas(providers: dict) -> VariantMeta:
3940
for provider, keys in providers.items():
4041
for key, value in keys.items():
41-
yield VariantMeta(provider=provider,
42-
key=key,
43-
value=value)
42+
yield VariantMeta(provider=provider, key=key, value=value)
4443

4544
for variant_hash, providers in variants_from_json.items():
46-
desc = VariantDescription(variant_to_metas(providers))
45+
desc = VariantDescription(list(variant_to_metas(providers)))
4746
assert variant_hash == desc.hexdigest
4847
yield desc
4948

5049

51-
def filtered_sorted_variants(variants_from_json: dict,
52-
data: list[ProviderConfig]
53-
) -> Generator[VariantDescription]:
50+
def filtered_sorted_variants( # noqa: C901
51+
variants_from_json: dict, data: list[ProviderConfig]
52+
) -> Generator[VariantDescription]:
5453
providers = {}
5554
for provider_idx, provider_cnf in enumerate(data):
5655
keys = {}
@@ -94,15 +93,26 @@ def variant_sort_key_gen(desc: VariantDescription) -> Generator[tuple]:
9493
yield from (x[0:2] for x in meta_keys)
9594
yield from (x[2] for x in meta_keys)
9695

97-
res = sorted(filter(variant_filter,
98-
unpack_variants_from_json(variants_from_json)),
99-
key=lambda x: tuple(variant_sort_key_gen(x)))
96+
res = sorted(
97+
filter(variant_filter, unpack_variants_from_json(variants_from_json)),
98+
key=lambda x: tuple(variant_sort_key_gen(x)),
99+
)
100+
100101
if missing_providers:
101-
logger.warn("No plugins provide the following variant providers: "
102-
f"{' '.join(missing_providers)}; some variants will be ignored")
102+
logger.warning(
103+
"No plugins provide the following variant providers: "
104+
"%(provider)s; some variants will be ignored",
105+
provider=" ".join(missing_providers),
106+
)
107+
103108
for provider, provider_missing_keys in missing_keys.items():
104-
logger.warn(f"The {provider} provider does not provide the following expected keys: "
105-
f"{' '.join(provider_missing_keys)}; some variants will be ignored")
109+
logger.warning(
110+
"The %(provider)s provider does not provide the following expected keys: "
111+
"%(missing_keys)s; some variants will be ignored",
112+
provider=provider,
113+
missing_keys=" ".join(provider_missing_keys),
114+
)
115+
106116
return res
107117

108118

variantlib/meta.py

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,53 @@
22
import hashlib
33
import re
44
from collections.abc import Iterator
5+
from dataclasses import asdict
6+
from dataclasses import dataclass
7+
from dataclasses import field
58
from typing import Self
69

7-
from attrs import Converter
8-
from attrs import asdict
9-
from attrs import field
10-
from attrs import frozen
11-
from attrs import validators
12-
1310
from variantlib.constants import VALIDATION_REGEX
1411
from variantlib.constants import VALIDATION_VALUE_REGEX
1512
from variantlib.constants import VARIANT_HASH_LEN
13+
from variantlib.validators import validate_instance_of
14+
from variantlib.validators import validate_list_of
15+
from variantlib.validators import validate_matches_re
1616

1717

18-
@frozen
18+
@dataclass(frozen=True)
1919
class VariantMeta:
2020
provider: str = field(
21-
validator=[
22-
validators.instance_of(str),
23-
validators.matches_re(VALIDATION_REGEX),
24-
]
21+
metadata={
22+
"validators": [
23+
lambda v: validate_instance_of(v, str),
24+
lambda v: validate_matches_re(v, VALIDATION_REGEX),
25+
]
26+
}
2527
)
2628
key: str = field(
27-
validator=[
28-
validators.instance_of(str),
29-
validators.matches_re(VALIDATION_REGEX),
30-
]
29+
metadata={
30+
"validators": [
31+
lambda v: validate_instance_of(v, str),
32+
lambda v: validate_matches_re(v, VALIDATION_REGEX),
33+
]
34+
}
3135
)
3236
value: str = field(
33-
validator=[
34-
validators.instance_of(str),
35-
validators.matches_re(VALIDATION_VALUE_REGEX),
36-
]
37+
metadata={
38+
"validators": [
39+
lambda v: validate_instance_of(v, str),
40+
lambda v: validate_matches_re(v, VALIDATION_VALUE_REGEX),
41+
]
42+
}
3743
)
3844

45+
def __post_init__(self):
46+
# Execute the validators
47+
for field_name, field_def in self.__dataclass_fields__.items():
48+
value = getattr(self, field_name)
49+
for validator in field_def.metadata.get("validators", []):
50+
validator(value)
51+
3952
def __hash__(self) -> int:
4053
# Variant Metas are unique in provider & key and ignore the value.
4154
return hash((self.__class__, self.provider, self.key))
@@ -75,16 +88,8 @@ def deserialize(cls, data: dict[str, str]) -> Self:
7588
return cls(**data)
7689

7790

78-
def _sort_variantmetas(value: list[VariantMeta]) -> list[VariantMeta]:
79-
# We sort the data so that they always get displayed/hashed
80-
# in a consistent manner.
81-
with contextlib.suppress(AttributeError):
82-
return sorted(value, key=lambda x: (x.provider, x.key))
83-
# Error will be rejected during validation
84-
return value
85-
86-
87-
@frozen
91+
@dataclass(frozen=True)
92+
# @dataclass
8893
class VariantDescription:
8994
"""
9095
A `Variant` is being described by a N >= 1 `VariantMeta` metadata.
@@ -96,17 +101,31 @@ class VariantDescription:
96101
"""
97102

98103
data: list[VariantMeta] = field(
99-
validator=validators.instance_of(list), converter=Converter(_sort_variantmetas)
104+
metadata={
105+
"validators": [
106+
lambda v: validate_instance_of(v, list),
107+
lambda v: validate_list_of(v, VariantMeta),
108+
]
109+
}
100110
)
101111

102-
@data.validator
103-
def validate_data(self, _, data: list[VariantMeta]) -> None:
104-
"""The field `data` must comply with the following
105-
- Being a non-empty list of `VariantMeta`
106-
- Each value inside the list must be unique
107-
"""
108-
assert len(data) > 0
109-
assert all(isinstance(inst, VariantMeta) for inst in data)
112+
def __post_init__(self):
113+
# Execute the validators
114+
for field_name, field_def in self.__dataclass_fields__.items():
115+
value = getattr(self, field_name)
116+
for validator in field_def.metadata.get("validators", []):
117+
validator(value)
118+
119+
# We verify `data` is not empty
120+
assert len(self.data) > 0
121+
122+
# We sort the data so that they always get displayed/hashed
123+
# in a consistent manner.
124+
with contextlib.suppress(AttributeError):
125+
# Only "legal way" to modify a frozen dataclass attribute post init.
126+
object.__setattr__(
127+
self, "data", sorted(self.data, key=lambda x: (x.provider, x.key))
128+
)
110129

111130
# Detect multiple `VariantMeta` with identical provider/key
112131
# Ignores the attribute `value` of `VariantMeta`.
@@ -116,7 +135,7 @@ def validate_data(self, _, data: list[VariantMeta]) -> None:
116135
# an exception when there is a collision instead of
117136
# a silent behavior.
118137
seen = set()
119-
for vmeta in data:
138+
for vmeta in self.data:
120139
vmeta_hash = hash(vmeta)
121140
if vmeta_hash in seen:
122141
raise ValueError(

variantlib/validators.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import re
2+
from typing import Any
3+
4+
5+
def validate_instance_of(value: Any, expected_type: type) -> None:
6+
if not isinstance(value, expected_type):
7+
raise TypeError(f"Expected {expected_type}, got {type(value)}")
8+
9+
10+
def validate_list_of(data: list[Any], expected_type: type) -> None:
11+
for value in data:
12+
if not isinstance(value, expected_type):
13+
raise TypeError(f"Expected {expected_type}, got {type(value)}")
14+
15+
16+
def validate_matches_re(value: str, pattern: str) -> None:
17+
if not re.match(pattern, value):
18+
raise ValueError(f"Value must match regex {pattern}")

0 commit comments

Comments
 (0)