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:
2026-02-04 00:22:47 +00:00
parent f18427e123
commit aa687f43cd
2 changed files with 38 additions and 6 deletions

View File

@@ -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

View File

@@ -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)