From aa4a9b56a38e0a16bca792ad9112e0d6555a35ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 22 Apr 2025 17:02:50 +0200 Subject: [PATCH 1/3] Use `re.fullmatch()` instead of embedding `^$` Use `re.fullmatch()` (and the equivalents) to match the whole string against the regular expression, rather than explicit `^` and `$`. This has the same result, but at the same time makes the regular expression patterns reusable without having to strip these symbols. --- tests/models/test_configuration.py | 18 +++++++++--------- variantlib/commands/analyze_wheel.py | 2 +- variantlib/commands/make_variant.py | 2 +- variantlib/constants.py | 11 +++++------ variantlib/models/variant.py | 18 +++++++++--------- variantlib/validators.py | 10 +++++----- 6 files changed, 30 insertions(+), 31 deletions(-) diff --git a/tests/models/test_configuration.py b/tests/models/test_configuration.py index 39a91ce..66353ce 100644 --- a/tests/models/test_configuration.py +++ b/tests/models/test_configuration.py @@ -51,19 +51,19 @@ def test_from_toml_config(config_params: dict[str, list[str]]): ) -@given(st.lists(st.from_regex(VALIDATION_NAMESPACE_REGEX))) +@given(st.lists(st.from_regex(VALIDATION_NAMESPACE_REGEX, fullmatch=True))) def test_namespace_priorities_validation(namespaces: list[str]): config = VariantConfiguration(namespace_priorities=namespaces) assert config.namespace_priorities == namespaces @given( - st.lists(st.from_regex(VALIDATION_NAMESPACE_REGEX)), + st.lists(st.from_regex(VALIDATION_NAMESPACE_REGEX, fullmatch=True)), st.lists( st.builds( VariantFeature, namespace=st.just("OmniCorp"), - feature=st.from_regex(VALIDATION_FEATURE_REGEX), + feature=st.from_regex(VALIDATION_FEATURE_REGEX, fullmatch=True), ) ), ) @@ -77,20 +77,20 @@ def test_feature_priorities_validation( @given( - st.lists(st.from_regex(VALIDATION_NAMESPACE_REGEX)), + st.lists(st.from_regex(VALIDATION_NAMESPACE_REGEX, fullmatch=True)), st.lists( st.builds( VariantFeature, - namespace=st.from_regex(VALIDATION_NAMESPACE_REGEX), - feature=st.from_regex(VALIDATION_FEATURE_REGEX), + namespace=st.from_regex(VALIDATION_NAMESPACE_REGEX, fullmatch=True), + feature=st.from_regex(VALIDATION_FEATURE_REGEX, fullmatch=True), ) ), st.lists( st.builds( VariantProperty, - namespace=st.from_regex(VALIDATION_NAMESPACE_REGEX), - feature=st.from_regex(VALIDATION_FEATURE_REGEX), - value=st.from_regex(VALIDATION_VALUE_REGEX), + namespace=st.from_regex(VALIDATION_NAMESPACE_REGEX, fullmatch=True), + feature=st.from_regex(VALIDATION_FEATURE_REGEX, fullmatch=True), + value=st.from_regex(VALIDATION_VALUE_REGEX, fullmatch=True), ) ), ) diff --git a/variantlib/commands/analyze_wheel.py b/variantlib/commands/analyze_wheel.py index f2d7a5d..ef3dac4 100644 --- a/variantlib/commands/analyze_wheel.py +++ b/variantlib/commands/analyze_wheel.py @@ -37,7 +37,7 @@ def analyze_wheel(args: list[str]) -> None: raise TypeError(f"File must have a `.whl` extension: `{input_file.name}`") # Checking if the wheel file is a valid wheel file - if (wheel_info := WHEEL_NAME_VALIDATION_REGEX.match(input_file.name)) is None: + if (wheel_info := WHEEL_NAME_VALIDATION_REGEX.fullmatch(input_file.name)) is None: raise TypeError( f"The file is not a valid python wheel filename: `{input_file.name}`" ) diff --git a/variantlib/commands/make_variant.py b/variantlib/commands/make_variant.py index bf5d59f..a622988 100644 --- a/variantlib/commands/make_variant.py +++ b/variantlib/commands/make_variant.py @@ -79,7 +79,7 @@ def make_variant(args: list[str]) -> None: ) # Input Validation - Wheel Filename is valid and non variant already. - wheel_info = WHEEL_NAME_VALIDATION_REGEX.match(input_filepath.name) + wheel_info = WHEEL_NAME_VALIDATION_REGEX.fullmatch(input_filepath.name) if wheel_info is None: raise ValueError(f"{input_filepath.name!r} is not a valid wheel filename.") diff --git a/variantlib/constants.py b/variantlib/constants.py index 0cebf8f..b68d034 100644 --- a/variantlib/constants.py +++ b/variantlib/constants.py @@ -8,17 +8,16 @@ VARIANTS_JSON_PROVIDER_DATA_KEY = "providers" VARIANTS_JSON_VARIANT_DATA_KEY = "variants" -VALIDATION_VARIANT_HASH_REGEX = re.compile(rf"^[0-9a-f]{{{VARIANT_HASH_LEN}}}$") -VALIDATION_NAMESPACE_REGEX = re.compile(r"^[A-Za-z0-9_]+$") -VALIDATION_FEATURE_REGEX = re.compile(r"^[A-Za-z0-9_]+$") -VALIDATION_VALUE_REGEX = re.compile(r"^[A-Za-z0-9_.]+$") +VALIDATION_VARIANT_HASH_REGEX = re.compile(rf"[0-9a-f]{{{VARIANT_HASH_LEN}}}") +VALIDATION_NAMESPACE_REGEX = re.compile(r"[A-Za-z0-9_]+") +VALIDATION_FEATURE_REGEX = re.compile(r"[A-Za-z0-9_]+") +VALIDATION_VALUE_REGEX = re.compile(r"[A-Za-z0-9_.]+") METADATA_VARIANT_HASH_HEADER = "Variant-hash" METADATA_VARIANT_PROPERTY_HEADER = "Variant" METADATA_VARIANT_PROVIDER_HEADER = "Variant-provider" WHEEL_NAME_VALIDATION_REGEX = re.compile( - r"^ " r"(?P " # group (without variant) r" (?P " # "namever" group contains - r" (?P[^\s-]+?) " # @@ -34,6 +33,6 @@ r" ) " r")? " r"\.whl " # ".whl" suffix - r"$ ", + r" ", re.VERBOSE, ) diff --git a/variantlib/models/variant.py b/variantlib/models/variant.py index a5cf934..f1634af 100644 --- a/variantlib/models/variant.py +++ b/variantlib/models/variant.py @@ -76,15 +76,15 @@ def deserialize(cls, data: dict[str, str]) -> Self: @classmethod def from_str(cls, input_str: str) -> Self: # removing starting `^` and trailing `$` - pttn_nmspc = VALIDATION_NAMESPACE_REGEX.pattern[1:-1] - pttn_feature = VALIDATION_FEATURE_REGEX.pattern[1:-1] + pttn_nmspc = VALIDATION_NAMESPACE_REGEX.pattern + pttn_feature = VALIDATION_FEATURE_REGEX.pattern pattern = re.compile( - rf"^(?P{pttn_nmspc})\s*::\s*(?P{pttn_feature})$" + rf"(?P{pttn_nmspc})\s*::\s*(?P{pttn_feature})" ) # Try matching the input string with the regex pattern - match = pattern.match(input_str.strip()) + match = pattern.fullmatch(input_str.strip()) if match is None: raise ValidationError( @@ -130,16 +130,16 @@ def to_str(self) -> str: @classmethod def from_str(cls, input_str: str) -> Self: # removing starting `^` and trailing `$` - pttn_nmspc = VALIDATION_NAMESPACE_REGEX.pattern[1:-1] - pttn_feature = VALIDATION_FEATURE_REGEX.pattern[1:-1] - pttn_value = VALIDATION_VALUE_REGEX.pattern[1:-1] + pttn_nmspc = VALIDATION_NAMESPACE_REGEX.pattern + pttn_feature = VALIDATION_FEATURE_REGEX.pattern + pttn_value = VALIDATION_VALUE_REGEX.pattern pattern = re.compile( - rf"^(?P{pttn_nmspc})\s*::\s*(?P{pttn_feature})\s*::\s*(?P{pttn_value})$" + rf"(?P{pttn_nmspc})\s*::\s*(?P{pttn_feature})\s*::\s*(?P{pttn_value})" ) # Try matching the input string with the regex pattern - match = pattern.match(input_str.strip()) + match = pattern.fullmatch(input_str.strip()) if match is None: raise ValidationError( diff --git a/variantlib/validators.py b/variantlib/validators.py index c4bfc03..5a682b1 100644 --- a/variantlib/validators.py +++ b/variantlib/validators.py @@ -22,7 +22,7 @@ def validate_matches_re(value: str, pattern: str | re.Pattern) -> None: - if not re.match(pattern, value): + if not re.fullmatch(pattern, value): raise ValidationError(f"Value `{value}` must match regex {pattern}") @@ -184,7 +184,7 @@ def validate_variants_json(data: dict) -> None: raise ValidationError( f"Invalid `variant_hash` type: `{variant_hash}` in data (expected str)" ) - if VALIDATION_VARIANT_HASH_REGEX.match(variant_hash) is None: + if VALIDATION_VARIANT_HASH_REGEX.fullmatch(variant_hash) is None: raise ValidationError(f"Invalid variant hash `{variant_hash}` in data") # Check the Variant Data @@ -204,7 +204,7 @@ def validate_variants_json(data: dict) -> None: raise ValidationError( f"Invalid variant namespace `{namespace}` for hash `{variant_hash}`" ) - if VALIDATION_NAMESPACE_REGEX.match(namespace) is None: + if VALIDATION_NAMESPACE_REGEX.fullmatch(namespace) is None: raise ValidationError( f"Invalid variant namespace `{namespace}` for hash `{variant_hash}`" ) @@ -229,7 +229,7 @@ def validate_variants_json(data: dict) -> None: f"Invalid variant feature name `{feature_name}` for hash " f"`{variant_hash}`" ) - if VALIDATION_FEATURE_REGEX.match(feature_name) is None: + if VALIDATION_FEATURE_REGEX.fullmatch(feature_name) is None: raise ValidationError( f"Invalid variant feature name `{feature_name}` for hash " f"`{variant_hash}`" @@ -242,7 +242,7 @@ def validate_variants_json(data: dict) -> None: f"`{variant_hash}` - Type received: `{type(feature_value)}`, " "expected: `str`." ) - if VALIDATION_VALUE_REGEX.match(feature_value) is None: + if VALIDATION_VALUE_REGEX.fullmatch(feature_value) is None: raise ValidationError( f"Invalid variant value `{feature_value}` for hash " f"`{variant_hash}`" From 7691a8934592bbae712acde878ac2f96ac104cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 22 Apr 2025 17:14:02 +0200 Subject: [PATCH 2/3] Use hypothesis `from_regex()` strategy in API tests too --- tests/test_api.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 19b86a9..0d71152 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -22,6 +22,9 @@ from variantlib.api import get_variant_hashes_by_priority from variantlib.api import set_variant_metadata from variantlib.api import validate_variant +from variantlib.constants import VALIDATION_FEATURE_REGEX +from variantlib.constants import VALIDATION_NAMESPACE_REGEX +from variantlib.constants import VALIDATION_VALUE_REGEX from variantlib.constants import VARIANTS_JSON_VARIANT_DATA_KEY from variantlib.loader import PluginLoader from variantlib.models import provider as pconfig @@ -95,29 +98,19 @@ def test_get_variant_hashes_by_priority_roundtrip(mocker, configs): unique_by=lambda provider_cfg: provider_cfg.namespace, elements=st.builds( ProviderConfig, - namespace=st.text( - string.ascii_letters + string.digits + "_", min_size=1, max_size=64 - ), + namespace=st.from_regex(VALIDATION_NAMESPACE_REGEX, fullmatch=True), configs=st.lists( min_size=1, max_size=2, unique_by=lambda vfeat_cfg: vfeat_cfg.name, elements=st.builds( VariantFeatureConfig, - name=st.text( - alphabet=string.ascii_letters + string.digits + "_", - min_size=1, - max_size=64, - ), + name=st.from_regex(VALIDATION_FEATURE_REGEX, fullmatch=True), values=st.lists( min_size=1, max_size=3, unique=True, - elements=st.text( - alphabet=string.ascii_letters + string.digits + "_.", - min_size=1, - max_size=64, - ), + elements=st.from_regex(VALIDATION_VALUE_REGEX, fullmatch=True), ), ), ), From 97f46b47852dce5cea3eab7b3560b1eb088b178b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 22 Apr 2025 17:21:24 +0200 Subject: [PATCH 3/3] Disable deadline for fuzzing test, since it troubles PyPy --- tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index 0d71152..9cfe5f7 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -73,7 +73,7 @@ def test_get_variant_hashes_by_priority_roundtrip(mocker, configs): ] -@settings(deadline=1000, suppress_health_check=[HealthCheck.function_scoped_fixture]) +@settings(deadline=None, suppress_health_check=[HealthCheck.function_scoped_fixture]) @example( [ ProviderConfig(