Skip to content

[Plugin] Dynamic Plugin API #105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions tests/mocked_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from dataclasses import dataclass

from variantlib.models.provider import VariantFeatureConfig
from variantlib.protocols import PluginType
from variantlib.protocols import PluginStaticType
from variantlib.protocols import VariantFeatureConfigType
from variantlib.protocols import VariantPropertyType

Expand All @@ -16,8 +16,8 @@ class MockedEntryPoint:
dist: None = None


class MockedPluginA(PluginType):
namespace = "test_namespace"
class MockedPluginA(PluginStaticType):
namespace = "test_namespace" # pyright: ignore[reportAssignmentType,reportIncompatibleMethodOverride]

def get_all_configs(self) -> list[VariantFeatureConfigType]:
return [
Expand Down Expand Up @@ -48,7 +48,7 @@ def get_build_setup(
MyVariantFeatureConfig = namedtuple("MyVariantFeatureConfig", ("name", "values"))


# NB: this plugin deliberately does not inherit from PluginType
# NB: this plugin deliberately does not inherit from PluginStaticType
# to test that we don't rely on that inheritance
class MockedPluginB:
namespace = "second_namespace"
Expand All @@ -73,7 +73,7 @@ def __init__(self, name: str) -> None:
self.values = ["on"]


class MockedPluginC(PluginType):
class MockedPluginC(PluginStaticType):
namespace = "incompatible_namespace"

def get_all_configs(self) -> list[VariantFeatureConfigType]:
Expand Down
4 changes: 2 additions & 2 deletions tests/models/test_variant.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from variantlib.constants import VALIDATION_FEATURE_NAME_REGEX
from variantlib.constants import VALIDATION_NAMESPACE_REGEX
from variantlib.constants import VALIDATION_VALUE_STR_REGEX
from variantlib.constants import VALIDATION_VALUE_REGEX
from variantlib.constants import VARIANT_HASH_LEN
from variantlib.errors import ValidationError
from variantlib.models.variant import VariantDescription
Expand Down Expand Up @@ -112,7 +112,7 @@ def test_failing_regex_value() -> None:
_ = VariantProperty(namespace="provider", feature="feature", value="")

for c in string.printable:
if VALIDATION_VALUE_STR_REGEX.fullmatch(c):
if VALIDATION_VALUE_REGEX.fullmatch(c):
continue

with pytest.raises(ValidationError, match="must match regex"):
Expand Down
16 changes: 8 additions & 8 deletions tests/plugins/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from variantlib.plugins.loader import EntryPointPluginLoader
from variantlib.plugins.loader import ListPluginLoader
from variantlib.plugins.loader import PluginLoader
from variantlib.protocols import PluginType
from variantlib.protocols import PluginStaticType
from variantlib.protocols import VariantFeatureConfigType
from variantlib.protocols import VariantNamespace
from variantlib.pyproject_toml import VariantPyProjectToml
Expand All @@ -41,8 +41,8 @@
RANDOM_STUFF = 123


class ClashingPlugin(PluginType):
namespace = "test_namespace"
class ClashingPlugin(PluginStaticType):
namespace = "test_namespace" # pyright: ignore[reportAssignmentType,reportIncompatibleMethodOverride]

def get_all_configs(self) -> list[VariantFeatureConfigType]:
return [
Expand All @@ -53,8 +53,8 @@ def get_supported_configs(self) -> list[VariantFeatureConfigType]:
return []


class ExceptionPluginBase(PluginType):
namespace = "exception_test"
class ExceptionPluginBase(PluginStaticType):
namespace = "exception_test" # pyright: ignore[reportAssignmentType,reportIncompatibleMethodOverride]

returned_value: list[VariantFeatureConfigType]

Expand Down Expand Up @@ -236,8 +236,8 @@ def test_namespace_incorrect_type() -> None:
pytest.raises(
PluginError,
match=r"'tests.plugins.test_loader:RANDOM_STUFF' does not meet "
r"the PluginType prototype: 123 \(missing attributes: get_all_configs, "
r"get_supported_configs, namespace\)",
r"the PluginStaticType prototype: 123 \(missing attributes: "
r"get_all_configs, get_supported_configs, namespace\)",
),
ListPluginLoader(["tests.plugins.test_loader:RANDOM_STUFF"]),
):
Expand Down Expand Up @@ -291,7 +291,7 @@ def test_namespace_instantiation_returns_incorrect_type(
pytest.raises(
PluginError,
match=re.escape(
f"'tests.plugins.test_loader:{cls}' does not meet the PluginType "
f"'tests.plugins.test_loader:{cls}' does not meet the PluginStaticType "
"prototype: <tests.plugins.test_loader.IncompletePlugin object at"
)
+ r".*(missing attributes: get_all_configs)",
Expand Down
6 changes: 2 additions & 4 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from variantlib.constants import PYPROJECT_TOML_TOP_KEY
from variantlib.constants import VALIDATION_FEATURE_NAME_REGEX
from variantlib.constants import VALIDATION_NAMESPACE_REGEX
from variantlib.constants import VALIDATION_VALUE_STR_REGEX
from variantlib.constants import VALIDATION_VALUE_REGEX
from variantlib.constants import VARIANT_INFO_DEFAULT_PRIO_KEY
from variantlib.constants import VARIANT_INFO_FEATURE_KEY
from variantlib.constants import VARIANT_INFO_NAMESPACE_KEY
Expand Down Expand Up @@ -167,9 +167,7 @@ def test_get_variant_hashes_by_priority_roundtrip(
min_size=1,
max_size=3,
unique=True,
elements=st.from_regex(
VALIDATION_VALUE_STR_REGEX, fullmatch=True
),
elements=st.from_regex(VALIDATION_VALUE_REGEX, fullmatch=True),
),
),
),
Expand Down
11 changes: 10 additions & 1 deletion variantlib/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,19 @@ def get_variant_hashes_by_priority(
venv_path=venv_path,
enable_optional_plugins=enable_optional_plugins,
) as plugin_loader:
known_properties = list(
{
vprop
for vdesc in variants_json.variants.values()
for vprop in vdesc.properties
}
)
supported_vprops = list(
itertools.chain.from_iterable(
provider_cfg.to_list_of_properties()
for provider_cfg in plugin_loader.get_supported_configs().values()
for provider_cfg in plugin_loader.get_supported_configs(
variant_properties=known_properties
).values()
)
)

Expand Down
22 changes: 11 additions & 11 deletions variantlib/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,7 @@

VALIDATION_NAMESPACE_REGEX = re.compile(r"[a-z0-9_]+")
VALIDATION_FEATURE_NAME_REGEX = re.compile(r"[a-z0-9_]+")

# For `Property value` there is two regexes:
# 1. `VALIDATION_VALUE_VSPEC_REGEX` - if `packaging.specifiers.SpecifierSet` is used
# Note: for clarity - only "full version" are allowed
# i.e. so no "a|b|alpha|beta|rc|post|etc." versions
VALIDATION_VALUE_VSPEC_REGEX = re.compile(r"[0-9_.,!>~<=]+")
# 2. `VALIDATION_VALUE_STR_REGEX` - if string matching is used
VALIDATION_VALUE_STR_REGEX = re.compile(r"[a-z0-9_.]+")
VALIDATION_VALUE_REGEX = re.compile(
rf"{VALIDATION_VALUE_VSPEC_REGEX.pattern}|{VALIDATION_VALUE_STR_REGEX.pattern}"
)
VALIDATION_VALUE_REGEX = re.compile(r"[a-z0-9_.,!>~<=]+")

VALIDATION_FEATURE_REGEX = re.compile(
rf"""
Expand Down Expand Up @@ -73,6 +63,16 @@
)
VALIDATION_PROVIDER_REQUIRES_REGEX = re.compile(r"[\S ]+")

# This "magic value" will be use during `make-variant` and other commands
# when validating if a given `VariantProperty` is valid.
# Unless we want to also make the `validate_property(vprop)` part of the
# `PluginDynamicType` interface. We can just "not analyze" and allow everything.
# The magic value is used as a "*" value. If detected - allow any value that
# passes the regex.
#
# @mgorny: if you have a better idea - happy to hear it :)
VARIANTLIB_DYNAMIC_ANY_VALUE_MAGIC_VALUE = "__variant_property_any_value_magic__"


# VALIDATION_PYTHON_PACKAGE_NAME_REGEX = re.compile(r"[^\s-]+?")
# Per PEP 508: https://peps.python.org/pep-0508/#names
Expand Down
4 changes: 2 additions & 2 deletions variantlib/models/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from variantlib.constants import VALIDATION_FEATURE_NAME_REGEX
from variantlib.constants import VALIDATION_NAMESPACE_REGEX
from variantlib.constants import VALIDATION_VALUE_STR_REGEX
from variantlib.constants import VALIDATION_VALUE_REGEX
from variantlib.models.base import BaseModel
from variantlib.models.variant import VariantProperty
from variantlib.protocols import VariantFeatureName
Expand Down Expand Up @@ -49,7 +49,7 @@ class VariantFeatureConfig(BaseModel):
"validator": lambda val: validate_and(
[
lambda v: validate_type(v, list[VariantFeatureValue]),
lambda v: validate_list_matches_re(v, VALIDATION_VALUE_STR_REGEX),
lambda v: validate_list_matches_re(v, VALIDATION_VALUE_REGEX),
lambda v: validate_list_min_len(v, 1),
lambda v: validate_list_all_unique(v),
],
Expand Down
4 changes: 2 additions & 2 deletions variantlib/models/variant.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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 VARIANT_HASH_LEN
from variantlib.constants import VariantInfoJsonDict
from variantlib.errors import ValidationError
Expand All @@ -24,7 +25,6 @@
from variantlib.validators.base import validate_matches_re
from variantlib.validators.base import validate_type
from variantlib.validators.combining import validate_and
from variantlib.validators.vprop import validate_variant_property_value

if sys.version_info >= (3, 11):
from typing import Self
Expand Down Expand Up @@ -102,7 +102,7 @@ class VariantProperty(VariantFeature):
"validator": lambda val: validate_and(
[
lambda v: validate_type(v, VariantFeatureValue),
lambda v: validate_variant_property_value(v), # pyright: ignore[reportArgumentType]
lambda v: validate_matches_re(v, VALIDATION_VALUE_REGEX),
],
value=val,
)
Expand Down
1 change: 1 addition & 0 deletions variantlib/models/variant_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Any

from packaging.requirements import Requirement

from variantlib.constants import VALIDATION_FEATURE_NAME_REGEX
from variantlib.constants import VALIDATION_NAMESPACE_REGEX
from variantlib.constants import VALIDATION_PROVIDER_ENABLE_IF_REGEX
Expand Down
Loading
Loading