Source Model⚓︎
Talika keeps two versions of a table value in mind:
- the current value being parsed
- the original source cell the author wrote
That distinction is the reason diagnostics can point back to a precise table cell even after parsing, defaults, references, validators, output conversion, or table transformations have run.
Feature: Source-aware table diagnostics
Scenario: Inspect where parsed content values came from
Given the content items:
| IDs | A-1 |
| Headline | Market brief |
Then the parsed record keeps source cell metadata
The source model is not something most users need for basic parsing. It becomes important when you write custom validators, table transformers, static checks, editor integrations, or diagnostics that should point to authored table cells.
Separate value from provenance
A parsed value answers "what should the test use?" Source metadata answers "where did this value come from in the authored table?"
Raw Rows Become TableData⚓︎
Pytest-bdd style datatables usually arrive as ordinary nested strings. Talika
upgrades those rows into TableData before parsing.
from talika import (
ColumnTable,
RowTable,
TableCell,
TableData,
TableError,
field,
id_field,
)
raw_rows = [
["name", "role"],
["Alice", "admin"],
]
table = TableData.from_rows(raw_rows)
role_cell = table.cell(2, 2)
TableData contains TableCell objects. Each cell has:
value, the current logical valuesource_row, the original one-based row numbersource_column, the original one-based column numbersource_value, the original authored valuesource_uri, the source document URI when known
The table itself also has source_uri. Pass a URI string or a filesystem path
to TableData.from_rows(..., source=...) or
TableData.from_cells(..., source=...). Paths become absolute file: URIs;
URI strings remain unchanged.
>> table.to_rows()
[['name', 'role'], ['Alice', 'admin']]
>> role_cell
TableCell(value='admin', source_row=2, source_column=2, source_value='admin')
>> (role_cell.value, role_cell.source_row, role_cell.source_column, role_cell.source_value)
('admin', 2, 2, 'admin')
Coordinates are one-based because they are meant for people reading feature
tables and diagnostics. table.cell(2, 2) means row 2, column 2 in the
authored table.
Use one-based coordinates with cell()
TableData.cell(...) is deliberately one-based. Use table.rows directly
when writing normal zero-based Python iteration.
Change Values Without Losing Source⚓︎
When transformation code changes a logical value, use cell.with_value(...).
changed_role = role_cell.with_value("ADMIN")
changed_table = TableData.from_cells(
[
[table.cell(1, 1), table.cell(1, 2)],
[table.cell(2, 1), changed_role],
]
)
>> changed_role
TableCell(value='ADMIN', source_row=2, source_column=2, source_value='admin')
>> changed_table.to_rows()
[['name', 'role'], ['Alice', 'ADMIN']]
The new cell has value='ADMIN', but source_value='admin'. That means later
parsing sees the changed value, while later diagnostics can still quote the
authored table cell.
TableData.from_cells(...) is used when code already has source-aware cells
and wants to arrange them into a new logical table.
A transformer result without an explicit source inherits its input table's URI. Built-in transformers preserve it directly, and transformer pipelines carry it between stages.
to_rows only returns current values
to_rows() is useful for display and debugging, but it drops source
metadata. Do not use it inside transformers that need to preserve
diagnostics.
Row Records Keep Row Source⚓︎
Parsed schema records expose source metadata through table_source and
source_for(...).
Use parse(...) when you need schema records and source metadata.
>> user
UserTable(name='Alice', role='admin')
>> (user.table_source.row, user.table_source.column, user.table_source.item_id)
(2, None, None)
>> role_source
TableCell(value='admin', source_row=2, source_column=2, source_value='admin')
For a row-oriented record, table_source.row points to the data row. There is
no record column, so table_source.column is None. Field cells still know
their own row and column.
Use source_for for field-level diagnostics
table_source tells you where the record lives. source_for("field_name")
tells you which authored cell supplied one field.
Column Records Keep Item Source⚓︎
Column-shaped records use the item column as the record location.
class ContentTable(ColumnTable):
id = id_field("IDs")
headline = field("Headline")
status = field("Status", default="draft")
content = ContentTable.parse(
[
["IDs", "A-1"],
["Headline", "Market brief"],
]
)[0]
>> content
ContentTable(id='A-1', headline='Market brief', status='draft')
>> (content.table_source.item_id, content.table_source.column, content.table_source.row)
('A-1', 2, None)
>> content.source_for("id")
TableCell(value='A-1', source_row=1, source_column=2, source_value='A-1')
>> content.source_for("headline")
TableCell(value='Market brief', source_row=2, source_column=2, source_value='Market brief')
For a column-oriented record, table_source.item_id is the parsed ID and
table_source.column points to the item column. Field sources still point to
the row and column where that field value was authored.
This is useful in CMS-style tables because a diagnostic can say both which item failed and which field cell caused the failure.
Record location depends on table shape
Row tables usually identify records by row. Column tables usually identify records by item ID and item column.
Defaults May Not Have Source Cells⚓︎
A value can exist on a record without coming from an authored cell. Defaults are the common case.
The status field exists on the parsed record because the schema supplied a
default. There is no Status cell in the table, so source_for("status")
raises KeyError.
Only locate authored values
Call source_for(...) when a value came from the table. For missing fields
filled by defaults, raise record-level diagnostics or point to another
authored cell that caused the problem.
Source Metadata Is Read-Only⚓︎
Record source metadata is copied and exposed through read-only mappings.
This protects diagnostics from accidental mutation after parsing. A validator, factory, or helper can read provenance, but should not rewrite where a record came from.
Build new values instead of mutating source
If a transformer needs to change the logical table, it should build a new
TableData from source-aware cells. Parsed record source metadata is for
inspection.
Transformations Preserve Original Cells⚓︎
Source metadata is especially important when a transformer rewrites values before field parsing.
class UpperRoleTable(RowTable):
name = field("name")
role = field("role")
@classmethod
def transform_table(cls, table, context):
rows = [list(row) for row in table.rows]
rows[1][1] = rows[1][1].with_value(rows[1][1].value.upper())
return TableData.from_cells(rows)
>> record = UpperRoleTable.parse(raw_rows)[0]
>> record.role
'ADMIN'
>> record.source_for("role")
TableCell(value='ADMIN', source_row=2, source_column=2, source_value='admin')
The parsed value is ADMIN, but the source value is still admin. The
transformer changed what later parsing saw, not where the value came from.
This matters when a later parser or validator fails. The diagnostic should point to the table text the author can edit, not to an internal value created by the transformer.
Use with_value inside transformations
If a transformed value derives from an existing table cell, use
source_cell.with_value(...). That preserves row, column, and authored
value.
Build Errors from Source Cells⚓︎
Custom validators and transformers can build precise diagnostics from a
TableCell.
source = TableCell.from_value("invalid-range", row=3, column=4)
error = TableError.from_cell(
"Invalid range",
source,
schema="ContentTable",
)
>> str(error)
"Invalid range (code=table_error, schema=ContentTable, row=3, column=4, value='invalid-range')"
>> (error.row, error.column, error.value)
(3, 4, 'invalid-range')
TableError.from_cell(...) copies the source URI, source row, source column,
original source value, and current logical value into the diagnostic. Use it
when a custom rule can identify the exact authored cell that caused the
failure.
Point to the cell the author can fix
If the problem belongs to one table cell, build the error from that cell. If the problem belongs to a whole record or whole table, use a record-level or table-level diagnostic instead.
Parser Errors Use Source Values⚓︎
When a parser fails after transformation, Talika reports the original source cell.
def reject_role(value, context):
raise ValueError("role unavailable")
class BrokenRoleTable(RowTable):
name = field("name")
role = field("role", parser=reject_role)
@classmethod
def transform_table(cls, table, context):
rows = [list(row) for row in table.rows]
rows[1][1] = rows[1][1].with_value(rows[1][1].value.upper())
return TableData.from_cells(rows)
Field parser failed: role unavailable (code=parser_failed, schema=BrokenRoleTable, field='role', row=2, column=2, value='admin'). Hint: Check the cell value or adjust the field parser for this syntax.
The parser saw the transformed value internally, but the diagnostic reports
value='admin' because that is the authored cell text. This keeps the error
actionable for the person editing the table.
Current value and source value serve different readers
Parsers need the current logical value. Diagnostics need the authored source value. The source model keeps both available.
When to Use the Source Model⚓︎
Most ordinary tests can ignore these objects and simply use parse(...).
Reach for the source model when you are writing:
- custom validators with cell-specific errors
- table transformations
- static checkers or editor diagnostics
- source-aware assertions
- output builders that need record provenance
- tooling that reports row and column locations
Use parse(...) when you need source metadata after parsing. Use
TableData and TableCell directly when you are transforming or checking the
table before normal schema parsing.
Keep source handling close to diagnostics
Source metadata is most valuable at the point where you need to explain a problem. Keep ordinary domain logic focused on parsed values, and use source objects when you need to report where those values came from.