aboutsummaryrefslogtreecommitdiff
path: root/tests/pattern/composition.test.py
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-07-01 14:13:40 +0530
committerGitHub <[email protected]>2026-07-01 14:13:40 +0530
commitd46c40de921dd2ba62fbfcd0e2e4591eee8de2a9 (patch)
treefa0232556e112b320c0bfde96e3ef5f279b3df2d /tests/pattern/composition.test.py
parentbd3b12ff2f30c251c5fc52b43f81ec2f282d53ab (diff)
parent391a4c2fd4afbaa5910aaf670a5e1c88c5e5aa84 (diff)
downloadedify-d46c40de921dd2ba62fbfcd0e2e4591eee8de2a9.tar.xz
edify-d46c40de921dd2ba62fbfcd0e2e4591eee8de2a9.zip
feat: add Pattern class and .use() composition (#261)
## Summary Closes #125 — the ``Pattern`` class, a named reusable regex fragment that composes with any builder via ``.subexpression()`` or ``.use()``. Closes #126 — the ``.use(pattern)`` chain method, the ergonomic alias for the common-case ``.subexpression(pattern)`` (drops flag / anchor overrides, keeps sensible defaults). ## Design **``Pattern``** is the same fluent surface as ``RegexBuilder`` minus the terminals. It shares every chain-method mixin (anchors, assertions, captures, chain, chars, classes, flags, groups, quantifiers, subexpression). It deliberately does **not** carry ``to_regex`` / ``to_regex_string`` — a pattern is a fragment, and fragments compose upward through ``.use()`` rather than compile themselves. To emit a regex from a pattern, embed it in a builder: ``RegexBuilder().use(my_pattern).to_regex()``. **``BuilderCore``** (new) holds the immutable-state plumbing that both ``RegexBuilder`` and ``Pattern`` need — the ``_state`` attribute, the ``__init__``, and the clone-and-replace ``_with_state`` helper. Both concrete classes now inherit ``BuilderCore`` instead of duplicating three lines each. **``.use(pattern)``** lives on ``SubexpressionMixin`` alongside ``.subexpression``. It calls ``self.subexpression(pattern)`` with the common-case defaults, so ``Pattern`` and ``RegexBuilder`` both get it for free. Direct ``.subexpression`` calls remain available when you need the ``namespace`` / ``ignore_flags`` / ``ignore_start_and_end`` overrides. ## Fanout Closing this unblocks the operator algebra (#122, #123, #124), every validator-to-callable-Pattern migration (#177–#190), the AST walker (#143), and every building-block atom (#172–#176).
Diffstat (limited to 'tests/pattern/composition.test.py')
-rw-r--r--tests/pattern/composition.test.py40
1 files changed, 40 insertions, 0 deletions
diff --git a/tests/pattern/composition.test.py b/tests/pattern/composition.test.py
new file mode 100644
index 0000000..9cf0003
--- /dev/null
+++ b/tests/pattern/composition.test.py
@@ -0,0 +1,40 @@
+"""Tests for the :class:`Pattern` composition surface."""
+
+import pytest
+
+from edify import Pattern, RegexBuilder
+from edify.errors.input import MustBeInstanceError
+
+
+def test_pattern_builds_the_same_element_tree_as_a_builder():
+ pattern = Pattern().between(3, 20).word()
+ builder = RegexBuilder().between(3, 20).word()
+ assert pattern._state == builder._state
+
+
+def test_pattern_has_no_to_regex_terminal():
+ pattern = Pattern().digit()
+ assert not hasattr(pattern, "to_regex")
+
+
+def test_pattern_has_no_to_regex_string_terminal():
+ pattern = Pattern().digit()
+ assert not hasattr(pattern, "to_regex_string")
+
+
+def test_pattern_supports_nested_use_composition():
+ inner = Pattern().one_or_more().digit()
+ outer = Pattern().string("v").use(inner)
+ embedded = RegexBuilder().use(outer)
+ assert embedded.to_regex_string() == "v\\d+"
+
+
+def test_subexpression_still_accepts_a_pattern():
+ pattern = Pattern().at_least(3).word()
+ embedded = RegexBuilder().subexpression(pattern)
+ assert embedded.to_regex_string() == "\\w{3,}"
+
+
+def test_subexpression_rejects_non_builder_input():
+ with pytest.raises(MustBeInstanceError):
+ RegexBuilder().subexpression("not a pattern")