aboutsummaryrefslogtreecommitdiff
path: root/tests/errors
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-07-11 17:44:20 +0530
committerGitHub <[email protected]>2026-07-11 17:44:20 +0530
commitabdaeb4aad4284986f012efcce35a6e4189f2929 (patch)
tree9eaff3968954dc8f5bf079684dba46b6026e2221 /tests/errors
parent2038ee5c4a5e473c20cf1e38af5c32ab450a2aea (diff)
parentc7a8a902eb86dfec5dd5c4ec77c532f5b920d3ac (diff)
downloadedify-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.py93
-rw-r--r--tests/errors/errors.test.py35
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)