Skip to content

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.

A table whose parsed record keeps source metadata
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.

Wrapping raw rows
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 value
  • source_row, the original one-based row number
  • source_column, the original one-based column number
  • source_value, the original authored value
  • source_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.

Source-aware table cells
>> 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(...).

Changing a current 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],
    ]
)
Current value vs source value
>> 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(...).

A row table schema
class UserTable(RowTable):
    name = field("name")
    role = field("role")

Use parse(...) when you need schema records and source metadata.

Reading row record source
user = UserTable.parse(raw_rows)[0]
role_source = user.source_for("role")
Row record 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.

A column table schema
class ContentTable(ColumnTable):
    id = id_field("IDs")
    headline = field("Headline")
    status = field("Status", default="draft")
Reading column record source
content = ContentTable.parse(
    [
        ["IDs", "A-1"],
        ["Headline", "Market brief"],
    ]
)[0]
Column record source metadata
>> 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.

Trying to locate a defaulted field
content.source_for("status")
Missing source-cell error
KeyError: "No source cell is available for field 'status'"

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.

Trying to mutate record source cells
user.table_source.cells["role"] = None
Read-only source mapping
TypeError: 'mappingproxy' object does not support item assignment

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.

A transformation that uppercases one value
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)
Transformed record source
>> 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.

Creating a source-aware error
source = TableCell.from_value("invalid-range", row=3, column=4)
error = TableError.from_cell(
    "Invalid range",
    source,
    schema="ContentTable",
)
Error created from a source cell
>> 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.

A parser failure after transformation
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)
Parsing the transformed table
BrokenRoleTable.parse(raw_rows)
Diagnostic points to the authored value
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.