Skip to content

CellDSL Patterns⚓︎

Exact tokens are good for fixed words such as random or today. Patterns are for project vocabulary that has a small amount of structure inside the cell.

Examples include:

  • 3 words
  • CMS:market-brief
  • user:alice
  • between 1 and 5
  • tag:news,markets

The cell is still authored as plain text. The pattern gives that text a controlled meaning for one schema field.

A table with a patterned cell
Feature: CMS generated text syntax

  Scenario: Generate headline words from a compact cell
    Given the content items:
      | IDs      | A-1     |
      | Headline | 3 words |
    Then the headline text is generated from the count

Use patterns for parameterized vocabulary

If the value has a number, name, slug, or other parameter inside it, use a pattern or predicate instead of registering many exact tokens.

Register a Regex Pattern⚓︎

Create a CellDSL and register a regular expression with pattern(...).

A pattern with a named capture
from talika import CellDSL, ColumnTable, RowTable, field, id_field


content_cells = CellDSL()


@content_cells.pattern(r"(?P<count>\d+) words")
def generated_words(match, context):
    count = int(match["count"])
    return " ".join(
        f"{context.item_id}-{number}"
        for number in range(1, count + 1)
    )

The handler receives two arguments:

  • match, the regular-expression match object
  • context, the same cell context passed to other field parsers

Named groups keep the handler readable. Here, match["count"] becomes the number of words to generate.

A content schema using the pattern
class ContentTable(ColumnTable):
    id = id_field("IDs")
    headline = field("Headline", required=True, parser=content_cells)
A compact generated headline
content_table = [
    ["IDs", "A-1"],
    ["Headline", "3 words"],
]
Pattern result
>> ContentTable.parse(content_table)[0]
ContentTable(id='A-1', headline='A-1-1 A-1-2 A-1-3')

>> ContentTable.parse(content_table)[0].headline
'A-1-1 A-1-2 A-1-3'

The authored cell 3 words is not returned as text. The pattern handler turns it into the parsed headline value for that field.

Pattern handlers return parsed values

A pattern handler can return a string, list, number, enum, dataclass, or any other value the field should hold. It follows the same field-parser contract as ordinary parser functions.

Patterns Use Full Match⚓︎

Pattern rules use regular-expression fullmatch, not substring search.

Values that do and do not full-match
records = ContentTable.parse(
    [
        ["IDs", "A-1", "A-2", "A-3"],
        ["Headline", "3 words", "prefix 3 words", "3 words please"],
    ]
)
Full-match behavior
>> [record.headline for record in records]
['A-1-1 A-1-2 A-1-3', 'prefix 3 words', '3 words please']

Only 3 words matches the full pattern. prefix 3 words and 3 words please do not match, so they pass through unchanged.

This is deliberate. A pattern should describe the whole authored cell. If a project really wants substring behavior, make that explicit in the expression with .*.

Do not rely on accidental substring matches

Full-match behavior keeps table vocabulary predictable. A broad substring match can accidentally reinterpret ordinary prose as project syntax.

Pattern Order Matters⚓︎

Pattern rules are tried in registration order after exact tokens.

Specific pattern before catch-all
priority_cells = CellDSL()


@priority_cells.pattern(r"\d+ words")
def specific(match, context):
    return "specific"


@priority_cells.pattern(r".*")
def catch_all(match, context):
    return "catch-all"


class PriorityTable(RowTable):
    value = field("value", parser=priority_cells)
First matching pattern wins
>> PriorityTable.parse([["value"], ["3 words"], ["anything else"]])
[PriorityTable(value='specific'), PriorityTable(value='catch-all')]

The first value matches \d+ words, so the specific handler runs. The second value does not match that pattern, so the catch-all pattern handles it.

Keep specific patterns before broad patterns. A pattern such as .* should be near the end because it can match almost anything.

Dispatch order inside one DSL

CellDSL dispatch is exact tokens first, then patterns, then predicates, then fallback. This page focuses on the last three rule types; exact tokens keep their higher priority.

Use Predicates for Awkward Syntax⚓︎

Some project syntax is easier to express as Python code than as a regular expression. Use when(...) for that.

A predicate rule for CMS-prefixed values
predicate_cells = CellDSL()


@predicate_cells.when(
    lambda value, context: value.startswith("CMS:"),
    fields=("headline",),
)
def cms_headline(value, context):
    return value.removeprefix("CMS:").replace("-", " ").title()

Predicates receive the current value and context, and return True when the handler should run. The handler also receives the original value and context.

