aboutsummaryrefslogtreecommitdiff
AgeCommit message (Collapse)AuthorFilesLines
10 daysMulti-locale phone validator, expanded corpora, and all-validator round-trip ↵HEADmainBobby12-58/+285
+ operator-algebra coverage (#288) Completes the remaining non-docs library items on the v1.0.0 milestone: a genuinely multi-locale `phone` validator, real per-locale corpus hardening, and an end-to-end coverage layer that exercises every shipped validator and the operator algebra. ## Multi-locale phone (#154) `phone` is rebuilt as a table-driven multi-locale model: an optional `+` or `00` prefix, two to eight digit groups of one to four digits each, single-character space/dot/dash separators, and any group optionally parenthesised. It now accepts the display forms it previously missed — leading and inline parenthesised area codes such as `(555) 123-4567` and `+44 (0)20 7946 0958`, and variable national grouping from three-group North American through five-group French — plus two-to-six-digit service and short codes. Doubled separators, bare prefixes, and letters are still rejected. The corpus grows from a seven-string smoke test to twenty-one real numbers across eleven locales with ten adversarial rejects. ## Postal locale coverage (#155) Multi-locale postal codes are delivered by the existing `postal` validator; this pins it down with a twenty-code corpus spanning North America, Europe, Asia, and Oceania (including alphanumeric UK, Irish, Canadian, and Dutch shapes) with ten adversarial rejects. `zip_code` stays deliberately US-scoped, with `postal` as its locale-complete counterpart. ## Validator snapshots (#191) Every per-validator migration to a callable `Pattern` has landed, so regenerating the built-in snapshot corpus is a no-op apart from `phone`'s new emitted shape, which is refreshed here. ## End-to-end coverage of every construct (#258) - Every registered library validator now round-trips through both dict and JSON serialization with its emitted regex preserved — a property test over the full validator set, alongside the existing compiled-source invariant. - The operator algebra (`+`, `|`, and `.use()`) is asserted identical to its fluent-chain equivalent across every character-class constant, at both the emitted-string and compiled-matcher level, so the two surfaces can never drift. - Hardening corpora expand beyond the smoke-test handful to `phone`, `postal`, `semver`, `slug`, `hostname`, `port`, `isbn`, and `vin`, each with real-world accepts and adversarial rejects. Closes #154, Closes #155, Closes #191, Closes #258
10 daysfeat(library): multi-locale phone validator + ↵natsuoto12-58/+285
postal/semver/slug/hostname/port/isbn/vin corpora + all-validator round-trip and operator-algebra equivalence coverage
10 daysMove the public-surface migration gate to its own pull_request workflow (#287)Bobby2-35/+36
The public-surface migration gate needs a pull request's base/head SHAs, so it can't run on a plain push — it evaluates its `if` to false and shows up as a **skipped** check on every push, main included. This moves it out of the shared `[push, pull_request]` workflow into its own `on: pull_request` workflow, so on push runs the job simply doesn't exist and no skipped check appears. On pull requests it runs exactly as before: the bootstrap exemption (skip when `.public-surface` is first introduced) and the `[no-migration]` title escape hatch are both intact.
11 daysci: move the surface migration gate to its own pull_request workflow so it ↵natsuoto2-35/+36
stops showing as a skipped check on pushes
11 daysDeprecation policy, 0.3 → 1.0 upgrade guide, MIT relicense, and ↵Bobby100-894/+2374
codebase-wide strict typing (#286) Ships the deprecation-and-license milestone — a written deprecation policy, the rewritten 0.3 → 1.0 upgrade guide, the relicense to MIT, and the machinery that keeps the public surface and upgrade notes honest — alongside a codebase-wide pass to fully strict static typing. ## Deprecation policy & upgrade guide - A written deprecation policy covering the stub pattern, removal cadence, message format, and the behavior-change rule. - The 0.3 → 1.0 upgrade guide rewritten against the shipped breaking surface, with frozen section anchors so a warning can link straight to its migration note. - `docs/upgrading/` scaffolding wired into the documentation table of contents. - Every deprecated stub carries a warning-assertion harness, and a gate parses each warning's URL to guarantee the anchor it points at exists in the guide. ## Changes fragments & public-surface snapshot - A one-file-per-change `changes/` fragment format plus a small standard-library generator that assembles the changelog and upgrade-guide sections from them. - A committed `.public-surface` snapshot of the exported API, regenerated whenever the surface moves. - A gate that fails a pull request whose public surface shifts without an accompanying `changes/` fragment or an explicit no-migration marker. ## Relicense to MIT - The changelog records the relicense in a single cross-linked entry, and the upgrade guide carries the matching license anchor. ## Trusted Publishing - Releases publish through Trusted Publishing, retiring the long-lived upload token. ## Fully strict typing - The library, tests, and tooling now pass fully strict static type-checking end to end, with zero inline suppressions and zero blanket rule overrides. - Optional third-party integrations that ship no type information are reached through typed boundaries and local stubs, keeping the strict guarantee intact without importing them at module load. - Helper-level tests now exercise their behavior through the public API rather than reaching into private functions, and unreachable defensive branches were retired so every line stays covered. Closes #199, Closes #200, Closes #201, Closes #202, Closes #203, Closes #204, Closes #205, Closes #206, Closes #207, Closes #228, Closes #235
11 daysci: skip the surface migration gate when the surface snapshot is first ↵natsuoto1-0/+4
introduced
11 dayschore: enforce fully strict static typing across library, tests, and tooling ↵natsuoto86-908/+879
with zero suppressions
11 daysfeat(release): changes/ fragment system + generator, .public-surface ↵natsuoto15-18/+1314
snapshot + CI migration gate, deprecation-warning + anchor harnesses, Trusted Publishing, relicense CHANGELOG entry
11 daysdocs(upgrading): rewrite 0.3-to-1.0 guide to the shipped breaking surface ↵natsuoto4-41/+250
with frozen anchors; add upgrading index + deprecation policy
11 daysfeat(integrations): pydantic, fastapi, django integration modules as opt-in ↵Bobby64-217/+1140
extras (#284) Ships the framework-integrations bundle end to end so an :class:`edify.Pattern` can drop straight into the field-validation layers of the three Python frameworks most teams actually use. ## Integration modules - **`edify.integrations.pydantic`** — `pattern_validator(pattern)` returns a Pydantic-compatible validator callable; `pattern_field(pattern)` returns an `Annotated[str, AfterValidator(...)]` type you drop into a `BaseModel` field. - **`edify.integrations.fastapi`** — `pattern_query(pattern, ...)` and `pattern_path(pattern, ...)` return `fastapi.Query` / `fastapi.Path` values pinned to the pattern's emitted regex string, with `default=` and every other FastAPI kwarg forwarded through. - **`edify.integrations.django`** — `pattern_validator(pattern, message=..., code=...)` returns a `django.core.validators.RegexValidator` pinned to the pattern's regex source, with the message default derived from the pattern. Every integration follows the same deferred-import pattern that `edify[regex]` uses: `import edify.integrations.pydantic` (etc.) succeeds without the framework installed; the first helper call resolves the framework lazily and raises `MissingIntegrationDependencyError` (annotated summary + pointer block + `= note:` + `help:` line) telling the caller to `pip install edify[<framework>]`. ## Extras + optional-dependencies - `[project.optional-dependencies]` gains `pydantic = ["pydantic>=2.0"]`, `fastapi = ["fastapi>=0.100"]`, `django = ["django>=4.2"]`, and `all = [everything]`. - The `dev` group installs all three frameworks so local + CI runs cover the integrations without gating on the extras. ## CI install-per-extra verification - New matrix job `install-with-integration-extra` runs one entry per extra (`pydantic`, `fastapi`, `django`, `all`). - Each entry installs into a fresh venv, imports the integration module, and drives a small probe script that verifies the helper accepts a matching value and rejects a non-matching value. - Same shape as the existing `install: edify[regex]` verification — any drift in the `[project.optional-dependencies]` declarations fails loudly. ## Sphinx autodoc - New `docs/integrations/index.rst` page with `.. automodule::` blocks for each of the three integration modules. - Wired into `docs/index.rst` toctree. - `.readthedocs.yml` installs `.[all]` so autodoc always has every framework annotation available to resolve. Closes #220, closes #221, closes #222, closes #223, closes #224, closes #225, closes #226, closes #227.
11 daystest(serialize): cover the composite-value branch of _require_schema_version ↵natsuoto1-0/+6
so PyPy and every CPython matrix entry reaches 100%
12 dayschore: codebase-wide rule audit — eliminate Any/object where non-protocol, ↵natsuoto59-432/+534
hoist in-function imports, rename compound/folder-repeat files, replace generic-exception raises, bind chained comprehensions, strip __init__ docstrings, remove pragmas
12 daysfeat(integrations): edify[pydantic], edify[fastapi], edify[django] with ↵natsuoto14-1/+816
deferred imports + Sphinx autodoc section + per-extra CI install verification
12 daysfeat(library): new ipv4/ipv6/iso_date/zip_code/email_rfc_5322 validators + ↵Bobby103-67/+673
real-world corpus hardening + guarantee docstrings (#283) Ships the validator-hardening epic and splits the bench-baseline regenerate job into its own workflow. ## New validators - **`ipv4`** — strict IPv4 dotted-quad. Splits the shape out of the unified `ip` for callers who need v4-only. - **`ipv6`** — every documented IPv6 form: full 8-group, `::`-compressed, link-local `%zone`, IPv4-mapped `::ffff:1.2.3.4`, and the hybrid IPv4-suffix. Splits the shape out of `ip` the same way. - **`iso_date`** — strict ISO 8601 date shape `YYYY-MM-DD`. Distinct from the existing `datetime` (which includes a time component) and the loose `date` (which accepts many separators). - **`zip_code`** — US ZIP or ZIP+4 postal-code shape. - **`email_rfc_5322`** — the strict RFC 5322 mailbox subset already inside `email`, exposed as its own callable so downstream callers can pick permissive vs strict semantics without composing themselves. ## Real-world corpus hardening - **`tests/library/corpora/<name>.toml`** — per-validator TOML files with `accepts` and `rejects` lists (233 cases across 14 validators). Adding a new validator just means dropping in a new TOML. - **`tests/library/hardening.test.py`** — parametrized test that walks every corpus file, resolves the validator from `edify.library`, and asserts the boolean verdict matches the expected side. `pytest -k hardening` runs the whole corpus in under half a second. - Coverage: `email`, `email_rfc_5322`, `url`, `ipv4`, `ipv6`, `mac`, `uuid`, `guid`, `ssn`, `date`, `iso_date`, `phone`, `zip_code`, `password`. ## Guarantee documentation pass - Every hardened validator's module docstring now spells out what it does and does not guarantee — shape vs semantics, anchoring, case sensitivity, structural exclusions. Callers no longer have to grep the regex or read the source to know whether `mac(...)` accepts EUI-64 or whether `iso_date(...)` validates calendar correctness. - The `password` validator gets a first-class guarantee block covering the default policy (min length, class thresholds, special-char set) and the passphrase-strength and non-ASCII limits it deliberately does not check. ## CI hygiene — split bench-baseline job - The `bench: regenerate runner baseline` job moves out of `build` into its own `.github/workflows/bench-baseline.yml`. It only registers when workflow_dispatch fires with a typed `YES` confirmation. - Every push and pull_request stops surfacing the two skipped baseline-regen checks that were appearing on every commit. - The runtime behavior is unchanged: baselines are still runner-generated and committed back with `contents: write` permission. Closes #78, closes #152, closes #153, closes #156, closes #157, closes #158, closes #159, closes #160, closes #161, closes #162, closes #163, closes #164, closes #165, closes #166, closes #167, closes #168, closes #169.
12 daysfeat(library): ipv4/ipv6/email_rfc_5322/iso_date/zip_code validators + ↵natsuoto101-28/+635
real-world corpus hardening across 14 patterns + guarantee docstrings
12 dayschore(ci): move bench-baseline regenerate job to its own workflow so it ↵natsuoto2-39/+38
stops showing as a skipped check on every push
12 daysfeat(ci): Hypothesis property tests, coverage discipline, bench suite + ↵Bobby17-3/+693
advisory CI job, Codecov PR comments (#282) Ships the CI quality-gates epic end to end. ## Coverage discipline - **Global coverage enforcement.** Adds `--cov=edify --cov-fail-under=100` to `[tool.pytest.ini_options].addopts` so every pytest run (local and CI) enforces the gate — no separate matrix entry, no way to accidentally run the suite without measuring coverage. - **`exclude_also`** covers `if TYPE_CHECKING:`, `@overload`, and `raise NotImplementedError` — the three shapes where coverage-target lines are truly unreachable. - **Anti-vacuous-green test.** `tests/discipline/coverage.test.py` asserts `[tool.coverage.run].source` resolves to exactly `["edify"]` and that the tree contains real non-`__init__` source files. Catches the source-path misconfiguration that makes the gate pass green over zero measured lines. - **`# pragma: no cover` discipline.** `tests/discipline/pragmas.test.py` uses `tokenize` to walk every Python file, finds every pragma comment, and fails CI if any lacks an inline reason after the pragma text. File-level pragmas are banned outright. ## Hypothesis harness + property tests - **Conftest profiles.** `tests/conftest.py` registers `dev` (50 examples, default), `ci` (300 examples), and `nightly` (2000 examples with verbose shrinker), selected via `EDIFY_HYPOTHESIS_PROFILE`. - **`tests/property/emitted.test.py`** — Property: emitted regex matches a hand-rolled reference for `.string()`, `.any_of_chars()`, and `.exactly(n)` over digit/word/letter across the alphabet. - **`tests/property/roundtrip.test.py`** — Property: `Pattern.from_dict(pattern.to_dict())` and `Pattern.from_json(pattern.to_json())` roundtrip every generated pattern (recursive strategy covering strings, character classes, capture / group / one_or_more / optional wrappers). - **`tests/property/quantifiers.test.py`** — Property: every quantifier chain call produces exactly one quantifier suffix in the emitted pattern — no chain silently drops a quantifier. - **`tests/property/parsing.test.py`** — Property: `RegexBuilder.from_regex(source).to_regex_string()` roundtrips behaviorally — the rebuilt chain matches the same corpus as the original raw regex. ## Benchmarks - **`tests/bench/` scaffolding** with `[dependency-groups] bench = ["pytest-benchmark>=4.0"]` — no runtime deps added, and `--ignore=tests/bench` in addopts so contributors without the bench group don't hit unrecognized-option errors. - **Case suite.** Simple (`digit().one_or_more()`), medium (hex-number-class with anchors + bounded quantifier), complex (composed URL shape with nested groups + lookaround + alternation over multi-char literals). Each case has a matches / near-matches / non-matches corpus so the match hot path is covered end to end. - **Compile + match benches.** Two files parametrize the case suite across `to_regex()` / `to_regex_string()` and across the three match-kind corpora. - **Cold/warm ratio gate** (`tests/builder/cold_warm.test.py`) — asserts the warm `.to_regex()` call is at least 10× cheaper than the cold call. Hardware-independent ratio, deterministic blocking signal for the item-41A lazy compile cache. ## CI wiring - **Bench job** — advisory-only, runs `pytest tests/bench/` with `|| true` against a committed `tests/bench/baseline.json` when it exists. Never a required status check. - **`workflow_dispatch` baseline regeneration** — a manual-dispatch job (with `regenerate_bench_baseline: true` input) runs `pytest --benchmark-json=tests/bench/baseline.json` on the runner and commits it back to the branch. Baselines are always runner-generated, never local. - **Codecov PR comments** — `codecov.yml` at the repo root enables `comment.behavior: default` with the diff / flags / files layout so every PR shows the coverage delta in a review comment. Project + patch statuses both target 100% with zero threshold. Closes #83, closes #198, closes #238, closes #239, closes #240, closes #241, closes #242, closes #243, closes #244, closes #245, closes #246, closes #247, closes #248, closes #249, closes #250, closes #251.
12 daysfix(ci): pragma-out regex.DEBUG branch — PyPy's disassembler crashes and ↵natsuoto1-1/+1
no end-to-end test can reach it there
12 dayschore(bench): tests/bench scaffolding + case suite + cold/warm ratio gate + ↵natsuoto9-1/+298
advisory CI job + workflow_dispatch baseline + codecov PR comments
12 daystest(property): Hypothesis harness + property tests for builder emission, ↵natsuoto5-0/+291
roundtrips, quantifier count parity, reverse parser
12 dayschore(coverage): exclude_also + global --cov=edify addopts + anti-vacuous ↵natsuoto3-1/+103
and pragma discipline tests
12 daysfeat: snapshot suite, char-class normalization, ReDoS detector, and ↵Bobby320-58/+1529
structured open-frame errors (#281) Ships the compile-path correctness epic + the build-time validation epic end to end. ## Snapshot suite - **Snapshot helper.** `edify/testing/snapshots.py` — an ~80-line difflib-based helper (`assert_snapshot(actual, path)` + `SnapshotMismatchError` / `SnapshotMissingError`) with `EDIFY_UPDATE_SNAPSHOTS=1` regenerate mode. Publicly re-exported through `edify.testing`. - **Library snapshots.** Every one of the 223 public library validators emits a committed `.regex` snapshot under `tests/snapshots/library/`. Any compile-path change that touches the emitted form surfaces here first. - **Doc snapshots.** Every `.. code-block:: python` block across `docs/` is exec-ed in a fresh edify-populated namespace and its resulting `Regex` / `Pattern` bodies are snapshotted under `tests/snapshots/docs/<file-stem>/<line>.regex`. Broken blocks pinned to planned validators (`email_rfc_5322`, `ipv4`, `ipv6`, `iso_date`, `zip`) are explicitly deferred to the docs rewrite (epic #81). - **Edge-case fixtures.** Deeply-nested alternation, combined lookaround, item-4 named-backref, item-6 ReDoS-flagged constructs, hex/email/composition — 8 fixtures under `tests/snapshots/fixtures/`. A parallel test asserts the ReDoS warning fires for the nested-quantifier fixture. - **Orphan detection.** A discipline test walks all three snapshot roots and fails CI if any snapshot has no live source (library Pattern / registered fixture / discovered code-block). The reverse direction (source without snapshot) is caught by `SnapshotMissingError` in the per-source test. ## Compile-path invariants - **Insertion-order determinism** pinned: `any_of_chars("zyxabc321")` emits `[zyxabc321]` deterministically across runs; a discipline test defends against future set/dict-backed rewrites. - **`to_regex_string() == to_regex().pattern`** pinned per registered library Pattern (223 parametrized checks). Item 5's regex-module backend was the change most likely to silently split these — the invariant now holds them together. ## Char-class escaping normalization (breaking) - Introduces `edify.compile.escape.escape_for_char_class(value)` — the minimal correct form. Inside `[...]`, only `\`, `]`, `^` (first position), and `-` (interior position) are escaped; everything else is a literal. - `any_of_chars` and `anything_but_chars` now use the minimal escaper; `string`, `char`, `anything_but_string` keep the full `re.escape` since they live outside a class. - **Before:** `any_of_chars('#?!@$%^&*-')` → `[\#\?!@$%\^\&\*\-]`. **After:** `[#?!@$%^&*-]`. Match behavior is identical (equivalence corpus test asserts this). ## AST walker substrate + ReDoS detector - **`edify.elements.walk.walk_elements`** — the shared depth-first walker that yields every `BaseElement` in an emission-order tree. Serialize already had one; extracted, generalized, and reused by ReDoS. - **Build-time ReDoS detector** (`edify.compile.redos.warn_on_redos_constructs`) — walks the element tree at `to_regex()` time and emits `ReDoSWarning` when it finds the classic `(x+)+` shape (an unbounded quantifier wrapping a single-child group whose one child is itself an unbounded quantifier). The check is intentionally conservative — it requires the group's children tuple to have length one, so composite patterns like `(?:X)*(?:Y*)*` aren't false-positived. ## Structured open-frame stack in error messages - `CannotCallSubexpressionError.__init__(open_frames: tuple[OpenFrameInfo, ...])` — lists every open frame innermost-first, one line per frame, with its opener call site (`filename:line:col`). Named captures reproduce the declared name (`.named_capture("username")`). - `DanglingQuantifierError.__init__(pending_quantifier_name, pending_quantifier_call_site)` — names the specific queued quantifier (`.exactly(3)`) and points at its call site. - `edify.builder.diagnose.describe_open_frames(state)` — the shared helper both terminals and subexpression use. ## Breaking changes - `any_of_chars` and `anything_but_chars` emit the minimal-correct form, not the over-escaped form. Match behavior is unchanged (equivalence corpus test verifies), but any caller comparing the emitted string against a hard-coded expected value with `\.`/`\-` etc. needs to drop the redundant backslashes. - `CannotCallSubexpressionError.__init__` now takes `tuple[OpenFrameInfo, ...]` instead of a bare string. Direct constructors need updating. - `DanglingQuantifierError.__init__` accepts optional `pending_quantifier_name` / `pending_quantifier_call_site` kwargs; the bare `DanglingQuantifierError()` call still works. Closes #72, closes #98, closes #99, closes #100, closes #101, closes #102, closes #103, closes #104, closes #105, closes #106, closes #73, closes #107, closes #114, closes #192.
12 daysfix(ci): skip re.DEBUG doc block on PyPy — hits the same upstream ↵natsuoto1-0/+11
disassembler bug
12 daysfeat(compile): AST-walker ReDoS detection for the (x+)+ shape emits ↵natsuoto4-1/+180
ReDoSWarning at build time
12 daysfeat(errors): structured open-frame stack with per-frame call sites; ↵natsuoto7-22/+225
DanglingQuantifierError names the pending quantifier
12 daystest(discipline): orphan detection for library/fixture/doc snapshot rootsnatsuoto1-0/+131
12 daystest: snapshot deeply-nested alternation, combined lookaround, ↵natsuoto10-3/+135
named-backref, and ReDoS edge fixtures
12 daystest: snapshot every executable doc code-block; pin the docs against silent ↵natsuoto65-0/+131
regex drift
12 daystest: snapshot suite for every public library validator (223 patterns)natsuoto225-2/+259
12 daysfeat!(compile): normalize char-class escaping to minimal correct form; add ↵natsuoto5-10/+176
equivalence corpus
12 daystest: pin char-class insertion-order and to_regex_string() == ↵natsuoto2-0/+55
to_regex().pattern invariants
12 daysfeat(testing): difflib-based snapshot helper with EDIFY_UPDATE_SNAPSHOTS=1 ↵natsuoto3-0/+184
regenerate mode
12 daysrefactor(elements): extract shared walk_elements substrate; consume from ↵natsuoto2-25/+47
serialize/load
12 daysfeat: optional regex engine, per-instance compile cache, testing helpers, ↵Bobby36-216/+1731
and reverse parser (#280) Ships the opt-in regex-engine backend and the builder-ergonomics polish end to end. ## Optional regex-engine backend - Introduces `edify/compile/backend.py` — a small backend layer that owns the "compile a rendered pattern string with these flags" operation per engine. - `edify.RegexBuilder().*.to_regex(engine="re" | "regex")` now actually routes: `"re"` uses the stdlib :mod:`re` module (default, unchanged); `"regex"` uses the third-party `regex` module. - The `regex` module is a **deferred import** — `import edify` never imports it. When the caller selects `engine="regex"` without the extra installed, they get a clean, annotated `MissingRegexBackendError` telling them to `pip install edify[regex]`, not a `ModuleNotFoundError` deep in a traceback. - Under `engine="regex"`, **variable-width lookbehind** compiles cleanly (`assert_behind` bodies with quantifiers like `.between(1, 3).string("foo")` no longer fail). Under `engine="re"`, the compile path catches stdlib re's cryptic "look-behind requires fixed-width pattern" and re-raises `VariableWidthLookbehindNotSupportedError` — an annotated message that names the construct and points the caller at `engine="regex"`. - Every match method on `Regex` (`match`, `search`, `fullmatch`, `findall`, `finditer`, `sub`, `subn`, `split`) accepts a per-call `timeout=` kwarg. Under `engine="regex"` the value flows through to the underlying pattern; under `engine="re"` it raises `TimeoutNotSupportedByEngineError`. - Two new CI jobs — one installs edify without the extra and verifies both the friendly ImportError path and the re engine's happy path; the other installs `edify[regex]` and verifies both engines compile plus the variable-width-lookbehind fixture only compiles under `engine="regex"`. ## Builder ergonomics - **Per-instance lazy compile cache.** The first no-kwargs `.to_regex()` (or match verb) compiles once and caches the resulting `Regex`; every subsequent no-kwargs call returns the same object (`builder.to_regex() is builder.to_regex()`). A chain step or `fork()` produces a fresh builder with its own empty cache. Kwarg calls bypass the cache and always compile fresh. - **Match-verb surface closed at five.** `MatcherMixin` now exposes exactly `test` / `match` / `search` / `findall` / `sub` on the builder. The four less-common verbs (`fullmatch`, `finditer`, `subn`, `split`) are reached through `.to_regex()` on the `Regex` wrapper. A discipline test asserts the closed surface and the absence of every other `re.Pattern` attribute (`groups`, `groupindex`, `pattern`, `flags`). - **`Pattern.__call__` delegates to `.test`** — so a `Pattern` doubles as a validator callable (`email(value) -> bool`) without an intermediate `.test` step. - **Testing helpers.** `.assert_matches([...])` and `.assert_rejects([...])` raise annotated `PatternDidNotMatchInputsError` / `PatternMatchedRejectedInputsError` (both subclasses of `AssertionError` so pytest introspects them exactly like a bare `assert`). - **Match wrapper.** Every match verb now returns an edify `Match` that exposes named captures as attributes — `m.username` and `m.captures.username` — while delegating everything else (`group()`, `groupdict()`, `span()`, ...) to the underlying `re.Match` via `__getattr__`. `re.Pattern.sub`/`subn` callables receive the wrapped `Match`. - **`RegexBuilder.from_regex(pattern)` reverse parser.** Converts a raw regex string back into a builder chain via `re._parser.parse`, covering the common construct set (literals, character classes with ranges/categories, quantifiers greedy + lazy including `?`/`*`/`+`/`{n}`/`{m,n}`, capture / named-capture / non-capturing groups, all four lookarounds, alternation over literals, anchors, categories). Anything unrecognized raises the annotated `UnsupportedReverseParseError`. - **Per-call allocation win.** Cached `os.path.abspath` for caller-context filenames, cached `co_positions()` per code object, and moved `source_line` reading behind a lazy property on `CallerContext`. Chain-step allocation is ~4.6× faster on a 300-step build/compile microbenchmark (9.4s → 2.0s / 1000 iterations). - **Immutability contract documented.** Dedicated section in the API reference + one-liner in the README explaining that every chain method returns a new builder, that branching (`base.exactly(2).digit()` vs `base.end_of_input()`) is always safe, and that repeat `.to_regex()` returns the cached instance. ## Breaking changes - `MatcherMixin` no longer exposes `fullmatch`, `finditer`, `subn`, `split` on the builder — call `.to_regex()` first and use them on the returned `Regex`. - `Regex.match` / `.search` / `.fullmatch` and `Regex.finditer` now return the edify `Match` wrapper (or an iterator of it) rather than a bare `re.Match`. Existing code that calls `.group(...)`, `.groupdict()`, `.span(...)`, etc. keeps working via `__getattr__` delegation. - `EngineNotWiredError` is deleted; both engines are wired now, so the class had no live callers. Closes #74, closes #115, closes #116, closes #117, closes #118, closes #119, closes #120, closes #121, closes #129, closes #138, closes #140, closes #141, closes #142, closes #143, closes #144, closes #149, closes #150, closes #151, closes #175.
12 daysci: end-to-end install verification for edify and edify[regex]natsuoto1-1/+73
12 daysdocs(builder): document the immutability contract with branchability example ↵natsuoto3-1/+37
in the API reference and README
12 daysfeat(builder): RegexBuilder.from_regex reverse parser for the common ↵natsuoto4-0/+359
construct set
12 daysfeat(result): Match wrapper exposes named captures as attributes ↵natsuoto5-22/+220
(m.username, m.captures.username)
12 daysfeat(builder): first-class .assert_matches and .assert_rejects helpers for ↵natsuoto5-0/+169
pattern authors
12 daysfeat(pattern): Pattern.__call__ delegates to .test so validators double as ↵natsuoto4-12/+36
callables
12 daysperf(builder): lazy source_line and cached co_positions/abspath cut ↵natsuoto4-22/+60
chain-step allocation by ~4.6x
12 daysfeat!(builder): close the match-verb surface at ↵natsuoto4-83/+58
test/match/search/findall/sub; reach fullmatch/finditer/subn/split via .to_regex()
12 daysfeat(builder): per-instance lazy compile cache shared across to_regex and ↵natsuoto3-13/+80
the matcher methods
12 daysfeat(engine): pass-through timeout= kwarg on every match method under ↵natsuoto4-29/+255
engine=regex
12 daysfeat(engine): unlock variable-width lookbehind under engine=regex; annotate ↵natsuoto3-3/+96
the re-engine failure with an actionable error
12 daysfeat(engine): route to_regex(engine=re|regex) through a backend layer; regex ↵natsuoto7-90/+234
is a deferred-import extra
12 dayschore(deps): expose regex module via [project.optional-dependencies].regexnatsuoto2-2/+116
12 daysfeat(typing): PEP 561 marker, strict-mode gates, and full public-surface ↵Bobby33-241/+482
annotations (#279) Ships the static-typing surface end to end. - Installs the `edify/py.typed` marker so downstream code sees `edify` as a typed distribution, and force-includes it in the wheel. - Adds a `[tool.mypy]` block that runs `mypy --strict` against `edify.*` (with `ignore_missing_imports` scoped to `regex` and `graphviz`), and a `[tool.pyright]` block with `typeCheckingMode = "strict"` bound to `include = ["edify"]`. - Pins `mypy==1.13.0` and `pyright==1.1.389` in the dev group and refreshes `uv.lock`. - Annotates the full public surface: `Self` on every fluent chain method, `TypeVar`-generic private helpers across the mixins, precise element unions on `render_element` / `merge` / `fuse`, `FrameType` on the caller-context walker, a `JSONValue` recursive type over the serialize layer (no `Any`, no `object`), and precise dict types on the `class_for` / `kind_for` registry. - Introduces a typed `engine: Literal["re", "regex"]` kwarg on `to_regex`, with `EngineNotWiredError` (annotated summary + pointer block + `= note:` + `help:`) surfacing when a caller asks for an engine that is not wired in this build. - Introduces `edify/builder/types/protocol.py::BuilderProtocol` — a `runtime_checkable` `Protocol` that declares the shared attribute + method surface every mixin assumes about `self`, so mixins in isolation see the full chain surface. - Introduces `edify/serialize/types.py::JSONValue` (recursive: `str | int | float | bool | None | list[JSONValue] | dict[str, JSONValue]`) and threads it through `element_to_dict`, `state_to_dict`, and the load path (now returning `BuilderState` to break a circular import with `Pattern`). - Adds a discipline test that grep-fails on any bare `# type: ignore` or `# pyright: ignore` in `edify/`, so every suppression must carry a code and a reason (there are none in this branch). - Wires `mypy` and `pyright` as required CI matrix entries in `.github/workflows/github-actions.yml`, both running `--strict` against `edify/`. Closes #71, closes #84, closes #85, closes #86, closes #87, closes #88, closes #89, closes #90, closes #91, closes #92, closes #93, closes #94, closes #95.
12 dayschore(typing): drop pyright executionEnvironments; hoist library-Unknown ↵natsuoto1-9/+1
overrides to top level
12 daysci: mypy --strict and pyright --strict gates on edify/natsuoto1-0/+8