diff options
| author | Bobby <[email protected]> | 2026-07-11 17:44:20 +0530 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-07-11 17:44:20 +0530 |
| commit | abdaeb4aad4284986f012efcce35a6e4189f2929 (patch) | |
| tree | 9eaff3968954dc8f5bf079684dba46b6026e2221 /tests/builder | |
| parent | 2038ee5c4a5e473c20cf1e38af5c32ab450a2aea (diff) | |
| parent | c7a8a902eb86dfec5dd5c4ec77c532f5b920d3ac (diff) | |
| download | edify-abdaeb4aad4284986f012efcce35a6e4189f2929.tar.xz edify-abdaeb4aad4284986f012efcce35a6e4189f2929.zip | |
feat!: callable-Pattern validator API + 223-pattern library across 22 category folders (#274)
Ships the callable-`Pattern` validator API and expands the built-in
library from 14 validators to **223**, organized into 22 category
folders.
## Callable-`Pattern` contract
Every built-in validator is now a `Pattern` instance whose `__call__`
returns `bool`. The old function-style `validator(value)` call form
still works; every `Pattern` method is now also available on each
validator (`match`, `to_regex_string`, `.explain()`, `.visualize()`,
etc.).
```python
from edify import Pattern
from edify.library import uuid, email
isinstance(uuid, Pattern) # True
uuid("550e8400-e29b-11d4-a716-446655440000") # True
uuid.match("550e8400-e29b-11d4-a716-446655440000") # <re.Match ...>
email("[email protected]", strict=True) # available on parameterized validators
```
Implementation:
- `Pattern.__call__(value) -> bool` on the base class (non-string
returns `False`, no crash on user input).
- **Every validator is built via the fluent chain** —
`Pattern().start_of_input().exactly(8).any_of()....end_of_input()`. No
raw-regex escape hatch anywhere in the library.
- Composable atoms in `edify/library/_support/atoms.py` (`hex_any`,
`octet`, etc.) keep the chains readable across categories.
## The 223 patterns across 22 categories
| Category | Count | Patterns |
|---|---|---|
| `identifier/` | 26 | uuid, guid, mac, ssn, ein, tin, itin, iban, bic,
lei, isin, cusip, sedol, sku, asin, imei, meid, iccid, vin, imo, mmsi,
iata, icao, arn, orcid, did |
| `data/` | 14 | xml, json, yaml, toml, csv, tsv, ini, avro, protobuf,
msgpack, html, hdf5, parquet, orc |
| `address/` | 13 | ip, cidr, subnet, hostname, domain, subdomain, tld,
url, uri, path, port, socket, ptr |
| `software/` | 13 | semver, version, package, docker, image, digest,
git, ref, checksum, component, makefile, cargo, bump |
| `api/` | 12 | oauth, openid, saml, openapi, swagger, jsonapi, hal,
graphql, soap, webhook, rss, atom |
| `auth/` | 19 | password, pin, token, jwt, otp, apikey, bearer,
session, csrf, mnemonic, hmac, passkey, mfa, webauthn, secret, sso,
refresh, signing, challenge |
| `temporal/` | 11 | date, time, datetime, duration, timezone, offset,
epoch, timestamp, year, cron, interval |
| `text/` | 11 | slug, base, script, ascii, printable, alphanumeric,
alpha, numeric, word, unicode, emoji |
| `media/` | 11 | mimetype, extension, filename, glob, regex, shebang,
encoding, charset, locale, favicon, codec |
| `document/` | 11 | svg, pdf, epub, mobi, docx, xlsx, pptx, odt,
readme, rtf, tex |
| `security/` | 11 | pgp, pem, der, csr, x509, certificate, ssh, age,
keyring, nonce, signature |
| `web/` | 11 | sitemap, captcha, robots, manifest, htaccess, humans,
nginx, apache, cookie, csp, useragent |
| `numeric/` | 10 | number, integer, hash, percentage, ratio, fraction,
scientific, roman, ordinal, natural |
| `geo/` | 8 | coordinate, geohash, plus, postal, place, altitude, mgrs,
bearing |
| `contact/` | 7 | email, phone, fax, pager, address, username, handle |
| `financial/` | 7 | currency, card, wallet, crypto, routing, sortcode,
vat |
| `publishing/` | 6 | isbn, issn, doi, arxiv, pmid, pmc |
| `grammar/` | 6 | bnf, ebnf, abnf, peg, pest, antlr |
| `color/` | 5 | color, palette, swatch, gradient, filter |
| `transport/` | 4 | vehicle, aircraft, flight, plate |
| `medical/` | 4 | medical, blood, dosage, dicom |
| `product/` | 3 | gtin, mpn, barcode |
| **Total** | **223** | |
Each pattern is importable both from its category submodule and from the
flat top-level namespace:
```python
from edify.library import uuid # flat
from edify.library.identifier import uuid # category
```
## Merged and renamed 0.3 validators
Per the \"each pattern is unique / everything doable via a form must be
merged\" rule:
- `iso_date` → `date` (accepts every recognised date form)
- `email_rfc_5322` → `email` (accepts basic or RFC 5322)
- `ipv4` + `ipv6` → `ip`
- `phone_number` → `phone`
- `zip` → `postal`
Documented in \`docs/upgrading/0.3-to-1.0.rst\` under the \`..
_validators-callable:\` anchor.
Closes #176
Closes #177
Closes #178
Closes #179
Closes #180
Closes #181
Closes #182
Closes #183
Closes #184
Closes #185
Closes #186
Closes #187
Closes #188
Closes #189
Closes #190
Diffstat (limited to 'tests/builder')
| -rw-r--r-- | tests/builder/cache.test.py | 69 | ||||
| -rw-r--r-- | tests/builder/diagnose.test.py | 30 | ||||
| -rw-r--r-- | tests/builder/equality.test.py | 8 | ||||
| -rw-r--r-- | tests/builder/frame.test.py | 17 | ||||
| -rw-r--r-- | tests/builder/passthrough.test.py | 12 |
5 files changed, 11 insertions, 125 deletions
diff --git a/tests/builder/cache.test.py b/tests/builder/cache.test.py deleted file mode 100644 index 80862d3..0000000 --- a/tests/builder/cache.test.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Tests for the lazy :class:`Regex` cache on :class:`BuilderCore`.""" - -from edify import Pattern, RegexBuilder - - -def test_repeated_lazy_regex_calls_return_the_same_instance(): - builder = RegexBuilder().digit() - first = builder._lazy_regex() - second = builder._lazy_regex() - third = builder._lazy_regex() - assert first is second - assert second is third - - -def test_repeated_matcher_calls_reuse_the_cached_regex(): - builder = RegexBuilder().digit() - builder.match("1") - cached = builder._cached_regex - builder.search("2") - builder.findall("3") - builder.test("4") - assert builder._cached_regex is cached - - -def test_pattern_lazy_regex_is_memoised_too(): - pattern = Pattern().word() - first = pattern._lazy_regex() - second = pattern._lazy_regex() - assert first is second - - -def test_a_forked_builder_gets_a_fresh_cache_slot(): - original = RegexBuilder().digit() - original.match("1") - assert original._cached_regex is not None - forked = original.fork() - assert forked._cached_regex is None - - -def test_a_copied_builder_gets_a_fresh_cache_slot(): - original = RegexBuilder().digit() - original.match("1") - copied = original.copy() - assert copied._cached_regex is None - - -def test_a_chain_extension_gets_a_fresh_cache_slot(): - original = RegexBuilder().digit() - original.match("1") - extended = original.word() - assert extended._cached_regex is None - - -def test_a_fresh_builder_starts_without_a_cached_regex(): - builder = RegexBuilder() - assert builder._cached_regex is None - - -def test_calling_to_regex_directly_does_not_populate_the_cache(): - builder = RegexBuilder().digit() - _ = builder.to_regex() - assert builder._cached_regex is None - - -def test_lazy_regex_produces_a_regex_that_matches_the_pattern(): - builder = RegexBuilder().digit() - result = builder._lazy_regex() - assert result.source == "\\d" - assert result.match("7") is not None diff --git a/tests/builder/diagnose.test.py b/tests/builder/diagnose.test.py index c61b3a9..88c4f03 100644 --- a/tests/builder/diagnose.test.py +++ b/tests/builder/diagnose.test.py @@ -1,11 +1,8 @@ """Tests for the diagnostic helpers in :mod:`edify.builder.diagnose`.""" -from types import SimpleNamespace - import pytest from edify import Pattern, RegexBuilder -from edify.builder.diagnose import _fallback, _frame_display_name, diagnose_unfinished from edify.errors.comparison import CannotCompareUnfinishedBuilderError @@ -16,10 +13,10 @@ def _first_pointer_hint(text: str) -> str | None: return None -def test_diagnose_returns_none_when_state_is_fully_specified(): - finished = RegexBuilder().digit() - problem = diagnose_unfinished(finished._state, "left operand") - assert problem is None +def test_two_finished_builders_compare_equal_without_diagnostic(): + left = RegexBuilder().digit() + right = RegexBuilder().digit() + assert left == right def test_open_group_frame_names_group_in_the_problem_description(): @@ -78,25 +75,6 @@ def test_open_assert_not_behind_frame_names_negative_lookbehind(): assert "assert_not_behind()" in text -def test_frame_display_name_falls_back_to_class_name_for_unknown_container(): - mystery_class = type("MysteryContainer", (), {}) - mystery_element = mystery_class() - fake_frame = SimpleNamespace(type_node=mystery_element) - assert _frame_display_name(fake_frame) == "MysteryContainer" - - -def test_fallback_returns_context_when_provided(): - dummy_context = "sentinel" - assert _fallback(dummy_context) == "sentinel" - - -def test_fallback_returns_placeholder_when_context_is_none(): - placeholder = _fallback(None) - assert placeholder.filename == "<unknown>" - assert placeholder.lineno == 0 - assert placeholder.source_line == "" - - def test_dangling_quantifier_on_pattern_reports_quantifier_name(): unfinished_pattern = Pattern().one_or_more() with pytest.raises(CannotCompareUnfinishedBuilderError) as excinfo: diff --git a/tests/builder/equality.test.py b/tests/builder/equality.test.py index 8482b51..f02eddc 100644 --- a/tests/builder/equality.test.py +++ b/tests/builder/equality.test.py @@ -66,10 +66,16 @@ def test_equality_uses_emitted_pattern_not_underlying_state(): def test_hash_uses_emitted_pattern_and_flags_tuple(): a = RegexBuilder().string("hi") b = RegexBuilder().string("hi") - assert hash(a) == hash((a.to_regex_string(), a._state.flags)) assert hash(a) == hash(b) +def test_hash_differs_when_flags_differ_but_pattern_matches(): + without_flag = RegexBuilder().string("hi") + with_flag = RegexBuilder().ignore_case().string("hi") + assert without_flag.to_regex_string() == with_flag.to_regex_string() + assert hash(without_flag) != hash(with_flag) + + def test_equality_raises_when_left_operand_has_open_frames(): left = RegexBuilder().any_of() right = RegexBuilder().digit() diff --git a/tests/builder/frame.test.py b/tests/builder/frame.test.py deleted file mode 100644 index ffd2242..0000000 --- a/tests/builder/frame.test.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Test for the ``UnexpectedFrameTypeError`` raise when an AST is built incorrectly.""" - -import pytest - -from edify import RegexBuilder -from edify.builder.types.frame import StackFrame -from edify.elements.types.leaves import DigitElement -from edify.errors.internal import UnexpectedFrameTypeError - - -def test_end_on_unrecognised_frame_type_raises(): - builder = RegexBuilder().capture() - stray_frame = StackFrame(type_node=DigitElement()) - new_state = builder._state.with_frame_pushed(stray_frame) - builder_with_stray = builder._with_state(new_state) - with pytest.raises(UnexpectedFrameTypeError, match="DigitElement"): - builder_with_stray.end() diff --git a/tests/builder/passthrough.test.py b/tests/builder/passthrough.test.py index de45146..a2e77b5 100644 --- a/tests/builder/passthrough.test.py +++ b/tests/builder/passthrough.test.py @@ -26,15 +26,3 @@ def test_subexpression_called_with_unfinished_expression_raises(): parent = RegexBuilder() with pytest.raises(CannotCallSubexpressionError): parent.subexpression(unfinished_sub) - - -def test_to_regex_with_invalid_pattern_raises_failed_to_compile(): - import pytest - - from edify.errors.internal import FailedToCompileRegexError - - expr = RegexBuilder().capture().digit().end().back_reference(1) - state_with_extra = expr._state.with_capture_groups_added(98) - bogus = expr._with_state(state_with_extra).back_reference(99) - with pytest.raises(FailedToCompileRegexError): - bogus.to_regex() |
