Custom Parsers⚓︎
Use a custom parser when the built-in parser factories cannot fully describe your table language.
The parser contract is simple: a parser is a callable that receives the current
cell value and a CellContext, then returns the Python value for that field.
The value is usually a string from the table, but it may already have been changed by a table transformer. The context tells the parser which schema, field, source cell, item ID, and parse-time project data are involved.
Write custom parsers for project vocabulary
A custom parser is best when the table uses language that belongs to your project: imported IDs, role aliases, external status codes, compact domain syntax, or validation that depends on parse-time configuration.
Write the Parser Signature⚓︎
A custom parser must accept two arguments:
value: the current cell value being parsed.context: aCellContextfor the active field.
from talika import RowTable, field, id_field
seen_context = []
def parse_username(value, context):
seen_context.append(
{
"schema": context.schema.__name__,
"field_name": context.field_name,
"field_label": context.field_label,
"row": context.row,
"column": context.column,
"item_id": context.item_id,
"source_value": context.source_value,
}
)
prefix = context.user_data["prefix"]
return f"{prefix}-{context.item_id}-{value.strip().lower()}"
class ImportUsers(RowTable):
user_id = id_field("user id")
username = field("username", parser=parse_username)
This parser uses three pieces of information:
valuesupplies the authored username cell.context.item_idsupplies the parsed row ID fromid_field(...).context.user_data["prefix"]supplies project data passed toparse(...).
users = ImportUsers.parse(
[
["user id", "username"],
["U-1", " Alice "],
],
context={"prefix": "import"},
)
assert users[0].username == "import-U-1-alice"
assert seen_context[0]["item_id"] == "U-1"
assert seen_context[0]["source_value"] == " Alice "
>> users[0]
ImportUsers(user_id='U-1', username='import-U-1-alice')
>> seen_context[0]
{'schema': 'ImportUsers', 'field_name': 'username', 'field_label': 'username', 'row': 2, 'column': 2, 'item_id': 'U-1', 'source_value': ' Alice '}
The parser returns the value stored on the record. Talika does not apply a second conversion after the custom parser returns.
The signature must accept context
A parser that only accepts value will fail because Talika always calls
parsers with both value and context.
Read CellContext⚓︎
CellContext is the parser's view of the parsing operation. It gives the
parser source-aware information without forcing the parser to know how row and
column tables are implemented.
| Attribute | Meaning |
|---|---|
schema |
The schema class currently parsing the field. |
field_name |
The Python attribute that will receive the parsed value. |
field_label |
The canonical table label declared by field(...). |
row |
One-based source row, when available. |
column |
One-based source column, when available. |
item_id |
Parsed item ID, when the table has one. |
source_value |
The original authored cell text. |
user_data |
Read-only project data supplied to parse(..., context=...). |
Use field_name when your parser cares about the Python schema attribute. Use
field_label when the error or normalization belongs to the authored table
label. Use item_id when parsing depends on the current record identity.
Value and source_value are not always the same
value is the current value being parsed. context.source_value is the
original authored cell text. They are usually the same in basic tables, but
table transformation can change value while preserving the original
source for diagnostics.
Pass Project Data into Parsers⚓︎
Do not hide project state in global variables when the value can change per
test run. Pass it to parse(..., context={...}) and read it from
context.user_data.
def parse_role(value, context):
aliases = context.user_data.get("role_aliases", {})
allowed_roles = context.user_data["allowed_roles"]
normalized = str(value).strip().lower()
role = aliases.get(normalized, normalized)
if role not in allowed_roles:
raise ValueError(f"{role!r} is not allowed for {context.field_label}")
return role
class RoleImport(RowTable):
email = field("email", required=True)
role = field("role", parser=parse_role, required=True)
This parser normalizes authored role names, applies aliases from the parse context, and checks the result against a configured set of allowed roles.
records = RoleImport.parse(
[
["email", "role"],
["a@example.com", "Administrator"],
],
context={
"allowed_roles": {"admin", "editor", "viewer"},
"role_aliases": {"administrator": "admin"},
},
)
assert records[0].role == "admin"
The table author can write Administrator, while the test receives the project
role value admin.
If the value is not accepted, raise a normal exception with a clear message:
RoleImport.parse(
[
["email", "role"],
["a@example.com", "owner"],
],
context={
"allowed_roles": {"admin", "editor", "viewer"},
"role_aliases": {"administrator": "admin"},
},
)
Field parser failed: 'owner' is not allowed for role
(code=parser_failed, schema=RoleImport, field='role',
row=2, column=2, value='owner').
Hint: Check the cell value or adjust the field parser for this syntax.
Talika wraps the exception with schema, field, row, column, and authored value. That means the parser message can focus on the domain problem.
Keep parser messages domain-specific
A good parser error says what was wrong with the table value, not where the value came from. Talika adds the source location around it.
Parse Compact Domain Syntax⚓︎
Custom parsers are also useful when a table cell has compact syntax that is specific to your test domain.
def parse_percent(value, context):
text = str(value).strip()
if not text.endswith("%"):
raise ValueError("expected a percent value such as '95%'")
number = int(text[:-1])
if not 0 <= number <= 100:
raise ValueError("percent must be between 0 and 100")
return number / 100
class Metrics(RowTable):
success_rate = field("success rate", parser=parse_percent)
This parser accepts values such as 95%, rejects values without the percent
sign, rejects percentages outside the range 0..100, and returns a decimal
ratio that test code can compare directly.
metric = Metrics.parse(
[
["success rate"],
["95%"],
]
)[0]
assert metric.success_rate == 0.95
When the value is outside the project rule, the parser raises ValueError:
Field parser failed: percent must be between 0 and 100
(code=parser_failed, schema=Metrics, field='success rate',
row=2, column=1, value='110%').
Hint: Check the cell value or adjust the field parser for this syntax.
This style keeps the parser small and readable. It does not try to validate the whole record. It only answers one field-level question: how should this cell be converted?
Keep record rules out of field parsers
A field parser should parse one cell. If a rule needs multiple fields from the same record, it belongs in record validation rather than in a field parser.
Empty Cells and Custom Parsers⚓︎
By default, an explicit empty optional cell is not sent to the parser. Talika
returns "" for that field.
If your parser needs to handle empty text itself, set empty="parse" on the
field.
def parse_blank(value, context):
return "<blank>" if value == "" else value
class EmptyAware(RowTable):
normal = field("normal", parser=parse_blank)
parsed_empty = field("parsed empty", parser=parse_blank, empty="parse")
record = EmptyAware.parse(
[
["normal", "parsed empty"],
["", ""],
]
)[0]
assert record.normal == ""
assert record.parsed_empty == "<blank>"
The normal field keeps the empty string because the parser is skipped for
blank optional cells. The parsed_empty field sends the blank string to the
parser because the field explicitly opts into empty="parse".
Do not assume your parser sees blanks
If an optional field has an empty cell and the field does not use
empty="parse", your custom parser will not run for that cell.
Use Source Value Carefully⚓︎
Most custom parsers should use value. That is the current value in the table
lifecycle. In ordinary tables, it is the text written in the feature file.
Use context.source_value when the original authored text matters for a
diagnostic or for project syntax that should survive transformation.
def require_poll(value, context):
if value != "Poll":
raise ValueError(
f"expected Poll after transformation; original cell was {context.source_value!r}"
)
return value
The parser above checks the transformed value, but its error message can still mention the original cell text. This matters when a transformer expands or normalizes compact authored syntax before field parsing.
Prefer value for conversion
Treat value as the thing you are converting. Treat source_value as
source context that helps explain where the value came from.
Understand Parser Failures and Exception Wrapping⚓︎
When a custom parser raises an ordinary exception during parsing, Talika wraps
it as TableError(code="parser_failed") and retains the original exception as
the diagnostic cause.
Because of this auto-wrapping behavior:
- Raise Plain Exceptions when Talika should classify the failure as
parser_failedand populate its normal schema, field, location, and values. - Raise
TableErrordeliberately when the project owns a more specific code, hint, or location. Talika passes that exact exception through unchanged. - Raise
TableErrorsorSchemaDefinitionErrordeliberately when the extension owns an aggregate or schema-level failure; these also pass through.
The same pass-through rule applies to default factories, transformers, reference-key parsers, validators, and output builders.
def bad_parser(value):
return value
class BadSignature(RowTable):
value = field("value", parser=bad_parser)
BadSignature.parse(
[
["value"],
["x"],
]
)
Field parser failed: bad_parser() takes 1 positional argument but 2 were given
(code=parser_failed, schema=BadSignature, field='value',
row=2, column=1, value='x').
Hint: Check the cell value or adjust the field parser for this syntax.
Test custom parsers through a schema
A parser function can be unit-tested directly for small conversions, but
also test it through RowTable.parse(...) or ColumnTable.parse(...).
That verifies the error message, source location, context data, and
empty-cell behavior together.