Engine — Converter, Template & Errors¶
Overview¶
The engine module is the runtime backbone of InfinityParse: it defines
Converter (per-field configuration data), Field (a field's converter branch
set), Template (a compiled template) and the error hierarchy. The high-level
scanner entry points normalize annotation metadata into a Field
sequence at construction time and hand it to Template for compilation.
Converter — Per-Field Configuration¶
@dataclass(frozen=True)
class Converter[T]:
fn: Callable[[str], T] # conversion callable: captured text -> T
pattern: str = r".+?" # capture pattern (lazy, non-empty, single-line)
strip: bool = True # strip the captured text before converting
name: str | None = None # display label used in error messages
flags: int = 0 # re flags applied to the pattern
| Parameter | Type | Default | Description |
|---|---|---|---|
fn |
Callable[[str], T] |
(required) | The conversion callable. Any exception it raises is wrapped into ConvertError. |
pattern |
str |
.+? |
This branch's capture pattern; branches of one field each carry their own. |
strip |
bool |
True |
Strips the captured text before converting; False keeps raw whitespace (e.g. t.Whitespace needs it). |
name |
str \| None |
None |
Display label in error messages; falls back to fn.__name__. |
flags |
int |
0 |
re flags applied to this branch's own pattern. Supported inline subset: i / m / s / x / a; unsupported bits raise ValueError at compile time. |
Usage examples:
import json
import re
from typing import Annotated
from infinity_parse import Converter, scan
# Custom conversion callable (JSON instead of Python literals, say)
scan[Annotated[dict[str, str], Converter(json.loads, name="json")]]("{}")(
'{"key": "value"}'
) # ({"key": "value"},)
# Custom pattern + label (friendlier error messages)
scan[Annotated[int, Converter(int, pattern=r"\d+", name="age")]]("age={}")(
"age=42"
) # (42,)
# strip=False: keep the whitespace inside the field
scan[Annotated[str, Converter(str, strip=False)]]("{}!")("hi !") # ("hi ",)
# flags: applied to this branch's own pattern
scan[Annotated[str, Converter(str, pattern=r"[a-z]+", flags=re.IGNORECASE)]]("{}")(
"HeLLo"
) # ("HeLLo",)
Converteris a frozen dataclass: safe to store and reuse. It participates in the template compile cache, sofnmust be hashable.
Escape-Aware String Fields¶
Extend the pattern to be escape-aware and let json.loads decode escapes;
malformed escapes fail hard in the converter (never silently):
import json
from typing import Annotated
from infinity_parse import Converter, scan
parse_name = scan[
Annotated[str, Converter(json.loads, pattern=r'"(?:[^"\\]|\\.)*"', name="name")]
]("{}")
parse_name(r'"kim \"the fox\""') # ('kim "the fox"',)
parse_name(r'"C:\\path\\to\\file"') # ('C:\\path\\to\\file',)
Capturing Large Blobs¶
The default .+? is lazy — designed for fields followed by an anchor: it expands
one character at a time to find the shortest match. For a large capture with no
following anchor (a document body, say) that is expensive on CPython: a 1 MB
capture measures ~8 ms, while an explicit greedy DOTALL pattern takes
~1 µs:
from typing import Annotated
from infinity_parse import Converter, scan
Blob = Annotated[str, Converter(str, pattern=r"(?s:.+)")]
parse_doc = scan[Blob]("{}")
parse_doc("arbitrarily large text…") # near-zero overhead at any size
Do not change the default — shortest-match (lazy) semantics are the heart of the template design.
Scanners Inside Converters¶
Converter.fn may be another scanner — the outer pattern captures the field, then
the inner scanner parses it:
from typing import Annotated
from infinity_parse import Converter, scan
point = scan[int, int]("{} {}")
parse = scan[
Annotated[
tuple[int, int], Converter(point, pattern=r"(?<=\[).+?(?=\])", name="point")
]
]("point=[{}]")
parse("point=[1 2]") # ((1, 2),)
An inner failure surfaces as a ConvertError, whose __cause__ chain points at
the inner ScanError.
Field — The Converter Branch Set¶
@dataclass(frozen=True)
class Field:
converters: tuple[Converter[Any], ...] # in competition order; at least one
- Single-branch field: that branch is the field's whole behavior.
- Multi-branch field: branches compete in declaration order; the first branch whose pattern matches wins; if the winner's converter fails, the whole scan fails (no fallback).
- Multiple branches come from
OneOfor|unions (see scanner.md). - Like
Converter,Fieldmust be hashable.
Template — The Compiled Template¶
class Template:
def __init__(self, template: str, fields: tuple[Field, ...]) -> None
def fullmatch(self, input_value: str) -> tuple[Any, ...]
def search(self, input_value: str) -> tuple[Any, ...] | None
def findall(self, input_value: str) -> list[tuple[Any, ...]]
All validation happens once, at construction time; matching carries no parsing overhead afterwards:
| Validation | Failure | Message |
|---|---|---|
| Placeholder count ≠ field count | ValueError |
... has N placeholder(s) but M converter type(s) were given |
A single { / } |
ValueError |
single '{' in template ... |
| Converter not callable | TypeError |
converters must be callable |
| A field without converters | ValueError |
each field requires at least one converter |
| Invalid pattern (regex error) | re.error |
raised by re.compile |
| Converter not hashable | TypeError |
— |
Compilation details (for the curious):
- The template is split into literal segments and placeholders; literal segments
are
re.escaped, and whitespace runs become\s+. - Every field gets a named group
s0/s1/ …; with multiple branches each branch gets its own named group (s0b0,s0b1, …) to identify the winner. flagsare bound to each branch's own pattern via scoped inline flags ((?i:...)), so branches never leak flags into each other.- Compilation is LRU-cached (up to 1024 templates).
Error Model¶
graph TD
V[ValueError] --> S[ScanError]
S --> M["MatchError — soft failure"]
S --> C["ConvertError — hard failure"]
| Error | Meaning | Typical trigger | Swallowed by alt? |
|---|---|---|---|
MatchError |
Input does not match the template | Full match miss / search finds nothing | ✅ Yes (alt tries other branches) |
ConvertError |
Field matched but conversion failed | Converter raised; malformed escapes, etc. | ❌ No (hard failure, aborts immediately) |
The ConvertError message carries the field number, text, offset and label:
scan[int, int]("{} {}")("1 zz")
# ConvertError: cannot convert field 2 ('zz') at offset 2 using int
| Piece | Meaning |
|---|---|
field 2 |
The 2nd {} (1-based) |
'zz' |
The text used for conversion (after stripping) |
offset 2 |
Index of the field's captured text in the original input (scan compensates for the strip shift automatically) |
using int |
The branch's display label (Converter.name or fn.__name__) |
- The original exception is preserved in
__cause__:ValueError,SyntaxError,json.JSONDecodeError, … - Offsets in
search/findallare relative to the original input as well (no strip compensation applies there). - Errors from nested scanners form a
__cause__chain.
Both errors also expose the same details as structured attributes, so callers can consume them programmatically without parsing the message:
| Error | Structured attributes |
|---|---|
MatchError |
input_value, template (None when unknown) |
ConvertError |
field, offset, text, converter, input_value, template |
from infinity_parse import ConvertError, scan
try:
scan[int, int]("{} {}")("1 zz")
except ConvertError as error:
error.field # 2
error.offset # 2
error.text # 'zz'
error.converter # 'int'
MatchError raised by alt carries input_value as well (see
combinators.md).
Low-Level Construction¶
To build without scan[...], combine resolve_field / Field / Template:
from infinity_parse import Converter, Template, resolve_field
from infinity_parse.engine import Field
field = resolve_field(int, [Converter(int, pattern=r"\d+", name="x")])
template = Template("x={}", (field,))
template.fullmatch("x=42") # (42,)
template.search("x=42!") # (42,)
template.findall("x=1, x=2") # [(1,), (2,)]
Or build branches by hand:
field = Field(converters=(Converter(int, pattern=r"\d+", name="x"),))
Notes¶
- Conversion failures never fall back: for "try several shapes" use explicit
OneOfbranches and make each branch's pattern precise. flagssupports the five inline flagsi/m/s/x/aonly; other flag bits raiseValueError.Templateused directly returnstuple[Any, ...]; for precise inferred types use the scanner entry points.Field/Converterare frozen dataclasses — safe to reuse as constants.