| Age | Commit message (Collapse) | Author | Files | Lines |
|
visualize + annotated errors (#272)
Ships the regex introspection API on the `Regex` wrapper — three new
methods that turn a compiled pattern into something a human can read,
plus a full retrofit of every error message to a shape that shows the
caller *where* the failure happened and *how* to fix it.
## `Regex.explain()`
Plain-English bullet list of what the pattern accepts, followed by a
small set of concrete accepted strings.
```
- The text must start with either "http" or "https".
- Then the text must have "://".
- Then the text must have one or more letters, digits, or underscores.
Text this pattern accepts:
http://ab1
https://b1_c
http://1_c2d
```
## `Regex.to_verbose_string()`
An `re.VERBOSE`-compatible export where every fragment is annotated
inline. Copy-pasting the output into `re.compile(..., re.VERBOSE)`
compiles to the same pattern.
```
(?P<year> # begin group named "year"
\d{4} # exactly 4
) # end group named "year"
```
## `Regex.visualize(format='ascii')`
ASCII railroad diagram — `START` and `END` boxes flanking the pattern,
arrows between elements, fork/merge junctions for alternation,
dashed-caption blocks for captures and lookarounds.
```
+--------+
+--->| "cat" |----+
| +--------+ |
| |
+-------+ | +--------+ | +-----+
| START |------>+--->| "dog" |----+-->| END |
+-------+ | +--------+ | +-----+
| |
| +--------+ |
+--->| "fish" |----+
+--------+
```
## `Regex.visualize(format='svg', engine='graphviz')`
SVG rendered by Graphviz. Junction-point fork/merge for alternation,
dashed rounded clusters for captures/lookarounds, folded two-line node
labels for quantifiers (e.g. `digit\n(one or more)`). Requires the
optional `graphviz` extra; the missing-dependency error names the
install command.
## Annotated error messages
Every exception across `edify.errors` — anchors, captures, input,
internal, naming, quantifier, structure — now emits a message that names
the offending call in the caller's source, explains the invariant that
was violated, and prescribes the fix:
```
error: start_of_input has already been added to this pattern
--> user_code.py:14:26
|
14 | pattern = RegexBuilder().start_of_input().digit().start_of_input()
| ^^^^^^^^^^^^^^^^ second start_of_input added here
|
= note: a pattern can carry at most one start_of_input anchor; the earlier .start_of_input() call already set it.
help: remove the duplicate .start_of_input() call from the chain.
```
Caller source location is captured via `sys._getframe()` walking to the
first non-edify frame and `co_positions()` for precise column spans.
## Other landings
- `Regex` retains the AST elements the builder produced; every
introspection method operates on the AST directly rather than parsing
the emitted regex string.
- Lazy compile cache on `BuilderCore` — a builder's `to_regex()` result
is memoised so `.match()` / `.search()` / `.explain()` share one
compiled pattern.
- `Regex.__getattr__` delegates any attribute not on the wrapper to the
underlying `re.Pattern`, so `.pattern`, `.flags`, `.groups`,
`.groupindex` continue to work.
- `Regex.__eq__` / `Regex.__hash__` raise when called on an unfinished
builder, pointing at both the compare/hash call site and the still-open
frame or dangling quantifier.
- Property test extended: every recursively-built composition of
quantifiers, groups, captures, named captures, and subexpressions emits
its expected regex exactly, no fragment dropped or duplicated.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
subexpressions (#111)
|
|
(#133, #145, #146, #147, #148)
|
|
unfinished builders (#127, #137)
|
|
|
|
|
|
(#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
|
|
|
|
|
|
dotall=...)
|
|
|
|
|
|
Hypothesis property gate (#269)
Tightens quantifier semantics so the builder can no longer silently drop
a quantifier from the emitted regex, and locks the invariant in with a
Hypothesis property test.
## Dangling quantifier now raises
`RegexBuilder().exactly(3).to_regex_string()` used to emit `^$` — the
quantifier had no operand, so it was silently discarded. It now raises
`DanglingQuantifierError` at emit time.
```python
RegexBuilder().exactly(3).to_regex_string()
# edify.errors.quantifier.DanglingQuantifierError:
# Dangling quantifier with no operand. Append an element (e.g. .digit()) before compiling.
```
## Stacked quantifiers now raise
`RegexBuilder().one_or_more().exactly(3).digit()` used to emit `\d{3}` —
the second quantifier overwrote the first, silently dropping
`one_or_more`. It now raises `StackedQuantifierError` at chain-call
time.
```python
RegexBuilder().one_or_more().exactly(3).digit()
# edify.errors.quantifier.StackedQuantifierError:
# Cannot stack a quantifier on top of another pending quantifier.
# Add an operand between the two quantifiers or drop one.
```
## Property assertion
Hypothesis-driven test in `tests/builder/properties.test.py` — for any
list of `(quantifier method, element method)` pairs, the emitted regex
is exactly the concatenation of `<element><suffix>` fragments in order.
`hypothesis>=6.100` added to the `dev` dependency group.
## Breaking
Any chain that previously silently emitted a wrong-but-parseable regex
now raises. Correct chains are unaffected.
Closes #109
Closes #110
Closes #111
|
|
|
|
|
|
|
|
|
|
Three builder-ergonomics wins so `RegexBuilder` / `Pattern` behave like
proper Python values in interactive shells, dicts, and comparisons.
## `__repr__` shows the pattern-so-far
Replaces the useless default `<edify.builder.builder.RegexBuilder object
at 0x...>` with `<RegexBuilder '^\d+'>`. Builders whose frames are still
open render as `<RegexBuilder '<unclosed>'>` so the repr never raises.
## `__eq__` / `__hash__` — value equality
Two builders are equal when their underlying immutable `BuilderState`
matches; the hash derives from the same state. Builders become safe as
dict keys, set members, or assertion targets. `RegexBuilder` and
`Pattern` share every mixin below the class name, so a builder and
pattern with identical state compare equal.
**Breaking:** identity-based `==` on builders becomes value-based. Only
code that intentionally relied on identity equality is affected.
## `.fork()` / `.copy()` — explicit branch construction
Chaining already yields implicit forking (immutable state means
`root.digit()` and `root.word()` share nothing after the split);
`.fork()` and `.copy()` are aliases that surface the property via
autocomplete and signal branching intent in code.
## Example
```python
from edify import RegexBuilder
builder = RegexBuilder().start_of_input().exactly(4).digit().end_of_input()
repr(builder) # <RegexBuilder '^\d{4}$'>
RegexBuilder().digit() == RegexBuilder().digit() # True
lookup = {RegexBuilder().digit(): "hit"} # hashable, dict-key safe
root = RegexBuilder().digit()
branch = root.fork().word() # explicit branch off root
```
Closes #136
Closes #137
Closes #139
|
|
TerminalsMixin
|
|
|
|
|
|
|
|
.alphanumeric() (#267)
Adds four convenience ASCII character-class shortcuts so callers don't
have to spell out `.range("a","z").range("A","Z")` for the common cases.
## What is new
- **Chain methods** on `ClassesMixin` — `.letter()` → `[a-zA-Z]`,
`.uppercase()` → `[A-Z]`, `.lowercase()` → `[a-z]`, `.alphanumeric()` →
`[a-zA-Z0-9]`.
- **Leaf elements** — `LetterElement`, `UppercaseElement`,
`LowercaseElement`, `AlphanumericElement` in
`edify.elements.types.leaves`, joined into `LeafElement` and `Element`
unions and rendered by `edify.compile.leaves.render_leaf`.
- **Module constants** — `LETTER`, `UPPERCASE`, `LOWERCASE`,
`ALPHANUMERIC` at both `edify.pattern` and the top-level `edify`
namespace.
## Example
```python
from edify import LETTER, ALPHANUMERIC, START, END, one_or_more
username = START + one_or_more(ALPHANUMERIC) + END
LETTER.test("Q") # True
LETTER.test("4") # False
```
Follows the same dedicated-leaf pattern the existing `.digit()` /
`.word()` / `.whitespace_char()` methods use.
Closes #132
|
|
.alphanumeric()
|
|
Adds a varargs shorthand for literal alternation on both fluent surfaces
(`RegexBuilder` and `Pattern`):
- **`.any_of(*literals: str)`** — dual-mode overload. Zero args keeps
the existing frame-opener behavior
(`.any_of().string("cat").string("dog").end()`). One or more string args
skips the frame dance and appends the alternation directly.
- **`.one_of(*literals: str)`** — new method, always the varargs form.
Requires at least one literal (raises `MustBeAtLeastOneLiteralError` on
zero).
Literals go through the same escape + `CharElement`/`StringElement`
branching that `.char()` / `.string()` use, so the compile-path fusion
into `[...]` for single-character sets still applies:
```python
RegexBuilder().any_of("+", "-").to_regex_string() # [\+\-]
RegexBuilder().one_of("cat", "dog", "fish").to_regex_string() # (?:cat|dog|fish)
```
The pre-existing top-level factory `any_of(*operands: Pattern)` from
#263 stays untouched — it takes `Pattern` operands, not literal strings,
and serves the compositional (humre-style) API rather than the
chain-level shorthand this PR delivers.
Closes #131
|
|
|
|
at_least/exactly/between (#265)
Adds `.at_most(n)` on both `RegexBuilder`/`Pattern` and as an
`at_most(n, operand)` factory. Closes the symmetry gap in the quantifier
lineup — we had `exactly(n)`, `at_least(n)`, and `between(lo, hi)`, but
no `{0,n}` shorthand.
## What is new
- `AtMostElement(times, child)` in the elements union, rendered as
`{0,times}` by the quantifier suffix dispatch.
- `.at_most(count)` chain method on `QuantifiersMixin` — validates
`count > 0` (matches the `at_least` contract).
- `at_most(count, operand)` free-function factory in the humre-style
API.
- Re-exported through `edify.pattern` and top-level `edify`.
## Example
```python
from edify import DIGIT, at_most, Pattern
at_most(3, DIGIT).to_regex_string() # \d{0,3}
Pattern().at_most(3).digit().to_regex_string() # \d{0,3}
```
Closes #130
|
|
at_least/exactly/between
|
|
Adds the last convenience terminal from #127: `.test(string) -> bool` on
both `Pattern` and `RegexBuilder`.
## What is new
- `test(string, pos=0, endpos=sys.maxsize) -> bool` on `MatcherMixin` —
returns `True` when the pattern matches anywhere in the input, else
`False`. Semantically `self.to_regex().search(...) is not None`.
## Why
PR #263 already delivered `.match`, `.search`, `.fullmatch`, `.findall`,
`.finditer`, `.sub`, `.subn`, `.split` from the #127 wishlist. `.test` —
a boolean shortcut idiomatic in Ruby/humre/JS — was the only piece
missing.
## Example
```python
from edify import DIGIT, START, END, exactly
zip_code = START + exactly(5, DIGIT) + END
zip_code.test("10001") # True
zip_code.test("nope") # False
```
Closes #127
|
|
|
|
matcher (#263)
Delivers the humre-style parallel API on top of the fluent chain,
closing the operator-algebra epic (#122, #123, #124).
## What is new
- **Operators** — `+` (concatenation) and `|` (alternation) on both
`Pattern` and `RegexBuilder`, preserving anchors and flags from either
operand.
- **Constants (15)** — `START`, `END`, `WORD_BOUNDARY`,
`NON_WORD_BOUNDARY`, `DIGIT`, `NON_DIGIT`, `WORD`, `NON_WORD`,
`WHITESPACE`, `NON_WHITESPACE`, `ANY_CHAR`, `NEW_LINE`,
`CARRIAGE_RETURN`, `TAB`, `NULL_BYTE`.
- **Value factories (7)** — `string`, `char`, `range_of`, `chars`,
`nonchars`, `nonstring`, `nonrange`.
- **Quantifier factories (9)** — `optional`, `zero_or_more` (+ lazy),
`one_or_more` (+ lazy), `exactly`, `at_least`, `between` (+ lazy).
- **Grouping factories (6)** — `group`, `capture`, `named_capture`,
`back_reference`, `named_back_reference`, `any_of` (variadic
alternation).
- **Assertion factories (4)** — `assert_ahead`, `assert_not_ahead`,
`assert_behind`, `assert_not_behind`.
- **Terminals on `Pattern`** — `to_regex_string()` and `to_regex()` now
live on `Pattern` (previously builder-only).
- **Matcher proxies** — `.match()`, `.search()`, `.fullmatch()`,
`.findall()`, `.finditer()`, `.sub()`, `.subn()`, `.split()` on both
`Pattern` and `RegexBuilder`, delegating to `re.Pattern`.
- **New error** — `MustBeAtLeastTwoOperandsError` guards `any_of`.
## What it unlocks
```python
from edify import START, END, DIGIT, exactly
zip_code = START + exactly(5, DIGIT) + END
zip_code.match("10001")
zip_code.fullmatch("100")
```
Zero `Pattern()` boilerplate, full IDE completion, typed exception
surface, and seamless interop with the fluent chain
(`Pattern().exactly(5).digit()`).
Closes #122
Closes #123
Closes #124
|
|
|
|
## 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).
|
|
|
|
floor (#255)
## Summary
Closes #68 — package reorganization to a single-purpose, dir-per-concern
layout (no `src/`).
Closes #69 — toolchain migration to `uv` + `ruff` + `pytest` +
`hatchling` + `hatch-vcs`, driven entirely from `pyproject.toml`.
Closes #253.
Closes #259 — drop `.editorconfig`; LF / trim / final-newline / Python
indent now enforced by `mixed-line-ending` (added in fixup) + existing
pre-commit hooks + `ruff format`.— Python floor bump to 3.11; drops
`py39` / `py310` / `pypy310` from the CI matrix.
The three issues are paired by construction (dropping `src/` requires
the new build backend's package discovery, and the 3.11 floor lets the
new layout use `typing.Self`, frozen dataclasses, `match`/`case`, and
Python-3.11 idioms throughout).
## Highlights
**Layout** — `edify/builder/` (RegexBuilder + 11 mixin files + `types/`
for state/flags/frame/protocol), `edify/elements/types/` (sealed
dataclass union: 16 leaves, 7 char-shaped, 4 captures, 7 groups, 9
quantifiers, RootElement), `edify/compile/` (per-family render functions
+ dispatch, plus `quantifier/` subpackage for suffix + apply),
`edify/validate/`, `edify/errors/` (typed exception hierarchy),
`edify/library/` (per-validator files in family folders: `email/`,
`ip/`, `date/`, plus flat single-validator files), `pattern/`,
`serialize/`, `result/` skeletons.
**Toolchain** — replaces `tox` / `virtualenv` / `flake8` / `isort` /
`black` / `setup.py` / `setup.cfg` / `pytest.ini` / `.coveragerc` /
`.bumpversion.cfg` / `MANIFEST.in` / `ci/` / `tests.local.sh` with
`pyproject.toml`-driven `uv` + `ruff` + `pytest` + `hatchling` +
`hatch-vcs`. CI workflow rewritten to `uv sync --frozen` + `uv run
pytest`; required-status-check names preserved (`check`, `docs`, `py311
(ubuntu)`, `py312 (ubuntu)`, `py313 (ubuntu)`, `py314 (ubuntu)`,
`pypy311 (ubuntu)`). Pre-commit replaced with `ruff` hooks, all hook
repos pinned by commit SHA.
**Tests** — reorganised under `tests/builder/` and
`tests/library/<family>/` using the `*.test.py` naming shape. 173 tests,
100% line coverage, ruff lint + format clean, `uv build` produces sdist
+ wheel cleanly.
## Behaviour changes versus 0.3
These will get their own upgrade-guide entries when the migration
tooling lands (#80):
- `to_regex_string()` now returns the bare pattern (no `/.../` wrap).
- `to_regex()` on a named back-reference compiles successfully via
`(?P=name)` instead of raising.
- Builder errors raise typed `EdifyError` subclasses
(`MustBeAStringError`, `StartInputAlreadyDefinedError`, etc.) instead of
bare `Exception` — catch via `except EdifyError`.
- Internal helpers (`apply_quantifier`, `frame_creating_element`,
`evaluate`, `state`, etc.) removed outright per the 0.3 → 1.0 stub-free
policy.
|
|
|
|
|
|
|
|
|
|
`hatch-vcs` was generating `edify/version.py` at every build with an
unsorted `__all__`, which `ruff check` (RUF022) flagged on CI. The file
is unused — `edify/__init__.py` resolves `__version__` via
`importlib.metadata.version("edify")` directly — so dropping the
generation entirely is the right move. Removed the corresponding
coverage `omit` (no longer needed).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
|
LF / trim-trailing-whitespace / final-newline / Python 4-space indent / UTF-8
guarantees are now enforced by `mixed-line-ending` (added here),
`trailing-whitespace` + `end-of-file-fixer` (already in pre-commit),
and `ruff format` (Python). YAML 2-space indent is the only rule
.editorconfig provided that no other tool enforces; the three YAML files
in the repo are correctly indented and reviewer attention is the gate.
Closes #259
Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
|
Python 3.11 floor
Drops the `src/` layout. The package lives at `edify/` at repo root, split
into single-purpose subpackages: `builder/` (mixins composing RegexBuilder),
`elements/` (sealed dataclass AST), `compile/` (per-family render functions
plus dispatch), `validate/`, `errors/` (typed exception hierarchy), and
`library/` (per-validator files in family folders). Pattern, serialize,
and result subpackages ship as skeletons.
Replaces tox / virtualenv / flake8+isort+black / setup.py+setup.cfg /
pytest.ini / .coveragerc / .bumpversion.cfg / MANIFEST.in / ci/ /
tests.local.sh with a unified uv + ruff + pytest + hatchling + hatch-vcs
stack driven entirely from pyproject.toml. CI workflow rewritten to
`uv sync --frozen` + `uv run pytest` per matrix entry; required-status-
check names preserved for `check`, `docs`, `py3X (ubuntu)`, and
`pypy311 (ubuntu)`. `py39` / `py310` / `pypy310` dropped (3.11 floor).
Pre-commit replaced with ruff hooks (lint + format), all hook repos
SHA-pinned to commit SHAs.
Tests reorganised under `tests/builder/` and `tests/library/<family>/`
with the `*.test.py` naming shape. 173 tests pass, 100% line coverage.
Behaviour changes versus 0.3 (each will get a documented upgrade-guide
entry once #80 lands):
- `to_regex_string()` returns the bare pattern (no `/.../` wrap)
- `to_regex()` on `named_back_reference` compiles successfully via
`(?P=name)` instead of raising
- builder errors raise typed `EdifyError` subclasses instead of bare
`Exception` (catch via `except EdifyError`)
- internal helpers (`apply_quantifier`, `frame_creating_element`,
`evaluate`, `state`, etc.) removed outright per the 0.3 → 1.0
stub-free policy
Closes #68
Closes #69
Closes #253
Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
|
## Summary
Adds `design_docs/` to `.gitignore`. The directory is a local working
space for collaborative design notes between Bobby and Claude (currently
holds the 1.0 design doc) — its contents are intentionally local-only,
not user-facing documentation.
Without this line, the contents are at risk of being accidentally
tracked or committed during routine `git add` operations.
The single commit on this branch (`49297e1`) was authored and GPG-signed
locally; the PR just lands it on `main` via the standard issue+PR flow.
Closes #65
|
|
|