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 wordsCMS:market-briefuser:alicebetween 1 and 5tag:news,markets
The cell is still authored as plain text. The pattern gives that text a controlled meaning for one schema field.
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(...).
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 objectcontext, 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.
class ContentTable(ColumnTable):
id = id_field("IDs")
headline = field("Headline", required=True, parser=content_cells)
>> 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.
records = ContentTable.parse(
[
["IDs", "A-1", "A-2", "A-3"],
["Headline", "3 words", "prefix 3 words", "3 words please"],
]
)
>> [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.
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)
>> 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.
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.
class PredicateContentTable(ColumnTable):
id = id_field("IDs")
headline = field("Headline", parser=predicate_cells)
status = field("Status", parser=predicate_cells)
>> 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.
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(" ", "-")
class StatusTable(ColumnTable):
id = id_field("IDs")
status = field("Status", parser=status_cells)
>> 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.
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"
A DSL can also have only one fallback.
cells = CellDSL()
@cells.fallback
def first_fallback(value, context):
return value
@cells.fallback
def second_fallback(value, context):
return value
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.
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)
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.