diff --git a/src/veritext/validators/constraint.py b/src/veritext/validators/constraint.py index 5751f7a..bfe8a79 100644 --- a/src/veritext/validators/constraint.py +++ b/src/veritext/validators/constraint.py @@ -229,7 +229,7 @@ class ContainsValidator: case_sensitive: Whether matching is case-sensitive. Defaults to False. Raises: - InvalidThresholdError: If patterns list is empty. + InvalidThresholdError: If patterns list is empty or contains invalid regex. """ if not patterns: raise InvalidThresholdError("patterns list cannot be empty") @@ -238,6 +238,15 @@ class ContainsValidator: self._case_sensitive = case_sensitive 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 def name(self) -> str: """Return the name of this check.""" @@ -255,8 +264,10 @@ class ContainsValidator: CheckResult with pass/fail status. """ missing = [] - for pattern in self._patterns: - if not re.search(pattern, text, self._flags): + for pattern, compiled in zip( + self._patterns, self._compiled_patterns, strict=True + ): + if not compiled.search(text): missing.append(pattern) passed = len(missing) == 0 @@ -291,7 +302,7 @@ class ExcludesValidator: case_sensitive: Whether matching is case-sensitive. Defaults to False. Raises: - InvalidThresholdError: If patterns list is empty. + InvalidThresholdError: If patterns list is empty or contains invalid regex. """ if not patterns: raise InvalidThresholdError("patterns list cannot be empty") @@ -300,6 +311,15 @@ class ExcludesValidator: self._case_sensitive = case_sensitive 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 def name(self) -> str: """Return the name of this check.""" @@ -317,8 +337,10 @@ class ExcludesValidator: CheckResult with pass/fail status. """ found = [] - for pattern in self._patterns: - if re.search(pattern, text, self._flags): + for pattern, compiled in zip( + self._patterns, self._compiled_patterns, strict=True + ): + if compiled.search(text): found.append(pattern) passed = len(found) == 0 diff --git a/tests/test_validators/test_constraint.py b/tests/test_validators/test_constraint.py index 67d31ce..759d53b 100644 --- a/tests/test_validators/test_constraint.py +++ b/tests/test_validators/test_constraint.py @@ -263,6 +263,11 @@ class TestContainsValidator: with pytest.raises(InvalidThresholdError, match="cannot be empty"): 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: """Test the contains() factory function.""" validator = contains(patterns=["test"], case_sensitive=True) @@ -327,6 +332,11 @@ class TestExcludesValidator: with pytest.raises(InvalidThresholdError, match="cannot be empty"): 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: """Test the excludes() factory function.""" validator = excludes(patterns=["test"], case_sensitive=True)