aboutsummaryrefslogtreecommitdiff
path: root/tests/pattern
diff options
context:
space:
mode:
authornatsuoto <[email protected]>2026-07-01 15:36:40 +0530
committernatsuoto <[email protected]>2026-07-01 15:36:40 +0530
commita7974d090211955c394e0fecbc547f73b7786a97 (patch)
tree890106bf873d10604d5d141444519a039441bd14 /tests/pattern
parentd46c40de921dd2ba62fbfcd0e2e4591eee8de2a9 (diff)
downloadedify-a7974d090211955c394e0fecbc547f73b7786a97.tar.xz
edify-a7974d090211955c394e0fecbc547f73b7786a97.zip
feat: humre-style functional API — operators, constants, factories, matcher
Diffstat (limited to 'tests/pattern')
-rw-r--r--tests/pattern/anchors.test.py27
-rw-r--r--tests/pattern/boundaries.test.py27
-rw-r--r--tests/pattern/classes.test.py58
-rw-r--r--tests/pattern/composition.test.py9
-rw-r--r--tests/pattern/factories/assertions.test.py30
-rw-r--r--tests/pattern/factories/groups.test.py71
-rw-r--r--tests/pattern/factories/quantifiers.test.py94
-rw-r--r--tests/pattern/factories/values.test.py67
-rw-r--r--tests/pattern/operators.test.py54
9 files changed, 433 insertions, 4 deletions
diff --git a/tests/pattern/anchors.test.py b/tests/pattern/anchors.test.py
new file mode 100644
index 0000000..a42aca0
--- /dev/null
+++ b/tests/pattern/anchors.test.py
@@ -0,0 +1,27 @@
+"""Tests for the START and END module-level anchor constants."""
+
+from edify import END, START, Pattern
+
+
+def test_start_is_a_pattern():
+ assert isinstance(START, Pattern)
+
+
+def test_end_is_a_pattern():
+ assert isinstance(END, Pattern)
+
+
+def test_start_compiles_to_caret():
+ assert START.to_regex_string() == "^"
+
+
+def test_end_compiles_to_dollar_sign():
+ assert END.to_regex_string() == "$"
+
+
+def test_start_matches_start_of_input_element_from_the_fluent_chain():
+ assert START._state == Pattern().start_of_input()._state
+
+
+def test_end_matches_end_of_input_element_from_the_fluent_chain():
+ assert END._state == Pattern().end_of_input()._state
diff --git a/tests/pattern/boundaries.test.py b/tests/pattern/boundaries.test.py
new file mode 100644
index 0000000..b391733
--- /dev/null
+++ b/tests/pattern/boundaries.test.py
@@ -0,0 +1,27 @@
+"""Tests for the WORD_BOUNDARY and NON_WORD_BOUNDARY module-level constants."""
+
+from edify import NON_WORD_BOUNDARY, WORD_BOUNDARY, Pattern
+
+
+def test_word_boundary_is_a_pattern():
+ assert isinstance(WORD_BOUNDARY, Pattern)
+
+
+def test_non_word_boundary_is_a_pattern():
+ assert isinstance(NON_WORD_BOUNDARY, Pattern)
+
+
+def test_word_boundary_compiles_to_backslash_b():
+ assert WORD_BOUNDARY.to_regex_string() == "\\b"
+
+
+def test_non_word_boundary_compiles_to_backslash_capital_b():
+ assert NON_WORD_BOUNDARY.to_regex_string() == "\\B"
+
+
+def test_word_boundary_matches_fluent_chain_output():
+ assert WORD_BOUNDARY._state == Pattern().word_boundary()._state
+
+
+def test_non_word_boundary_matches_fluent_chain_output():
+ assert NON_WORD_BOUNDARY._state == Pattern().non_word_boundary()._state
diff --git a/tests/pattern/classes.test.py b/tests/pattern/classes.test.py
new file mode 100644
index 0000000..de6bfc0
--- /dev/null
+++ b/tests/pattern/classes.test.py
@@ -0,0 +1,58 @@
+"""Tests for the module-level character-class :class:`Pattern` constants."""
+
+import pytest
+
+from edify import (
+ ANY_CHAR,
+ CARRIAGE_RETURN,
+ DIGIT,
+ NEW_LINE,
+ NON_DIGIT,
+ NON_WHITESPACE,
+ NON_WORD,
+ NULL_BYTE,
+ TAB,
+ WHITESPACE,
+ WORD,
+ Pattern,
+)
+
+
+ ("constant", "expected"),
+ [
+ (ANY_CHAR, "."),
+ (WHITESPACE, "\\s"),
+ (NON_WHITESPACE, "\\S"),
+ (DIGIT, "\\d"),
+ (NON_DIGIT, "\\D"),
+ (WORD, "\\w"),
+ (NON_WORD, "\\W"),
+ (NEW_LINE, "\\n"),
+ (CARRIAGE_RETURN, "\\r"),
+ (TAB, "\\t"),
+ (NULL_BYTE, "\\0"),
+ ],
+)
+def test_character_class_constant_compiles_to_expected_regex(constant, expected):
+ assert constant.to_regex_string() == expected
+
+
+ "constant",
+ [
+ ANY_CHAR,
+ WHITESPACE,
+ NON_WHITESPACE,
+ DIGIT,
+ NON_DIGIT,
+ WORD,
+ NON_WORD,
+ NEW_LINE,
+ CARRIAGE_RETURN,
+ TAB,
+ NULL_BYTE,
+ ],
+)
+def test_character_class_constant_is_a_pattern(constant):
+ assert isinstance(constant, Pattern)
diff --git a/tests/pattern/composition.test.py b/tests/pattern/composition.test.py
index 9cf0003..f58df57 100644
--- a/tests/pattern/composition.test.py
+++ b/tests/pattern/composition.test.py
@@ -12,14 +12,15 @@ def test_pattern_builds_the_same_element_tree_as_a_builder():
assert pattern._state == builder._state
-def test_pattern_has_no_to_regex_terminal():
+def test_pattern_exposes_to_regex_string_terminal():
pattern = Pattern().digit()
- assert not hasattr(pattern, "to_regex")
+ assert pattern.to_regex_string() == "\\d"
-def test_pattern_has_no_to_regex_string_terminal():
+def test_pattern_exposes_to_regex_terminal():
pattern = Pattern().digit()
- assert not hasattr(pattern, "to_regex_string")
+ compiled = pattern.to_regex()
+ assert compiled.pattern == "\\d"
def test_pattern_supports_nested_use_composition():
diff --git a/tests/pattern/factories/assertions.test.py b/tests/pattern/factories/assertions.test.py
new file mode 100644
index 0000000..ae521de
--- /dev/null
+++ b/tests/pattern/factories/assertions.test.py
@@ -0,0 +1,30 @@
+"""Tests for the functional lookaround assertion factories."""
+
+from edify import (
+ DIGIT,
+ Pattern,
+ assert_ahead,
+ assert_behind,
+ assert_not_ahead,
+ assert_not_behind,
+)
+
+
+def test_assert_ahead_emits_positive_lookahead():
+ assert assert_ahead(DIGIT).to_regex_string() == "(?=\\d)"
+
+
+def test_assert_not_ahead_emits_negative_lookahead():
+ assert assert_not_ahead(DIGIT).to_regex_string() == "(?!\\d)"
+
+
+def test_assert_behind_emits_positive_lookbehind():
+ assert assert_behind(DIGIT).to_regex_string() == "(?<=\\d)"
+
+
+def test_assert_not_behind_emits_negative_lookbehind():
+ assert assert_not_behind(DIGIT).to_regex_string() == "(?<!\\d)"
+
+
+def test_assert_ahead_returns_a_pattern_instance():
+ assert isinstance(assert_ahead(DIGIT), Pattern)
diff --git a/tests/pattern/factories/groups.test.py b/tests/pattern/factories/groups.test.py
new file mode 100644
index 0000000..a90ce28
--- /dev/null
+++ b/tests/pattern/factories/groups.test.py
@@ -0,0 +1,71 @@
+"""Tests for the functional grouping, capture, back-reference, and alternation factories."""
+
+import pytest
+
+from edify import (
+ DIGIT,
+ WORD,
+ Pattern,
+ any_of,
+ back_reference,
+ capture,
+ group,
+ named_back_reference,
+ named_capture,
+ string,
+)
+from edify.errors.input import MustBeAtLeastTwoOperandsError, MustBePositiveIntegerError
+
+
+def test_group_wraps_operand_in_a_non_capturing_group():
+ assert group(DIGIT + WORD).to_regex_string() == "(?:\\d\\w)"
+
+
+def test_capture_wraps_operand_in_a_numbered_group():
+ assert capture(DIGIT).to_regex_string() == "(\\d)"
+
+
+def test_named_capture_wraps_operand_with_the_supplied_name():
+ assert named_capture("year", DIGIT).to_regex_string() == "(?P<year>\\d)"
+
+
+def test_back_reference_emits_a_numbered_backref():
+ assert back_reference(1).to_regex_string() == "\\1"
+
+
+def test_named_back_reference_emits_a_named_backref():
+ assert named_back_reference("year").to_regex_string() == "(?P=year)"
+
+
+def test_any_of_produces_alternation_between_multi_character_operands():
+ assert any_of(string("http"), string("https")).to_regex_string() == "(?:http|https)"
+
+
+def test_any_of_accepts_more_than_two_operands():
+ assert any_of(string("cat"), string("dog"), string("fish")).to_regex_string() == (
+ "(?:cat|dog|fish)"
+ )
+
+
+def test_group_returns_a_pattern_instance():
+ assert isinstance(group(DIGIT), Pattern)
+
+
+def test_any_of_rejects_a_single_operand():
+ with pytest.raises(MustBeAtLeastTwoOperandsError):
+ any_of(DIGIT)
+
+
+def test_any_of_rejects_zero_operands():
+ with pytest.raises(MustBeAtLeastTwoOperandsError):
+ any_of()
+
+
+def test_back_reference_rejects_zero_index():
+ with pytest.raises(MustBePositiveIntegerError):
+ back_reference(0)
+
+
+def test_back_reference_rejects_negative_index():
+ with pytest.raises(MustBePositiveIntegerError):
+ back_reference(-1)
diff --git a/tests/pattern/factories/quantifiers.test.py b/tests/pattern/factories/quantifiers.test.py
new file mode 100644
index 0000000..8a6f34f
--- /dev/null
+++ b/tests/pattern/factories/quantifiers.test.py
@@ -0,0 +1,94 @@
+"""Tests for the functional quantifier factories."""
+
+import pytest
+
+from edify import (
+ DIGIT,
+ WORD,
+ Pattern,
+ at_least,
+ between,
+ between_lazy,
+ char,
+ exactly,
+ one_or_more,
+ one_or_more_lazy,
+ optional,
+ string,
+ zero_or_more,
+ zero_or_more_lazy,
+)
+from edify.errors.input import (
+ MustBeIntegerGreaterThanZeroError,
+ MustBeLessThanError,
+ MustBePositiveIntegerError,
+)
+
+
+def test_optional_wraps_a_single_element_operand_directly():
+ assert optional(char("x")).to_regex_string() == "x?"
+
+
+def test_zero_or_more_wraps_a_module_constant():
+ assert zero_or_more(WORD).to_regex_string() == "\\w*"
+
+
+def test_zero_or_more_lazy_emits_lazy_star():
+ assert zero_or_more_lazy(WORD).to_regex_string() == "\\w*?"
+
+
+def test_one_or_more_wraps_a_module_constant():
+ assert one_or_more(DIGIT).to_regex_string() == "\\d+"
+
+
+def test_one_or_more_lazy_emits_lazy_plus():
+ assert one_or_more_lazy(DIGIT).to_regex_string() == "\\d+?"
+
+
+def test_exactly_wraps_a_module_constant_without_extra_grouping():
+ assert exactly(3, DIGIT).to_regex_string() == "\\d{3}"
+
+
+def test_exactly_groups_a_multi_element_operand():
+ combined = DIGIT + WORD
+ assert exactly(3, combined).to_regex_string() == "(?:\\d\\w){3}"
+
+
+def test_exactly_groups_a_multi_character_string_operand():
+ assert exactly(3, string("ab")).to_regex_string() == "(?:ab){3}"
+
+
+def test_at_least_produces_open_ended_quantifier():
+ assert at_least(2, DIGIT).to_regex_string() == "\\d{2,}"
+
+
+def test_between_produces_greedy_bounded_quantifier():
+ assert between(2, 4, DIGIT).to_regex_string() == "\\d{2,4}"
+
+
+def test_between_lazy_produces_lazy_bounded_quantifier():
+ assert between_lazy(2, 4, DIGIT).to_regex_string() == "\\d{2,4}?"
+
+
+def test_optional_returns_a_pattern_instance():
+ assert isinstance(optional(DIGIT), Pattern)
+
+
+def test_exactly_rejects_zero_count():
+ with pytest.raises(MustBePositiveIntegerError):
+ exactly(0, DIGIT)
+
+
+def test_at_least_rejects_negative_count():
+ with pytest.raises(MustBePositiveIntegerError):
+ at_least(-1, DIGIT)
+
+
+def test_between_rejects_negative_lower_bound():
+ with pytest.raises(MustBeIntegerGreaterThanZeroError):
+ between(-1, 5, DIGIT)
+
+
+def test_between_rejects_upper_bound_less_than_or_equal_to_lower():
+ with pytest.raises(MustBeLessThanError):
+ between(3, 3, DIGIT)
diff --git a/tests/pattern/factories/values.test.py b/tests/pattern/factories/values.test.py
new file mode 100644
index 0000000..69b04b7
--- /dev/null
+++ b/tests/pattern/factories/values.test.py
@@ -0,0 +1,67 @@
+"""Tests for the functional value factories."""
+
+import pytest
+
+from edify import Pattern, char, chars, nonchars, nonrange, nonstring, range_of, string
+from edify.errors.input import (
+ MustBeAStringError,
+ MustBeOneCharacterError,
+ MustBeSingleCharacterError,
+ MustHaveASmallerValueError,
+)
+
+
+def test_string_produces_the_literal_value():
+ assert string("hello").to_regex_string() == "hello"
+
+
+def test_string_returns_a_pattern_instance():
+ assert isinstance(string("x"), Pattern)
+
+
+def test_string_escapes_regex_metacharacters():
+ assert string("a.b").to_regex_string() == "a\\.b"
+
+
+def test_char_produces_a_single_character_literal():
+ assert char("a").to_regex_string() == "a"
+
+
+def test_char_rejects_multi_character_input():
+ with pytest.raises(MustBeSingleCharacterError):
+ char("ab")
+
+
+def test_range_of_produces_an_ascii_range():
+ assert range_of("a", "z").to_regex_string() == "[a-z]"
+
+
+def test_range_of_rejects_descending_bounds():
+ with pytest.raises(MustHaveASmallerValueError):
+ range_of("z", "a")
+
+
+def test_chars_produces_an_inline_character_class():
+ assert chars("abc").to_regex_string() == "[abc]"
+
+
+def test_nonchars_produces_a_negated_character_class():
+ assert nonchars("abc").to_regex_string() == "[^abc]"
+
+
+def test_nonstring_produces_a_per_character_negation():
+ assert nonstring("ab").to_regex_string() == "(?:[^a][^b])"
+
+
+def test_nonrange_produces_a_negated_character_range():
+ assert nonrange("a", "z").to_regex_string() == "[^a-z]"
+
+
+def test_string_rejects_empty_input():
+ with pytest.raises(MustBeOneCharacterError):
+ string("")
+
+
+def test_string_rejects_non_string_input():
+ with pytest.raises(MustBeAStringError):
+ string(42)
diff --git a/tests/pattern/operators.test.py b/tests/pattern/operators.test.py
new file mode 100644
index 0000000..74b672f
--- /dev/null
+++ b/tests/pattern/operators.test.py
@@ -0,0 +1,54 @@
+"""Tests for the ``+`` and ``|`` operators on :class:`Pattern`."""
+
+from edify import DIGIT, END, START, WORD, Pattern
+
+
+def test_plus_concatenates_two_patterns():
+ combined = Pattern().string("hello") + Pattern().string("world")
+ assert combined.to_regex_string() == "helloworld"
+
+
+def test_plus_preserves_anchors_from_both_operands():
+ combined = START + Pattern().exactly(4).digit() + END
+ assert combined.to_regex_string() == "^\\d{4}$"
+
+
+def test_plus_returns_a_new_pattern_and_leaves_operands_untouched():
+ left = Pattern().digit()
+ right = Pattern().word()
+ combined = left + right
+ assert combined is not left
+ assert combined is not right
+ assert left.to_regex_string() == "\\d"
+ assert right.to_regex_string() == "\\w"
+
+
+def test_plus_using_module_constants_produces_expected_string():
+ combined = DIGIT + WORD
+ assert combined.to_regex_string() == "\\d\\w"
+
+
+def test_or_produces_alternation_between_two_patterns():
+ combined = Pattern().string("http") | Pattern().string("https")
+ assert combined.to_regex_string() == "(?:http|https)"
+
+
+def test_or_returns_a_new_pattern_and_leaves_operands_untouched():
+ left = Pattern().string("cat")
+ right = Pattern().string("dog")
+ combined = left | right
+ assert combined is not left
+ assert combined is not right
+ assert left.to_regex_string() == "cat"
+ assert right.to_regex_string() == "dog"
+
+
+def test_or_chaining_produces_three_way_alternation():
+ combined = Pattern().string("cat") | Pattern().string("dog") | Pattern().string("fish")
+ assert combined.to_regex_string() == "(?:(?:cat|dog)|fish)"
+
+
+def test_plus_composes_with_or_to_form_a_realistic_pattern():
+ scheme = Pattern().string("http") | Pattern().string("https")
+ combined = scheme + Pattern().string("://")
+ assert combined.to_regex_string() == "(?:http|https)://"