Skip to content

Commit bf4ffb0

Browse files
authored
Introduce support for dynamic config return values in plugins (#106)
* Introduce support for dynamic config return values in plugins Add support for the plugins dynamically adjusting the return values for `get_all_configs()` and `get_supported_configs()` based on input `known_properties` values. The values are either provided based on the variants being validated (for `get_all_configs()`) or based on all properties found in available wheels (for `get_supported_configs()`). The dynamic API is entirely optional -- the plugin can simply ignore the additional argument and return a fixed list as usual. It also permits for hybrid plugins that combine static and dynamic properties. * Remove the default argument from protocol * Add a `dynamic` property to distinguish dynamic plugins * Use `frozenset` in plugin API * Use a stricter value regex * Extend failing regex tests with \t
1 parent f8a66f9 commit bf4ffb0

File tree

14 files changed

+293
-67
lines changed

14 files changed

+293
-67
lines changed
Binary file not shown.
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
from __future__ import annotations
2+
13
from dataclasses import dataclass
4+
from typing import TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
from variantlib.protocols import VariantPropertyType
28

39

410
@dataclass
@@ -8,16 +14,19 @@ class FeatConfig:
814

915

1016
namespace = "installable_plugin"
17+
dynamic = False
1118

1219

13-
def get_all_configs() -> list[FeatConfig]:
20+
def get_all_configs(known_properties: frozenset[VariantPropertyType] | None) -> list[FeatConfig]:
21+
assert known_properties is None
1422
return [
1523
FeatConfig("feat1", ["val1a", "val1b", "val1c"]),
1624
FeatConfig("feat2", ["val2a", "val2b"]),
1725
]
1826

1927

20-
def get_supported_configs() -> list[FeatConfig]:
28+
def get_supported_configs(known_properties: frozenset[VariantPropertyType] | None) -> list[FeatConfig]:
29+
assert known_properties is None
2130
return [
2231
FeatConfig("feat1", ["val1c", "val1b"]),
2332
]

tests/mocked_plugin_as_module.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,22 @@
55

66
if TYPE_CHECKING:
77
from variantlib.protocols import VariantFeatureConfigType
8+
from variantlib.protocols import VariantPropertyType
89

910

1011
namespace = "module_namespace"
12+
dynamic = False
1113

1214

13-
def get_all_configs() -> list[VariantFeatureConfigType]:
15+
def get_all_configs(
16+
known_properties: frozenset[VariantPropertyType] | None,
17+
) -> list[VariantFeatureConfigType]:
18+
assert known_properties is None
1419
return [Namespace(name="feature", values=["a", "b"])]
1520

1621

17-
def get_supported_configs() -> list[VariantFeatureConfigType]:
22+
def get_supported_configs(
23+
known_properties: frozenset[VariantPropertyType] | None,
24+
) -> list[VariantFeatureConfigType]:
25+
assert known_properties is None
1826
return []

tests/mocked_plugins.py

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,28 @@ class MockedEntryPoint:
1818

1919
class MockedPluginA(PluginType):
2020
namespace = "test_namespace"
21+
dynamic = False
2122

22-
def get_all_configs(self) -> list[VariantFeatureConfigType]:
23+
def get_all_configs(
24+
self, known_properties: frozenset[VariantPropertyType] | None
25+
) -> list[VariantFeatureConfigType]:
26+
assert known_properties is None
2327
return [
2428
VariantFeatureConfig("name1", ["val1a", "val1b", "val1c", "val1d"]),
2529
VariantFeatureConfig("name2", ["val2a", "val2b", "val2c"]),
2630
]
2731

28-
def get_supported_configs(self) -> list[VariantFeatureConfigType]:
32+
def get_supported_configs(
33+
self, known_properties: frozenset[VariantPropertyType] | None
34+
) -> list[VariantFeatureConfigType]:
35+
assert known_properties is None
2936
return [
3037
VariantFeatureConfig("name1", ["val1a", "val1b"]),
3138
VariantFeatureConfig("name2", ["val2a", "val2b", "val2c"]),
3239
]
3340

3441
def get_build_setup(
35-
self, properties: list[VariantPropertyType]
42+
self, properties: frozenset[VariantPropertyType]
3643
) -> dict[str, list[str]]:
3744
for prop in properties:
3845
assert prop.namespace == self.namespace
@@ -52,15 +59,36 @@ def get_build_setup(
5259
# to test that we don't rely on that inheritance
5360
class MockedPluginB:
5461
namespace = "second_namespace"
55-
56-
def get_all_configs(self) -> list[MyVariantFeatureConfig]:
62+
dynamic = True
63+
64+
def get_all_configs(
65+
self, known_properties: frozenset[VariantPropertyType] | None
66+
) -> list[MyVariantFeatureConfig]:
67+
assert known_properties is not None
68+
assert all(prop.namespace == self.namespace for prop in known_properties)
69+
vals3 = ["val3a", "val3b", "val3c"]
70+
vals3.extend(
71+
x.value
72+
for x in known_properties
73+
if x.feature == "name3" and x.value not in vals3
74+
)
5775
return [
58-
MyVariantFeatureConfig("name3", ["val3a", "val3b", "val3c"]),
76+
MyVariantFeatureConfig("name3", vals3),
5977
]
6078

61-
def get_supported_configs(self) -> list[MyVariantFeatureConfig]:
79+
def get_supported_configs(
80+
self, known_properties: frozenset[VariantPropertyType] | None
81+
) -> list[MyVariantFeatureConfig]:
82+
assert known_properties is not None
83+
assert all(prop.namespace == self.namespace for prop in known_properties)
84+
vals3 = ["val3a"]
85+
vals3.extend(
86+
x.value
87+
for x in known_properties
88+
if x.feature == "name3" and x.value not in vals3
89+
)
6290
return [
63-
MyVariantFeatureConfig("name3", ["val3a"]),
91+
MyVariantFeatureConfig("name3", vals3),
6492
]
6593

6694

@@ -75,20 +103,27 @@ def __init__(self, name: str) -> None:
75103

76104
class MockedPluginC(PluginType):
77105
namespace = "incompatible_namespace"
106+
dynamic = False
78107

79-
def get_all_configs(self) -> list[VariantFeatureConfigType]:
108+
def get_all_configs(
109+
self, known_properties: frozenset[VariantPropertyType] | None
110+
) -> list[VariantFeatureConfigType]:
111+
assert known_properties is None
80112
return [
81113
MyFlag("flag1"),
82114
MyFlag("flag2"),
83115
MyFlag("flag3"),
84116
MyFlag("flag4"),
85117
]
86118

87-
def get_supported_configs(self) -> list[VariantFeatureConfigType]:
119+
def get_supported_configs(
120+
self, known_properties: frozenset[VariantPropertyType] | None
121+
) -> list[VariantFeatureConfigType]:
122+
assert known_properties is None
88123
return []
89124

90125
def get_build_setup(
91-
self, properties: list[VariantPropertyType]
126+
self, properties: frozenset[VariantPropertyType]
92127
) -> dict[str, list[str]]:
93128
flag_opts = []
94129

tests/models/test_provider.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def test_failing_regex_name() -> None:
116116
with pytest.raises(ValidationError, match="must match regex"):
117117
_ = VariantFeatureConfig(name="", values=["7", "4", "8", "12"])
118118

119-
for c in "@#$%&*^()[]?.!-{}[]\\/ ":
119+
for c in "@#$%&*^()[]?.!-{}[]\\/ \t":
120120
with pytest.raises(ValidationError, match="must match regex"):
121121
_ = VariantFeatureConfig(name=f"name{c}value", values=["7", "4", "8", "12"])
122122

@@ -125,7 +125,7 @@ def test_failing_regex_value() -> None:
125125
with pytest.raises(ValidationError, match="must match regex"):
126126
_ = VariantFeatureConfig(name="name", values=[""])
127127

128-
for c in "@#$%&*^()[]?!-{}[]\\/ ":
128+
for c in "@#$%&*^()[]?-{}[]\\/ \t":
129129
with pytest.raises(ValidationError, match="must match regex"):
130130
_ = VariantFeatureConfig(name="name", values=[f"val{c}ue"])
131131

tests/plugins/test_loader.py

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55
from functools import partial
66
from pathlib import Path
7+
from typing import TYPE_CHECKING
78
from typing import Callable
89

910
import pytest
@@ -33,6 +34,9 @@
3334
from variantlib.pyproject_toml import VariantPyProjectToml
3435
from variantlib.variants_json import VariantsJson
3536

37+
if TYPE_CHECKING:
38+
from variantlib.protocols import VariantPropertyType
39+
3640
if sys.version_info >= (3, 11):
3741
import tomllib
3842
else:
@@ -44,25 +48,35 @@
4448

4549
class ClashingPlugin(PluginType):
4650
namespace = "test_namespace"
51+
dynamic = False
4752

48-
def get_all_configs(self) -> list[VariantFeatureConfigType]:
53+
def get_all_configs(
54+
self, known_properties: frozenset[VariantPropertyType] | None
55+
) -> list[VariantFeatureConfigType]:
4956
return [
5057
VariantFeatureConfig("name1", ["val1a", "val1b", "val1c", "val1d"]),
5158
]
5259

53-
def get_supported_configs(self) -> list[VariantFeatureConfigType]:
60+
def get_supported_configs(
61+
self, known_properties: frozenset[VariantPropertyType] | None
62+
) -> list[VariantFeatureConfigType]:
5463
return []
5564

5665

5766
class ExceptionPluginBase(PluginType):
5867
namespace = "exception_test"
68+
dynamic = False
5969

6070
returned_value: list[VariantFeatureConfigType]
6171

62-
def get_all_configs(self) -> list[VariantFeatureConfigType]:
72+
def get_all_configs(
73+
self, known_properties: frozenset[VariantPropertyType] | None
74+
) -> list[VariantFeatureConfigType]:
6375
return self.returned_value
6476

65-
def get_supported_configs(self) -> list[VariantFeatureConfigType]:
77+
def get_supported_configs(
78+
self, known_properties: frozenset[VariantPropertyType] | None
79+
) -> list[VariantFeatureConfigType]:
6680
return self.returned_value
6781

6882

@@ -115,6 +129,65 @@ def test_get_supported_configs(
115129
}
116130

117131

132+
def test_get_all_configs_dynamic(
133+
mocked_plugin_loader: BasePluginLoader,
134+
) -> None:
135+
assert mocked_plugin_loader.get_all_configs(
136+
[
137+
VariantProperty("test_namespace", "name1", "val1z"),
138+
VariantProperty("second_namespace", "name3", "val3bcde"),
139+
]
140+
) == {
141+
"incompatible_namespace": ProviderConfig(
142+
namespace="incompatible_namespace",
143+
configs=[
144+
VariantFeatureConfig("flag1", ["on"]),
145+
VariantFeatureConfig("flag2", ["on"]),
146+
VariantFeatureConfig("flag3", ["on"]),
147+
VariantFeatureConfig("flag4", ["on"]),
148+
],
149+
),
150+
"second_namespace": ProviderConfig(
151+
namespace="second_namespace",
152+
configs=[
153+
VariantFeatureConfig("name3", ["val3a", "val3b", "val3c", "val3bcde"]),
154+
],
155+
),
156+
"test_namespace": ProviderConfig(
157+
namespace="test_namespace",
158+
configs=[
159+
VariantFeatureConfig("name1", ["val1a", "val1b", "val1c", "val1d"]),
160+
VariantFeatureConfig("name2", ["val2a", "val2b", "val2c"]),
161+
],
162+
),
163+
}
164+
165+
166+
def test_get_supported_configs_dynamic(
167+
mocked_plugin_loader: BasePluginLoader,
168+
) -> None:
169+
assert mocked_plugin_loader.get_supported_configs(
170+
[
171+
VariantProperty("test_namespace", "name1", "val1z"),
172+
VariantProperty("second_namespace", "name3", "val3abcd"),
173+
]
174+
) == {
175+
"second_namespace": ProviderConfig(
176+
namespace="second_namespace",
177+
configs=[
178+
VariantFeatureConfig("name3", ["val3a", "val3abcd"]),
179+
],
180+
),
181+
"test_namespace": ProviderConfig(
182+
namespace="test_namespace",
183+
configs=[
184+
VariantFeatureConfig("name1", ["val1a", "val1b"]),
185+
VariantFeatureConfig("name2", ["val2a", "val2b", "val2c"]),
186+
],
187+
),
188+
}
189+
190+
118191
def test_namespace_clash() -> None:
119192
with (
120193
pytest.raises(
@@ -227,8 +300,11 @@ def test_namespace_incorrect_name() -> None:
227300

228301
class IncompletePlugin:
229302
namespace = "incomplete_plugin"
303+
dynamic = False
230304

231-
def get_supported_configs(self) -> list[VariantFeatureConfigType]:
305+
def get_supported_configs(
306+
self, known_properties: frozenset[VariantPropertyType] | None
307+
) -> list[VariantFeatureConfigType]:
232308
return []
233309

234310

@@ -237,8 +313,8 @@ def test_namespace_incorrect_type() -> None:
237313
pytest.raises(
238314
PluginError,
239315
match=r"'tests.plugins.test_loader:RANDOM_STUFF' does not meet "
240-
r"the PluginType prototype: 123 \(missing attributes: get_all_configs, "
241-
r"get_supported_configs, namespace\)",
316+
r"the PluginType prototype: 123 \(missing attributes: dynamic, "
317+
r"get_all_configs, get_supported_configs, namespace\)",
242318
),
243319
ListPluginLoader(["tests.plugins.test_loader:RANDOM_STUFF"]),
244320
):
@@ -251,10 +327,14 @@ class RaisingInstantiationPlugin:
251327
def __init__(self) -> None:
252328
raise RuntimeError("I failed to initialize")
253329

254-
def get_all_configs(self) -> list[VariantFeatureConfigType]:
330+
def get_all_configs(
331+
self, known_properties: frozenset[VariantPropertyType]
332+
) -> list[VariantFeatureConfigType]:
255333
return []
256334

257-
def get_supported_configs(self) -> list[VariantFeatureConfigType]:
335+
def get_supported_configs(
336+
self, known_properties: frozenset[VariantPropertyType]
337+
) -> list[VariantFeatureConfigType]:
258338
return []
259339

260340

@@ -273,14 +353,19 @@ def test_namespace_instantiation_raises() -> None:
273353

274354
class CrossTypeInstantiationPlugin:
275355
namespace = "cross_plugin"
356+
dynamic = False
276357

277358
def __new__(cls) -> IncompletePlugin: # type: ignore[misc]
278359
return IncompletePlugin()
279360

280-
def get_all_configs(self) -> list[VariantFeatureConfigType]:
361+
def get_all_configs(
362+
self, known_properties: frozenset[VariantPropertyType] | None
363+
) -> list[VariantFeatureConfigType]:
281364
return []
282365

283-
def get_supported_configs(self) -> list[VariantFeatureConfigType]:
366+
def get_supported_configs(
367+
self, known_properties: frozenset[VariantPropertyType] | None
368+
) -> list[VariantFeatureConfigType]:
284369
return []
285370

286371

@@ -314,9 +399,13 @@ def test_get_build_setup(
314399
]
315400
)
316401

317-
assert mocked_plugin_loader.get_build_setup(variant_desc) == {
318-
"cflags": ["-mflag1", "-mflag4", "-march=val1b"],
319-
"cxxflags": ["-mflag1", "-mflag4", "-march=val1b"],
402+
# flag order may depend on (random) property ordering
403+
assert {
404+
k: sorted(v)
405+
for k, v in mocked_plugin_loader.get_build_setup(variant_desc).items()
406+
} == {
407+
"cflags": ["-march=val1b", "-mflag1", "-mflag4"],
408+
"cxxflags": ["-march=val1b", "-mflag1", "-mflag4"],
320409
"ldflags": ["-Wl,--test-flag"],
321410
}
322411

0 commit comments

Comments
 (0)