Column Group Expansion⚓︎
Column group expansion is for column-shaped tables where one authored column stands for several logical records.
That sounds abstract, but the table shape is common in feature files. Authors often want to say "IDs 1 through 3 are articles with the same headline" without copying the same values three times.
Feature: CMS grouped content
Scenario: One compact column describes several content records
Given the content table:
| IDs | 1..3 | 4 |
| Type | 3:Article | Poll |
| Headline | Shared | Vote now |
Then the grouped columns become ordinary content records
Talika can expand that compact authoring shape before the ColumnTable schema
parses it. After expansion, the schema sees a normal column table with one
logical item column per ID.
Think in source groups
The first row contains the group key. Each following row contains one value for that source group. The range rule expands the key cell, and the repeat rule expands the aligned value cells to the same length.
Configure the Expander⚓︎
Attach ColumnGroupExpander as a table transformer on a ColumnTable.
from talika import (
ColumnGroupExpander,
ColumnTable,
NumericRange,
ParseContext,
PrefixRepeat,
TableData,
field,
id_field,
)
class ContentTable(ColumnTable):
table_transformer = ColumnGroupExpander(
key_row="IDs",
range_rule=NumericRange(".."),
repeat_rule=PrefixRepeat(":"),
)
id = id_field("IDs")
content_type = field("Type")
headline = field("Headline")
compact_content = [
["IDs", "1..3", "4"],
["Type", "3:Article", "Poll"],
["Headline", "Shared", "Vote now"],
]
items = ContentTable.parse(compact_content)
key_row="IDs" tells the expander which row is allowed to define groups.
NumericRange("..") turns 1..3 into 1, 2, and 3. PrefixRepeat(":")
turns 3:Article into three Article cells.
>> [item.as_dict() for item in items]
[
{'id': '1', 'content_type': 'Article', 'headline': 'Shared'},
{'id': '2', 'content_type': 'Article', 'headline': 'Shared'},
{'id': '3', 'content_type': 'Article', 'headline': 'Shared'},
{'id': '4', 'content_type': 'Poll', 'headline': 'Vote now'},
]
The expanded table is the logical shape the schema parses:
>> ContentTable.table_transformer.transform(
... TableData.from_rows(compact_content),
... ParseContext(),
... schema=ContentTable,
... ).to_rows()
[
['IDs', '1', '2', '3', '4'],
['Type', 'Article', 'Article', 'Article', 'Poll'],
['Headline', 'Shared', 'Shared', 'Shared', 'Vote now'],
]
Expansion happens before field parsing
Field labels, required fields, parsers, defaults, validators, and output conversion run after the grouped table has become a normal logical table.
Source Cells Stay Useful⚓︎
Expanded cells still point back to the compact cell that created them.
>> items[2].source_for("content_type")
TableCell(value='Article', source_row=2, source_column=2, source_value='3:Article')
>> items[1].source_for("headline")
TableCell(value='Shared', source_row=3, source_column=2, source_value='Shared')
The third content record gets Article from the original 3:Article cell.
Both Shared headlines for IDs 1, 2, and 3 point back to the single
Shared cell.
This is why group expansion can be used in real test suites without making diagnostics vague. The test gets simple records, while the failure still points to the authored compact table.
Use Built-In Range and Repeat Rules⚓︎
Range rules work on the key row:
NumericRange("..")supports values such as1..3AlphabeticRange("-")supports values such asA-C
NumericRange currently allows at most 10,000 generated keys from one compact
cell. Exactly 10,000 is allowed; a larger range fails before allocation with
expansion_limit at the compact source cell. This is a private stabilization
guard rather than a configurable public limits API.
Repeat rules work on the value rows:
PrefixRepeat(":")supports values such as3:ArticleSuffixRepeat(" x")supports values such asArticle x3
Values that do not use the configured syntax are treated as literals. For
example, 4 is one ID, and Poll is copied once because it belongs to a
single-key group.
from talika import AlphabeticRange, ColumnGroupExpander, ColumnTable, SuffixRepeat
from talika import field, id_field
class LetterContentTable(ColumnTable):
table_transformer = ColumnGroupExpander(
key_row="IDs",
range_rule=AlphabeticRange("-"),
repeat_rule=SuffixRepeat(" x"),
)
id = id_field("IDs")
content_type = field("Type")
letter_rows = [
["IDs", "A-C"],
["Type", "Article x3"],
]
letter_items = LetterContentTable.parse(letter_rows)
>> [item.as_dict() for item in letter_items]
[
{'id': 'A', 'content_type': 'Article'},
{'id': 'B', 'content_type': 'Article'},
{'id': 'C', 'content_type': 'Article'},
]
Recognized range syntax must be valid
For range rules, a value that contains the configured separator is treated
as range syntax. 3..1 is not a literal; it is a descending numeric range,
so it fails. Repeat rules are a little more forgiving: a value such as
News: Europe stays literal with PrefixRepeat(":") because the prefix is
not a repeat count.
Write Custom Rules⚓︎
Use custom rules when your project has its own group language. Range rules
implement expand(cell, context) and return key cells. Repeat rules implement
expand(cell, expected_count, context) and return exactly that many value
cells.
Those two shapes are the public RangeRule and RepeatRule protocols. They
are structural protocols, so custom rule classes do not need to inherit from a
Talika base class. The important part is the method signature and returning
TableCell objects.
from talika import ColumnGroupExpander, ParseContext, TableData
class LabelRange:
def expand(self, cell, context):
labels = context.user_data["labels"][cell.value]
return [cell.with_value(label) for label in labels]
class CopyWithIndex:
def expand(self, cell, expected_count, context):
return [
cell.with_value(f"{cell.value} {index}")
for index in range(1, expected_count + 1)
]
custom_expander = ColumnGroupExpander(
key_row="Slots",
range_rule=LabelRange(),
repeat_rule=CopyWithIndex(),
)
custom_table = TableData.from_rows(
[
["Slots", "launch"],
["Headline", "Draft"],
]
)
custom_context = ParseContext.from_value(
{"labels": {"launch": ["hero", "sidebar"]}}
)
custom_result = custom_expander.transform(custom_table, custom_context)
>> custom_result.to_rows()
[['Slots', 'hero', 'sidebar'], ['Headline', 'Draft 1', 'Draft 2']]
Custom rules should return TableCell objects, usually by calling
cell.with_value(...). Returning plain strings would lose source coordinates,
so Talika rejects it.
Keep rule objects small
Let the expander own the table mechanics. Let each rule own one syntax: how a key group is named, or how a value is spread across that group.
Understand Expansion Errors⚓︎
When a repeat count does not match the key range, the error points to the value cell with the incorrect count:
ContentTable.parse(
[
["IDs", "1..3"],
["Type", "2:Article"],
]
)
Repeat expansion failed: Repeat count 2 does not match group size 3 (code=table_error, schema=ContentTable, row=2, column=2, value='2:Article')
If the key row is wrong, the error points to the first cell:
Expected key row 'IDs' (code=table_error, schema=ContentTable, row=1, column=1, value='Keys')
If a range is recognized but invalid, the error points to the key cell:
Range expansion failed: Numeric range must be ascending (code=table_error, schema=ContentTable, row=1, column=2, value='3..1')
A practical rule of thumb
Use group expansion when compact authoring would remove noisy repetition from a column table. If the compact syntax makes the feature harder to review, prefer writing the ordinary expanded table directly.