A schema using a predicate DSL
class PredicateContentTable(ColumnTable):
    id = id_field("IDs")
    headline = field("Headline", parser=predicate_cells)
    status = field("Status", parser=predicate_cells)
Predicate result
>> PredicateContentTable.parse(
...     [
...         ["IDs", "A-1"],
...         ["Headline", "CMS:market-brief"],
...         ["Status", "CMS:draft"],
...     ]
... )[0]
PredicateContentTable(id='A-1', headline='Market Brief', status='CMS:draft')

The predicate is scoped to headline, so CMS:market-brief becomes Market Brief, while CMS:draft in status stays literal.

Keep predicates cheap

Predicates may be evaluated for many cells. They should be deterministic, quick, and free of side effects. Put the expensive work in the handler after the predicate has matched.

Use Fallback for Owned Fields⚓︎

By default, unmatched values pass through unchanged. A fallback changes that: it handles any value that did not match a token, pattern, or predicate.

A status DSL with fallback normalization
status_cells = CellDSL()


@status_cells.token("published")
def published(context):
    return "PUBLISHED"


@status_cells.fallback
def normalize_status(value, context):
    return value.strip().casefold().replace(" ", "-")
A status schema
class StatusTable(ColumnTable):
    id = id_field("IDs")
    status = field("Status", parser=status_cells)
Fallback result
>> records = StatusTable.parse(
...     [
...         ["IDs", "A-1", "A-2", "A-3"],
...         ["Status", "Drafted", "Ready For Review", "published"],
...     ]
... )
>> [record.status for record in records]
['drafted', 'ready-for-review', 'PUBLISHED']

The exact token published still wins before fallback. The other status values do not match explicit rules, so fallback normalizes them.

Use fallback when the DSL should own every value for that field, such as a status normalizer or a small controlled vocabulary parser.

Fallback matches everything

A fallback makes the DSL handle every unmatched value. That is useful for owned fields, but too broad for fields that should accept literal text unchanged.

Registration Is Checked⚓︎

The same pattern expression and same field scope cannot be registered twice.

Duplicate pattern registration
cells = CellDSL()


@cells.pattern(r"\d+ words")
def first_words(match, context):
    return "first"


@cells.pattern(r"\d+ words")
def second_words(match, context):
    return "second"
Duplicate pattern error
Cell DSL pattern '\\d+ words' is already registered for this scope

A DSL can also have only one fallback.

Duplicate fallback registration
cells = CellDSL()


@cells.fallback
def first_fallback(value, context):
    return value


@cells.fallback
def second_fallback(value, context):
    return value
Duplicate fallback error
Cell DSL fallback is already registered

Registration errors happen when the DSL is defined. Invalid regular expression syntax is also rejected immediately by Python's regex compiler.

Fail ambiguous vocabulary early

A DSL is project language. Duplicate rules make that language harder to explain, so Talika rejects duplicate pattern and fallback declarations before any table is parsed.

Handler Errors Keep Cell Location⚓︎

If a pattern handler raises, Talika reports a field parser failure at the cell that matched the pattern.

A pattern handler that fails
broken_cells = CellDSL()


@broken_cells.pattern(r"(?P<count>\d+) words")
def broken_words(match, context):
    raise RuntimeError("word generator unavailable")


class BrokenTable(ColumnTable):
    id = id_field("IDs")
    headline = field("Headline", parser=broken_cells)
Parsing the failing pattern
BrokenTable.parse([["IDs", "A-1"], ["Headline", "3 words"]])
Pattern diagnostic
Field parser failed: word generator unavailable (code=parser_failed, schema=BrokenTable, field='Headline', row=2, column=2, item_id='A-1', value='3 words'). Hint: Check the cell value or adjust the field parser for this syntax.

The diagnostic includes the schema, field, source row and column, item ID, and authored value. The handler can stay focused on project conversion while Talika preserves the table context around the failure.

Pattern failures are parser failures

A matched pattern is part of field parsing. If the handler cannot build a value, the failure is reported through the same source-aware parser diagnostic path as other field parser errors.

Choose the Rule Type⚓︎

Use exact tokens when the whole cell is a fixed word or phrase.

Use patterns when the cell has a predictable text shape with captures.

Use predicates when the rule is easier to express as Python logic than as a regular expression.

Use fallback when the DSL should own every unmatched value for that field.

The goal is not to make feature tables clever. The goal is to make repeated project setup vocabulary explicit, tested, and source-aware.

Keep authored syntax small

A CellDSL works best when it explains a few project-owned phrases. If a cell starts to look like a programming language, move complexity back into Python helpers or table transformations.