aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--edify/builder/mixins/matcher.py57
-rw-r--r--tests/builder/matcher.test.py18
-rw-r--r--tests/builder/timeout.test.py24
-rw-r--r--tests/discipline/matcher.test.py42
4 files changed, 58 insertions, 83 deletions
diff --git a/edify/builder/mixins/matcher.py b/edify/builder/mixins/matcher.py
index 6bf22ae..001d46f 100644
--- a/edify/builder/mixins/matcher.py
+++ b/edify/builder/mixins/matcher.py
@@ -1,14 +1,14 @@
-"""The :class:`MatcherMixin` — direct :mod:`re` method proxies on any fluent surface.
+"""The :class:`MatcherMixin` — the closed set of five match verbs on any fluent surface.
Compiles ``self`` on first use via :meth:`edify.builder.core.BuilderCore._lazy_regex`
and reuses the cached :class:`edify.result.regex.Regex` on every subsequent call,
so ``pattern.test("a")`` followed by ``pattern.match("b")`` compiles the underlying
regex exactly once.
-The signatures mirror :class:`re.Pattern` exactly so IDE autocomplete and
-type inference stay unchanged. :meth:`test` is the one non-:mod:`re` method:
-a boolean shortcut that returns ``True`` when the pattern matches anywhere
-in the input, ``False`` otherwise. Every match method also accepts a per-call
+The surface is intentionally limited to ``test``, ``match``, ``search``, ``findall``,
+``sub`` — the small verb set that keeps chain autocomplete uncluttered. Reach through
+:meth:`to_regex` and use :class:`edify.result.regex.Regex` for ``fullmatch``,
+``finditer``, ``subn``, or ``split``. Every match method also accepts a per-call
``timeout=`` kwarg that only applies under ``engine="regex"``.
"""
@@ -16,13 +16,13 @@ from __future__ import annotations
import re
import sys
-from collections.abc import Callable, Iterator
+from collections.abc import Callable
from edify.builder.types.protocol import BuilderProtocol
class MatcherMixin(BuilderProtocol):
- """Provides nine :class:`re.Pattern` proxy methods plus :meth:`test` on any fluent surface."""
+ """Provides the five-verb match surface (``test``/``match``/``search``/``findall``/``sub``)."""
def test(self, string: str, pos: int = 0, endpos: int = sys.maxsize) -> bool:
"""Return ``True`` when the pattern matches anywhere in ``string``, else ``False``."""
@@ -50,17 +50,6 @@ class MatcherMixin(BuilderProtocol):
"""Delegate to :meth:`re.Pattern.search`."""
return self._lazy_regex().search(string, pos, endpos, timeout=timeout)
- def fullmatch(
- self,
- string: str,
- pos: int = 0,
- endpos: int = sys.maxsize,
- *,
- timeout: float | None = None,
- ) -> re.Match[str] | None:
- """Delegate to :meth:`re.Pattern.fullmatch`."""
- return self._lazy_regex().fullmatch(string, pos, endpos, timeout=timeout)
-
def findall(
self,
string: str,
@@ -72,17 +61,6 @@ class MatcherMixin(BuilderProtocol):
"""Delegate to :meth:`re.Pattern.findall`."""
return self._lazy_regex().findall(string, pos, endpos, timeout=timeout)
- def finditer(
- self,
- string: str,
- pos: int = 0,
- endpos: int = sys.maxsize,
- *,
- timeout: float | None = None,
- ) -> Iterator[re.Match[str]]:
- """Delegate to :meth:`re.Pattern.finditer`."""
- return self._lazy_regex().finditer(string, pos, endpos, timeout=timeout)
-
def sub(
self,
replacement: str | Callable[[re.Match[str]], str],
@@ -93,24 +71,3 @@ class MatcherMixin(BuilderProtocol):
) -> str:
"""Delegate to :meth:`re.Pattern.sub`."""
return self._lazy_regex().sub(replacement, string, count=count, timeout=timeout)
-
- def subn(
- self,
- replacement: str | Callable[[re.Match[str]], str],
- string: str,
- count: int = 0,
- *,
- timeout: float | None = None,
- ) -> tuple[str, int]:
- """Delegate to :meth:`re.Pattern.subn`."""
- return self._lazy_regex().subn(replacement, string, count=count, timeout=timeout)
-
- def split(
- self,
- string: str,
- maxsplit: int = 0,
- *,
- timeout: float | None = None,
- ) -> list[str | None]:
- """Delegate to :meth:`re.Pattern.split`."""
- return self._lazy_regex().split(string, maxsplit=maxsplit, timeout=timeout)
diff --git a/tests/builder/matcher.test.py b/tests/builder/matcher.test.py
index 05a763a..d32097b 100644
--- a/tests/builder/matcher.test.py
+++ b/tests/builder/matcher.test.py
@@ -22,10 +22,10 @@ def test_pattern_search_finds_a_substring_hit():
assert hit.group() == "world"
-def test_pattern_fullmatch_requires_the_entire_string():
+def test_pattern_fullmatch_via_to_regex_requires_the_entire_string():
zip_code = START + exactly(5, DIGIT) + END
- assert zip_code.fullmatch("10001") is not None
- assert zip_code.fullmatch("10001x") is None
+ assert zip_code.to_regex().fullmatch("10001") is not None
+ assert zip_code.to_regex().fullmatch("10001x") is None
def test_pattern_findall_returns_every_hit():
@@ -33,9 +33,9 @@ def test_pattern_findall_returns_every_hit():
assert lower.findall("hi there") == ["hi", "there"]
-def test_pattern_finditer_yields_match_objects():
+def test_pattern_finditer_via_to_regex_yields_match_objects():
lower = one_or_more(range_of("a", "z"))
- hits = list(lower.finditer("foo bar"))
+ hits = list(lower.to_regex().finditer("foo bar"))
assert [match.group() for match in hits] == ["foo", "bar"]
@@ -44,16 +44,16 @@ def test_pattern_sub_replaces_hits():
assert lower.sub("[X]", "hi and hello!") == "[X] [X] [X]!"
-def test_pattern_subn_returns_count_of_replacements():
+def test_pattern_subn_via_to_regex_returns_count_of_replacements():
lower = one_or_more(range_of("a", "z"))
- result, count = lower.subn("[X]", "hi hi hi")
+ result, count = lower.to_regex().subn("[X]", "hi hi hi")
assert result == "[X] [X] [X]"
assert count == 3
-def test_pattern_split_splits_on_matches():
+def test_pattern_split_via_to_regex_splits_on_matches():
lower = one_or_more(range_of("a", "z"))
- assert lower.split("one two three") == ["", " ", " ", ""]
+ assert lower.to_regex().split("one two three") == ["", " ", " ", ""]
def test_regex_builder_match_delegates_to_re_pattern_match():
diff --git a/tests/builder/timeout.test.py b/tests/builder/timeout.test.py
index 466ffb3..e443d2b 100644
--- a/tests/builder/timeout.test.py
+++ b/tests/builder/timeout.test.py
@@ -68,42 +68,18 @@ def test_matcher_match_forwards_timeout_to_the_cached_regex():
builder.match("aaa", timeout=1.0)
-def test_matcher_fullmatch_forwards_timeout_to_the_cached_regex():
- builder = RegexBuilder().string("a")
- with pytest.raises(TimeoutNotSupportedByEngineError):
- builder.fullmatch("a", timeout=1.0)
-
-
def test_matcher_findall_forwards_timeout_to_the_cached_regex():
builder = RegexBuilder().string("a")
with pytest.raises(TimeoutNotSupportedByEngineError):
builder.findall("aaa", timeout=1.0)
-def test_matcher_finditer_forwards_timeout_to_the_cached_regex():
- builder = RegexBuilder().string("a")
- with pytest.raises(TimeoutNotSupportedByEngineError):
- list(builder.finditer("aaa", timeout=1.0))
-
-
def test_matcher_sub_forwards_timeout_to_the_cached_regex():
builder = RegexBuilder().string("a")
with pytest.raises(TimeoutNotSupportedByEngineError):
builder.sub("b", "aaa", timeout=1.0)
-def test_matcher_subn_forwards_timeout_to_the_cached_regex():
- builder = RegexBuilder().string("a")
- with pytest.raises(TimeoutNotSupportedByEngineError):
- builder.subn("b", "aaa", timeout=1.0)
-
-
-def test_matcher_split_forwards_timeout_to_the_cached_regex():
- builder = RegexBuilder().string("a")
- with pytest.raises(TimeoutNotSupportedByEngineError):
- builder.split("aaa", timeout=1.0)
-
-
def test_no_timeout_kwarg_never_raises_on_either_engine():
assert _regex_pattern().search("aaa") is not None
assert _re_pattern().search("aaa") is not None
diff --git a/tests/discipline/matcher.test.py b/tests/discipline/matcher.test.py
new file mode 100644
index 0000000..8697daf
--- /dev/null
+++ b/tests/discipline/matcher.test.py
@@ -0,0 +1,42 @@
+"""Discipline test — the builder exposes exactly the five closed match verbs."""
+
+import pytest
+
+from edify import Pattern, RegexBuilder
+
+_ALLOWED_MATCH_VERBS = frozenset({"test", "match", "search", "findall", "sub"})
+_FORBIDDEN_MATCH_VERBS = frozenset({"fullmatch", "finditer", "subn", "split"})
+_FORBIDDEN_RE_PATTERN_ATTRS = frozenset({"groups", "groupindex", "pattern", "flags"})
+
+
[email protected]("factory", [RegexBuilder, Pattern])
+def test_builder_exposes_every_allowed_match_verb(factory):
+ builder = factory()
+ for verb in _ALLOWED_MATCH_VERBS:
+ assert callable(getattr(builder, verb)), f"missing verb: {verb}"
+
+
[email protected]("factory", [RegexBuilder, Pattern])
+def test_builder_does_not_expose_any_forbidden_match_verb(factory):
+ builder = factory()
+ for verb in _FORBIDDEN_MATCH_VERBS:
+ assert not hasattr(builder, verb), (
+ f"builder must not expose re.Pattern verb {verb!r}; "
+ "use .to_regex() and call it on the Regex wrapper instead"
+ )
+
+
[email protected]("factory", [RegexBuilder, Pattern])
+def test_builder_does_not_expose_re_pattern_metadata_attributes(factory):
+ builder = factory()
+ for attribute in _FORBIDDEN_RE_PATTERN_ATTRS:
+ assert not hasattr(builder, attribute), (
+ f"builder must not expose re.Pattern attribute {attribute!r}"
+ )
+
+
+def test_regex_wrapper_exposes_every_re_pattern_verb():
+ regex = RegexBuilder().string("a").to_regex()
+ re_pattern_verbs = (_ALLOWED_MATCH_VERBS | _FORBIDDEN_MATCH_VERBS) - {"test"}
+ for verb in re_pattern_verbs:
+ assert callable(getattr(regex, verb)), f"Regex wrapper missing verb: {verb}"