Skip to content

Commit 526132a

Browse files
authored
Optional providers, take two (#104)
* Simplify plugin loader to use `providers` * Add an `optional` key to provider information Add a boolean `optional` key to provider information that can be used to specify that a provider is optional and therefore should not be enabled by default. * Update test artifacts to test `optional = true` too * Add plugin loader parameters to enable optional plugins * Expose `enable_optional_plugins` in API * Use `VariantNamespace` type for `enable_optional_plugins` * Handle empty plugin lists gracefully * Support filtering loaded plugins * Make `validate_variant()` only load plugins it needs
1 parent 732bdf4 commit 526132a

17 files changed

+221
-54
lines changed
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

tests/artifacts/test-package/dist/test_package-0-variants.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@
22
"$schema": "https://variants-schema.wheelnext.dev/",
33
"default-priorities": {
44
"namespace": [
5-
"installable_plugin"
5+
"installable_plugin",
6+
"non_existing_plugin"
67
]
78
},
89
"providers": {
910
"installable_plugin": {
1011
"requires": [
1112
"test-plugin-package"
1213
]
14+
},
15+
"non_existing_plugin": {
16+
"optional": true,
17+
"requires": [
18+
"do-not-install-me"
19+
]
1320
}
1421
},
1522
"variants": {

tests/artifacts/test-package/pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ description = "Installable package for testing"
99
requires-python = ">=3.9"
1010

1111
[variant.default-priorities]
12-
namespace = ["installable_plugin"]
12+
namespace = ["installable_plugin", "non_existing_plugin"]
1313

1414
[variant.providers.installable_plugin]
1515
requires = ["test-plugin-package"]
16+
optional = false
17+
18+
[variant.providers.non_existing_plugin]
19+
requires = ["do-not-install-me"]
20+
optional = true

tests/commands/test_generate_index_json.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from variantlib.constants import VARIANT_INFO_DEFAULT_PRIO_KEY
99
from variantlib.constants import VARIANT_INFO_NAMESPACE_KEY
1010
from variantlib.constants import VARIANT_INFO_PROVIDER_DATA_KEY
11+
from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY
1112
from variantlib.constants import VARIANT_INFO_PROVIDER_REQUIRES_KEY
1213
from variantlib.constants import VARIANTS_JSON_SCHEMA_KEY
1314
from variantlib.constants import VARIANTS_JSON_SCHEMA_URL
@@ -32,12 +33,17 @@ def test_generate_index_json(
3233
VARIANT_INFO_DEFAULT_PRIO_KEY: {
3334
VARIANT_INFO_NAMESPACE_KEY: [
3435
"installable_plugin",
36+
"non_existing_plugin",
3537
]
3638
},
3739
VARIANT_INFO_PROVIDER_DATA_KEY: {
3840
"installable_plugin": {
3941
VARIANT_INFO_PROVIDER_REQUIRES_KEY: ["test-plugin-package"],
4042
},
43+
"non_existing_plugin": {
44+
VARIANT_INFO_PROVIDER_REQUIRES_KEY: ["do-not-install-me"],
45+
VARIANT_INFO_PROVIDER_OPTIONAL_KEY: True,
46+
},
4147
},
4248
VARIANTS_JSON_VARIANT_DATA_KEY: {
4349
"00000000": {},

tests/plugins/test_loader.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import re
44
import sys
5+
from functools import partial
6+
from typing import Callable
57

68
import pytest
79

@@ -26,6 +28,7 @@
2628
from variantlib.plugins.loader import PluginLoader
2729
from variantlib.protocols import PluginType
2830
from variantlib.protocols import VariantFeatureConfigType
31+
from variantlib.protocols import VariantNamespace
2932
from variantlib.pyproject_toml import VariantPyProjectToml
3033
from variantlib.variants_json import VariantsJson
3134

@@ -389,6 +392,7 @@ def test_load_plugin_invalid_arg() -> None:
389392
"test_namespace",
390393
"second_namespace",
391394
"incompatible_namespace",
395+
"one_more",
392396
],
393397
providers={
394398
"test_namespace": ProviderInfo(
@@ -404,6 +408,10 @@ def test_load_plugin_invalid_arg() -> None:
404408
enable_if='platform_machine == "frobnicator"',
405409
plugin_api="tests.mocked_plugins:MockedPluginC",
406410
),
411+
"one_more": ProviderInfo(
412+
plugin_api="tests.mocked_plugins:NoSuchClass",
413+
optional=True,
414+
),
407415
},
408416
),
409417
VariantPyProjectToml(
@@ -477,3 +485,91 @@ def test_no_plugin_api(test_plugin_package_req: str) -> None:
477485

478486
with PluginLoader(variant_info, use_auto_install=True, isolated=True) as loader:
479487
assert set(loader.namespaces) == {"installable_plugin"}
488+
489+
490+
@pytest.mark.parametrize(
491+
("value", "expected"),
492+
[
493+
(False, False),
494+
(True, True),
495+
([], False),
496+
(["test_namespace"], False),
497+
(["second_namespace"], True),
498+
(["second_namespace", "test_namespace"], True),
499+
(["frobnicate"], False),
500+
(["frobnicate", "second_namespace"], True),
501+
],
502+
)
503+
def test_optional_plugins(value: bool | list[VariantNamespace], expected: bool) -> None:
504+
variant_info = VariantInfo(
505+
namespace_priorities=[
506+
"test_namespace",
507+
"second_namespace",
508+
],
509+
providers={
510+
"test_namespace": ProviderInfo(
511+
plugin_api="tests.mocked_plugins:MockedPluginA"
512+
),
513+
"second_namespace": ProviderInfo(
514+
plugin_api="tests.mocked_plugins:MockedPluginB", optional=True
515+
),
516+
},
517+
)
518+
519+
expected_namespaces = {"test_namespace"}
520+
if expected:
521+
expected_namespaces.add("second_namespace")
522+
with PluginLoader(
523+
variant_info, use_auto_install=False, enable_optional_plugins=value
524+
) as loader:
525+
assert set(loader.namespaces) == expected_namespaces
526+
527+
528+
@pytest.mark.parametrize(
529+
"loader_call",
530+
[
531+
partial(PluginLoader, VariantInfo(), use_auto_install=False),
532+
partial(ListPluginLoader, []),
533+
],
534+
)
535+
def test_empty_plugin_list(loader_call: Callable[[], BasePluginLoader]) -> None:
536+
with loader_call() as loader:
537+
assert loader.namespaces == []
538+
assert loader.get_supported_configs() == {}
539+
assert loader.get_all_configs() == {}
540+
assert loader.get_build_setup(VariantDescription([])) == {}
541+
542+
543+
@pytest.mark.parametrize(
544+
"value",
545+
[
546+
[],
547+
["test_namespace"],
548+
["second_namespace"],
549+
["second_namespace", "test_namespace"],
550+
["frobnicate"],
551+
["frobnicate", "second_namespace"],
552+
],
553+
)
554+
def test_filter_plugins(value: list[VariantNamespace]) -> None:
555+
variant_info = VariantInfo(
556+
namespace_priorities=[
557+
"test_namespace",
558+
"second_namespace",
559+
],
560+
providers={
561+
"test_namespace": ProviderInfo(
562+
plugin_api="tests.mocked_plugins:MockedPluginA"
563+
),
564+
"second_namespace": ProviderInfo(
565+
plugin_api="tests.mocked_plugins:MockedPluginB"
566+
),
567+
},
568+
)
569+
570+
expected_namespaces = set(value)
571+
expected_namespaces.discard("frobnicate")
572+
with PluginLoader(
573+
variant_info, use_auto_install=False, filter_plugins=value
574+
) as loader:
575+
assert set(loader.namespaces) == expected_namespaces

tests/test_api.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from variantlib.constants import VARIANT_INFO_PROPERTY_KEY
3838
from variantlib.constants import VARIANT_INFO_PROVIDER_DATA_KEY
3939
from variantlib.constants import VARIANT_INFO_PROVIDER_ENABLE_IF_KEY
40+
from variantlib.constants import VARIANT_INFO_PROVIDER_OPTIONAL_KEY
4041
from variantlib.constants import VARIANT_INFO_PROVIDER_PLUGIN_API_KEY
4142
from variantlib.constants import VARIANT_INFO_PROVIDER_REQUIRES_KEY
4243
from variantlib.constants import VARIANTS_JSON_SCHEMA_KEY
@@ -268,7 +269,8 @@ def test_validation_result_properties() -> None:
268269
]
269270

270271

271-
def test_validate_variant(mocked_plugin_apis: list[str]) -> None:
272+
@pytest.mark.parametrize("optional", [False, True])
273+
def test_validate_variant(mocked_plugin_apis: list[str], optional: bool) -> None:
272274
variant_info = VariantInfo(
273275
namespace_priorities=[
274276
"test_namespace",
@@ -277,13 +279,13 @@ def test_validate_variant(mocked_plugin_apis: list[str]) -> None:
277279
],
278280
providers={
279281
"test_namespace": ProviderInfo(
280-
plugin_api="tests.mocked_plugins:MockedPluginA"
282+
plugin_api="tests.mocked_plugins:MockedPluginA", optional=optional
281283
),
282284
"second_namespace": ProviderInfo(
283-
plugin_api="tests.mocked_plugins:MockedPluginB"
285+
plugin_api="tests.mocked_plugins:MockedPluginB", optional=optional
284286
),
285287
"incompatible_namespace": ProviderInfo(
286-
plugin_api="tests.mocked_plugins:MockedPluginC"
288+
plugin_api="tests.mocked_plugins:MockedPluginC", optional=optional
287289
),
288290
},
289291
)
@@ -360,6 +362,7 @@ def test_make_variant_dist_info(
360362
"old_ns2_provider; python_version < '3.11'",
361363
],
362364
VARIANT_INFO_PROVIDER_PLUGIN_API_KEY: "ns2_provider:Plugin",
365+
VARIANT_INFO_PROVIDER_OPTIONAL_KEY: True,
363366
},
364367
}
365368
)

0 commit comments

Comments
 (0)