diff options
| author | Bobby <[email protected]> | 2026-07-01 17:50:29 +0530 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-07-01 17:50:29 +0530 |
| commit | c757ea1fea447df3ed69e74b407ca9710a9f85e0 (patch) | |
| tree | a710f73b1a8b7b0117f41a18c9accd01127b033e /tests/builder | |
| parent | ad4eb75ddcadf263391bba8eed837cce3b860cf9 (diff) | |
| parent | e0d5e70a0d1a35a1e9311aa8200fb5c5f2ca0854 (diff) | |
| download | edify-c757ea1fea447df3ed69e74b407ca9710a9f85e0.tar.xz edify-c757ea1fea447df3ed69e74b407ca9710a9f85e0.zip | |
feat!: Regex result wrapper on .to_regex(), plus flag kwargs at the terminal (#270)
Reshapes what `.to_regex()` returns and takes, so pattern compilation
becomes a first-class edify value.
## `Regex` result wrapper class
`edify.Regex` is a composition wrapper over `re.Pattern` (which is a
CPython C type and cannot be subclassed). It exposes:
- `.source` — the emitted regex string.
- `.compiled` — the underlying `re.Pattern` for callers that need
identity checks, `isinstance` checks, or direct interop with libraries
typed on `re.Pattern`.
- Eight direct delegates — `.match()`, `.search()`, `.fullmatch()`,
`.findall()`, `.finditer()`, `.sub()`, `.subn()`, `.split()` — mirroring
the `re.Pattern` query surface.
- Value equality on `(source, flags)` plus `__hash__`.
## `to_regex()` now returns `Regex`
The terminal returns `edify.Regex` instead of raw `re.Pattern`. The
existing eight query methods continue to work via delegation. `.match()`
/ `.search()` / … on builders (via `MatcherMixin`) still work because
they go through the same delegation.
**Breaking:** `isinstance(x, re.Pattern)` and `re.Pattern`-typed
annotations on values returned from `.to_regex()` break. Callers that
need the raw pattern read `.compiled`.
## Flag kwargs on `.to_regex(...)`
`.to_regex()` now accepts six keyword-only flag arguments —
`ascii_only`, `debug`, `ignore_case`, `multiline`, `dotall`, `verbose` —
that OR-merge into the flag snapshot the builder already carries.
Passing `ignore_case=True` at the terminal is equivalent to calling
`.ignore_case()` mid-chain. Kwargs never turn a chain-set flag off; the
terminal is the natural home for pattern-global settings.
## Example
```python
from edify import RegexBuilder
email = RegexBuilder().start_of_input().one_or_more().letter().end_of_input()
regex = email.to_regex(ignore_case=True)
regex.source # ^[a-zA-Z]+$
regex.match("Hello") # <re.Match ...>
regex.compiled # <re.Pattern object; flags=re.IGNORECASE>
```
Closes #133
Closes #134
Closes #128
Diffstat (limited to 'tests/builder')
| -rw-r--r-- | tests/builder/builder.test.py | 2 | ||||
| -rw-r--r-- | tests/builder/flags.test.py | 72 | ||||
| -rw-r--r-- | tests/builder/use.test.py | 2 |
3 files changed, 74 insertions, 2 deletions
diff --git a/tests/builder/builder.test.py b/tests/builder/builder.test.py index ddd1997..82aa1cf 100644 --- a/tests/builder/builder.test.py +++ b/tests/builder/builder.test.py @@ -47,7 +47,7 @@ def regex_equality(regex, rb_expression): def regex_compilation(regex, rb_expression, f=0): rb_expression_c = rb_expression.to_regex() - assert re.compile(regex, flags=f) == rb_expression_c + assert re.compile(regex, flags=f) == rb_expression_c.compiled def test_empty_regex(): diff --git a/tests/builder/flags.test.py b/tests/builder/flags.test.py new file mode 100644 index 0000000..d35d2ff --- /dev/null +++ b/tests/builder/flags.test.py @@ -0,0 +1,72 @@ +"""Tests for the flag keyword arguments on :meth:`TerminalsMixin.to_regex`.""" + +import re +import sys + +import pytest + +from edify import Pattern, RegexBuilder + +_ON_PYPY = hasattr(sys, "pypy_version_info") + + +def test_no_kwargs_uses_only_the_chain_flag_snapshot(): + compiled = RegexBuilder().string("hi").to_regex() + assert compiled.compiled.flags & re.I == 0 + + +def test_ignore_case_kwarg_enables_the_ignore_case_flag(): + compiled = RegexBuilder().string("hi").to_regex(ignore_case=True) + assert compiled.compiled.flags & re.I == re.I + + +def test_multiline_kwarg_enables_the_multiline_flag(): + compiled = RegexBuilder().string("hi").to_regex(multiline=True) + assert compiled.compiled.flags & re.M == re.M + + +def test_dotall_kwarg_enables_the_dotall_flag(): + compiled = RegexBuilder().string("hi").to_regex(dotall=True) + assert compiled.compiled.flags & re.S == re.S + + +def test_ascii_only_kwarg_enables_the_ascii_flag(): + compiled = RegexBuilder().string("hi").to_regex(ascii_only=True) + assert compiled.compiled.flags & re.A == re.A + + +def test_verbose_kwarg_enables_the_verbose_flag(): + compiled = RegexBuilder().string("hi").to_regex(verbose=True) + assert compiled.compiled.flags & re.X == re.X + + + _ON_PYPY, + reason="PyPy's re.DEBUG opcode disassembler has an upstream IndexError bug", +) +def test_debug_kwarg_compiles_without_error(): + compiled = RegexBuilder().string("hi").to_regex(debug=True) + assert compiled.source == "hi" + + +def test_kwargs_or_merge_with_chain_flags(): + compiled = RegexBuilder().ignore_case().string("hi").to_regex(multiline=True) + assert compiled.compiled.flags & re.I == re.I + assert compiled.compiled.flags & re.M == re.M + + +def test_kwargs_never_turn_off_a_chain_flag(): + compiled = RegexBuilder().ignore_case().string("hi").to_regex(ignore_case=False) + assert compiled.compiled.flags & re.I == re.I + + +def test_kwargs_work_on_pattern_too(): + compiled = Pattern().string("hi").to_regex(ignore_case=True) + assert compiled.compiled.flags & re.I == re.I + + +def test_multiple_kwargs_combine(): + compiled = RegexBuilder().string("hi").to_regex(ignore_case=True, multiline=True, dotall=True) + assert compiled.compiled.flags & re.I == re.I + assert compiled.compiled.flags & re.M == re.M + assert compiled.compiled.flags & re.S == re.S diff --git a/tests/builder/use.test.py b/tests/builder/use.test.py index 10d7855..0253786 100644 --- a/tests/builder/use.test.py +++ b/tests/builder/use.test.py @@ -21,7 +21,7 @@ def test_use_drops_the_pattern_flag_snapshot_by_default(): case_insensitive_pattern = Pattern().ignore_case().string("hello") expression = RegexBuilder().use(case_insensitive_pattern) compiled = expression.to_regex() - assert compiled.flags & 2 == 0 + assert compiled.compiled.flags & 2 == 0 def test_use_accepts_another_regex_builder_as_the_source(): |
