diff options
| author | 夏音 / natsuoto.exe <[email protected]> | 2026-07-01 17:31:04 +0530 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-07-01 17:31:04 +0530 |
| commit | ad4eb75ddcadf263391bba8eed837cce3b860cf9 (patch) | |
| tree | b3ec15be9350eec74079c136d68dbd6c0239e32b /tests/errors | |
| parent | 2d9e7395dc62ca7ff9c0bf508ca4fb5e823b538e (diff) | |
| parent | a2195e709d7c4f1fc3c75740446397f1b62c37a0 (diff) | |
| download | edify-ad4eb75ddcadf263391bba8eed837cce3b860cf9.tar.xz edify-ad4eb75ddcadf263391bba8eed837cce3b860cf9.zip | |
fix!: quantifier validation — dangling and stacked quantifiers raise; Hypothesis property gate (#269)
Tightens quantifier semantics so the builder can no longer silently drop
a quantifier from the emitted regex, and locks the invariant in with a
Hypothesis property test.
## Dangling quantifier now raises
`RegexBuilder().exactly(3).to_regex_string()` used to emit `^$` — the
quantifier had no operand, so it was silently discarded. It now raises
`DanglingQuantifierError` at emit time.
```python
RegexBuilder().exactly(3).to_regex_string()
# edify.errors.quantifier.DanglingQuantifierError:
# Dangling quantifier with no operand. Append an element (e.g. .digit()) before compiling.
```
## Stacked quantifiers now raise
`RegexBuilder().one_or_more().exactly(3).digit()` used to emit `\d{3}` —
the second quantifier overwrote the first, silently dropping
`one_or_more`. It now raises `StackedQuantifierError` at chain-call
time.
```python
RegexBuilder().one_or_more().exactly(3).digit()
# edify.errors.quantifier.StackedQuantifierError:
# Cannot stack a quantifier on top of another pending quantifier.
# Add an operand between the two quantifiers or drop one.
```
## Property assertion
Hypothesis-driven test in `tests/builder/properties.test.py` — for any
list of `(quantifier method, element method)` pairs, the emitted regex
is exactly the concatenation of `<element><suffix>` fragments in order.
`hypothesis>=6.100` added to the `dev` dependency group.
## Breaking
Any chain that previously silently emitted a wrong-but-parseable regex
now raises. Correct chains are unaffected.
Closes #109
Closes #110
Closes #111
Diffstat (limited to 'tests/errors')
| -rw-r--r-- | tests/errors/quantifier.test.py | 88 |
1 files changed, 88 insertions, 0 deletions
diff --git a/tests/errors/quantifier.test.py b/tests/errors/quantifier.test.py new file mode 100644 index 0000000..27b5ddf --- /dev/null +++ b/tests/errors/quantifier.test.py @@ -0,0 +1,88 @@ +"""Tests for the :mod:`edify.errors.quantifier` exception classes.""" + +import pytest + +from edify import Pattern, RegexBuilder +from edify.errors.quantifier import DanglingQuantifierError, StackedQuantifierError + + +def test_to_regex_string_raises_when_a_bare_quantifier_has_no_operand(): + with pytest.raises(DanglingQuantifierError): + RegexBuilder().exactly(3).to_regex_string() + + +def test_to_regex_string_raises_for_optional_with_no_operand(): + with pytest.raises(DanglingQuantifierError): + RegexBuilder().optional().to_regex_string() + + +def test_to_regex_string_raises_for_zero_or_more_with_no_operand(): + with pytest.raises(DanglingQuantifierError): + RegexBuilder().zero_or_more().to_regex_string() + + +def test_to_regex_string_raises_for_one_or_more_with_no_operand(): + with pytest.raises(DanglingQuantifierError): + RegexBuilder().one_or_more().to_regex_string() + + +def test_to_regex_string_raises_for_between_with_no_operand(): + with pytest.raises(DanglingQuantifierError): + RegexBuilder().between(2, 5).to_regex_string() + + +def test_pattern_to_regex_string_raises_when_a_bare_quantifier_has_no_operand(): + with pytest.raises(DanglingQuantifierError): + Pattern().exactly(3).to_regex_string() + + +def test_to_regex_raises_when_a_bare_quantifier_has_no_operand(): + with pytest.raises(DanglingQuantifierError): + RegexBuilder().at_least(2).to_regex() + + +def test_dangling_message_hints_at_appending_an_operand(): + with pytest.raises(DanglingQuantifierError, match="Append an element"): + RegexBuilder().exactly(3).to_regex_string() + + +def test_dangling_quantifier_error_message_contains_expected_text(): + error = DanglingQuantifierError() + assert "Dangling quantifier" in str(error) + assert "no operand" in str(error) + + +def test_stacking_one_or_more_over_exactly_raises(): + with pytest.raises(StackedQuantifierError): + RegexBuilder().one_or_more().exactly(3).digit() + + +def test_stacking_optional_over_at_least_raises(): + with pytest.raises(StackedQuantifierError): + RegexBuilder().optional().at_least(2).digit() + + +def test_stacking_between_over_zero_or_more_raises(): + with pytest.raises(StackedQuantifierError): + RegexBuilder().zero_or_more().between(1, 3).digit() + + +def test_stacking_lazy_variants_also_raises(): + with pytest.raises(StackedQuantifierError): + RegexBuilder().one_or_more_lazy().exactly(2).digit() + + +def test_stacking_on_pattern_raises(): + with pytest.raises(StackedQuantifierError): + Pattern().one_or_more().exactly(3).digit() + + +def test_a_valid_quantifier_element_quantifier_element_chain_works(): + expr = RegexBuilder().one_or_more().digit().exactly(3).word() + assert expr.to_regex_string() == "\\d+\\w{3}" + + +def test_stacked_quantifier_error_message_contains_expected_text(): + error = StackedQuantifierError() + assert "stack" in str(error) + assert "pending" in str(error) |
