diff options
| -rw-r--r-- | edify/builder/mixins/matcher.py | 57 | ||||
| -rw-r--r-- | tests/builder/matcher.test.py | 18 | ||||
| -rw-r--r-- | tests/builder/timeout.test.py | 24 | ||||
| -rw-r--r-- | tests/discipline/matcher.test.py | 42 |
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}" |
