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/errors | |
| 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/errors')
| -rw-r--r-- | tests/errors/context.test.py | 93 | ||||
| -rw-r--r-- | tests/errors/errors.test.py | 35 |
2 files changed, 1 insertions, 127 deletions
diff --git a/tests/errors/context.test.py b/tests/errors/context.test.py index fff9e09..6595e92 100644 --- a/tests/errors/context.test.py +++ b/tests/errors/context.test.py @@ -2,37 +2,7 @@ from unittest import mock -from edify.errors.context import ( - CallerContext, - _context_for_frame, - capture_caller_context, -) - - -class _FakeCode: - """A stand-in for ``types.CodeType`` used to force specific ``co_positions`` behaviour.""" - - def __init__(self, filename: str, positions: list[tuple]) -> None: - self.co_filename = filename - self._positions = list(positions) - - def co_positions(self): - return iter(self._positions) - - -class _FakeFrame: - """A stand-in for ``types.FrameType`` shaped for :func:`_context_for_frame`.""" - - def __init__( - self, - filename: str, - f_lineno: int, - f_lasti: int, - positions: list[tuple], - ) -> None: - self.f_code = _FakeCode(filename, positions) - self.f_lineno = f_lineno - self.f_lasti = f_lasti +from edify.errors.context import CallerContext, capture_caller_context def test_capture_caller_context_returns_none_when_every_frame_is_inside_edify(): @@ -40,67 +10,6 @@ def test_capture_caller_context_returns_none_when_every_frame_is_inside_edify(): assert capture_caller_context() is None -def test_context_for_frame_uses_lineno_fallback_when_instruction_index_out_of_range(): - frame = _FakeFrame( - filename="/tmp/fake.py", - f_lineno=42, - f_lasti=999, - positions=[], - ) - context = _context_for_frame(frame) - assert context.lineno == 42 - assert context.colno == 1 - assert context.end_colno == 1 - - -def test_context_for_frame_defaults_start_line_when_position_start_line_is_none(): - frame = _FakeFrame( - filename="/tmp/fake.py", - f_lineno=99, - f_lasti=0, - positions=[(None, None, None, None)], - ) - context = _context_for_frame(frame) - assert context.lineno == 99 - - -def test_context_for_frame_defaults_end_line_when_position_end_line_is_none(): - frame = _FakeFrame( - filename="/tmp/fake.py", - f_lineno=7, - f_lasti=0, - positions=[(5, None, 2, 4)], - ) - context = _context_for_frame(frame) - assert context.lineno == 5 - assert context.colno == 3 - assert context.end_colno == 5 - - -def test_context_for_frame_defaults_start_col_when_position_start_col_is_none(): - frame = _FakeFrame( - filename="/tmp/fake.py", - f_lineno=1, - f_lasti=0, - positions=[(1, 1, None, 5)], - ) - context = _context_for_frame(frame) - assert context.colno == 1 - assert context.end_colno == 6 - - -def test_context_for_frame_defaults_end_col_when_position_end_col_is_none(): - frame = _FakeFrame( - filename="/tmp/fake.py", - f_lineno=1, - f_lasti=0, - positions=[(1, 1, 3, None)], - ) - context = _context_for_frame(frame) - assert context.colno == 4 - assert context.end_colno == 4 - - def test_caller_context_dataclass_is_frozen_and_holds_all_five_fields(): context = CallerContext(filename="a.py", lineno=1, colno=1, end_colno=5, source_line="x = 1") assert context.filename == "a.py" diff --git a/tests/errors/errors.test.py b/tests/errors/errors.test.py index 74acaa4..632045d 100644 --- a/tests/errors/errors.test.py +++ b/tests/errors/errors.test.py @@ -16,12 +16,6 @@ from edify.errors.input import ( MustBeSingleCharacterError, MustHaveASmallerValueError, ) -from edify.errors.internal import ( - FailedToCompileRegexError, - NonFusableElementError, - UnexpectedFrameTypeError, - UnknownElementTypeError, -) from edify.errors.naming import ( CannotCreateDuplicateNamedGroupError, NamedGroupDoesNotExistError, @@ -188,31 +182,6 @@ def test_cannot_call_subexpression(): assert "capture" in text -def test_unknown_element_type(): - error = UnknownElementTypeError("WeirdElement") - text = str(error) - assert "unknown element type 'WeirdElement'" in text - - -def test_non_fusable_element(): - error = NonFusableElementError("DigitElement") - text = str(error) - assert "cannot fuse element 'DigitElement' into a character class" in text - - -def test_unexpected_frame_type(): - error = UnexpectedFrameTypeError("DigitElement") - text = str(error) - assert "stack frame anchored at unexpected element 'DigitElement'" in text - - -def test_failed_to_compile_regex(): - error = FailedToCompileRegexError("missing )") - text = str(error) - assert "rejected by the re engine" in text - assert "missing )" in text - - def test_every_annotated_error_message_starts_with_error_prefix(): errors = [ StartInputAlreadyDefinedError(), @@ -234,10 +203,6 @@ def test_every_annotated_error_message_starts_with_error_prefix(): NamedGroupDoesNotExistError("x"), CannotEndWhileBuildingRootExpressionError(), CannotCallSubexpressionError("capture"), - UnknownElementTypeError("X"), - NonFusableElementError("X"), - UnexpectedFrameTypeError("X"), - FailedToCompileRegexError("boom"), ] for error in errors: text = str(error) |
