fix(validators): validate regex patterns at init time
ContainsValidator and ExcludesValidator now pre-compile regex patterns during initialisation and raise InvalidThresholdError if invalid.
This commit is contained in:
@@ -229,7 +229,7 @@ class ContainsValidator:
|
|||||||
case_sensitive: Whether matching is case-sensitive. Defaults to False.
|
case_sensitive: Whether matching is case-sensitive. Defaults to False.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
InvalidThresholdError: If patterns list is empty.
|
InvalidThresholdError: If patterns list is empty or contains invalid regex.
|
||||||
"""
|
"""
|
||||||
if not patterns:
|
if not patterns:
|
||||||
raise InvalidThresholdError("patterns list cannot be empty")
|
raise InvalidThresholdError("patterns list cannot be empty")
|
||||||
@@ -238,6 +238,15 @@ class ContainsValidator:
|
|||||||
self._case_sensitive = case_sensitive
|
self._case_sensitive = case_sensitive
|
||||||
self._flags = 0 if case_sensitive else re.IGNORECASE
|
self._flags = 0 if case_sensitive else re.IGNORECASE
|
||||||
|
|
||||||
|
self._compiled_patterns: list[re.Pattern[str]] = []
|
||||||
|
for pattern in patterns:
|
||||||
|
try:
|
||||||
|
self._compiled_patterns.append(re.compile(pattern, self._flags))
|
||||||
|
except re.error as e:
|
||||||
|
raise InvalidThresholdError(
|
||||||
|
f"Invalid regex pattern '{pattern}': {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return the name of this check."""
|
"""Return the name of this check."""
|
||||||
@@ -255,8 +264,10 @@ class ContainsValidator:
|
|||||||
CheckResult with pass/fail status.
|
CheckResult with pass/fail status.
|
||||||
"""
|
"""
|
||||||
missing = []
|
missing = []
|
||||||
for pattern in self._patterns:
|
for pattern, compiled in zip(
|
||||||
if not re.search(pattern, text, self._flags):
|
self._patterns, self._compiled_patterns, strict=True
|
||||||
|
):
|
||||||
|
if not compiled.search(text):
|
||||||
missing.append(pattern)
|
missing.append(pattern)
|
||||||
|
|
||||||
passed = len(missing) == 0
|
passed = len(missing) == 0
|
||||||
@@ -291,7 +302,7 @@ class ExcludesValidator:
|
|||||||
case_sensitive: Whether matching is case-sensitive. Defaults to False.
|
case_sensitive: Whether matching is case-sensitive. Defaults to False.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
InvalidThresholdError: If patterns list is empty.
|
InvalidThresholdError: If patterns list is empty or contains invalid regex.
|
||||||
"""
|
"""
|
||||||
if not patterns:
|
if not patterns:
|
||||||
raise InvalidThresholdError("patterns list cannot be empty")
|
raise InvalidThresholdError("patterns list cannot be empty")
|
||||||
@@ -300,6 +311,15 @@ class ExcludesValidator:
|
|||||||
self._case_sensitive = case_sensitive
|
self._case_sensitive = case_sensitive
|
||||||
self._flags = 0 if case_sensitive else re.IGNORECASE
|
self._flags = 0 if case_sensitive else re.IGNORECASE
|
||||||
|
|
||||||
|
self._compiled_patterns: list[re.Pattern[str]] = []
|
||||||
|
for pattern in patterns:
|
||||||
|
try:
|
||||||
|
self._compiled_patterns.append(re.compile(pattern, self._flags))
|
||||||
|
except re.error as e:
|
||||||
|
raise InvalidThresholdError(
|
||||||
|
f"Invalid regex pattern '{pattern}': {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return the name of this check."""
|
"""Return the name of this check."""
|
||||||
@@ -317,8 +337,10 @@ class ExcludesValidator:
|
|||||||
CheckResult with pass/fail status.
|
CheckResult with pass/fail status.
|
||||||
"""
|
"""
|
||||||
found = []
|
found = []
|
||||||
for pattern in self._patterns:
|
for pattern, compiled in zip(
|
||||||
if re.search(pattern, text, self._flags):
|
self._patterns, self._compiled_patterns, strict=True
|
||||||
|
):
|
||||||
|
if compiled.search(text):
|
||||||
found.append(pattern)
|
found.append(pattern)
|
||||||
|
|
||||||
passed = len(found) == 0
|
passed = len(found) == 0
|
||||||
|
|||||||
@@ -263,6 +263,11 @@ class TestContainsValidator:
|
|||||||
with pytest.raises(InvalidThresholdError, match="cannot be empty"):
|
with pytest.raises(InvalidThresholdError, match="cannot be empty"):
|
||||||
ContainsValidator(patterns=[])
|
ContainsValidator(patterns=[])
|
||||||
|
|
||||||
|
def test_contains_validator_raises_on_invalid_regex(self) -> None:
|
||||||
|
"""Test that invalid regex pattern raises error at init time."""
|
||||||
|
with pytest.raises(InvalidThresholdError, match="Invalid regex"):
|
||||||
|
ContainsValidator(patterns=[r"[invalid"])
|
||||||
|
|
||||||
def test_contains_factory_function(self) -> None:
|
def test_contains_factory_function(self) -> None:
|
||||||
"""Test the contains() factory function."""
|
"""Test the contains() factory function."""
|
||||||
validator = contains(patterns=["test"], case_sensitive=True)
|
validator = contains(patterns=["test"], case_sensitive=True)
|
||||||
@@ -327,6 +332,11 @@ class TestExcludesValidator:
|
|||||||
with pytest.raises(InvalidThresholdError, match="cannot be empty"):
|
with pytest.raises(InvalidThresholdError, match="cannot be empty"):
|
||||||
ExcludesValidator(patterns=[])
|
ExcludesValidator(patterns=[])
|
||||||
|
|
||||||
|
def test_excludes_validator_raises_on_invalid_regex(self) -> None:
|
||||||
|
"""Test that invalid regex pattern raises error at init time."""
|
||||||
|
with pytest.raises(InvalidThresholdError, match="Invalid regex"):
|
||||||
|
ExcludesValidator(patterns=[r"[invalid"])
|
||||||
|
|
||||||
def test_excludes_factory_function(self) -> None:
|
def test_excludes_factory_function(self) -> None:
|
||||||
"""Test the excludes() factory function."""
|
"""Test the excludes() factory function."""
|
||||||
validator = excludes(patterns=["test"], case_sensitive=True)
|
validator = excludes(patterns=["test"], case_sensitive=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user