API reference

Everything in __all__ is importable from the top-level package:

from greater_tables import (
    GT, TableSpec, HouseStyle, build,
    TableDoc, Column, Row, Cell, HeadCell, FormatSpec, IR_VERSION,
    render_html, render_text, render_tikz, Etcher,
    canonical_dict, canonical_json, doc_hash, stamp,
    Fabricator, quick_fab, load_spec, load_style, __version__,
)

Package

greater_tables: semantic table engine with a versioned table-document IR.

Python owns semantics (dtypes, format inference, hierarchy, spans); renderers own geometry and style. No import side effects — importing this package does not touch pandas options, warnings filters, or anything else global.

class greater_tables.Etcher(tex: str, *, file_name: str = '', base_path: str | Path | None = None, tex_engine: str = 'tectonic', pdf2svg: str = 'pdf2svg', font_size: int = 11)[source]

Bases: object

Compile a TeX blob containing a tikzpicture to PDF/SVG files.

Parameters:
  • tex – LaTeX source containing one tikzpicture (e.g. render_tikz output; any surrounding table environment is stripped — standalone cannot host floats).

  • file_name – Output stem; content hash when omitted (deterministic caching).

  • base_path – Working directory; outputs land in base_path/'tikz'.

  • tex_engine'tectonic' (default) or 'pdflatex' — name or full path.

  • pdf2svg – Converter executable for compile(svg=True).

  • font_size – Standalone document point size.

compile(*, svg: bool = False) Path[source]

Compile to PDF (and SVG when requested); return the output path.

display()[source]

Show the compiled SVG inline (Jupyter; lazy IPython import).

class greater_tables.GT(data=None, caption=None, label=None, aligners=None, formatters=None, unbreakable=None, ratio_cols=None, year_cols=None, date_cols=None, raw_cols=None, show_index=None, config_path=None, style=None, mimes=None, **overrides)[source]

Bases: object

Table facade: data + spec in, renderings out (all lazy, all cached).

Parameters mirror the greater_tables constructor where they still make sense; formatters accepts the 5.x shapes (callable, '{x:...}' format string, or int = decimal digits, now grouped). config_path loads YAML TableSpec defaults which explicit arguments override. style takes a HouseStyle and is forwarded to every renderer. mimes pins what _repr_mimebundle_ publishes — None (default) detects the target, an explicit tuple such as ('text/plain',) publishes exactly that. It is a whole-document policy, so the intended form is render = partial(GT, mimes=('text/plain',)). Additional keyword arguments naming TableSpec fields pass through; unknown ones warn and are ignored (migration aid), and a HouseStyle field name warns with the fix.

property doc: TableDoc[source]

The table document (the IR) — built on first access.

property html: str[source]

HTML fragment (stylesheet embedded).

property text: str[source]

Monospace text rendering (the conformance oracle’s view).

property tikz: str[source]

TikZ/LaTeX rendering (table environment + tikzpicture).

class greater_tables.Cell(*, text: str, raw: float | int | str | bool | None = None, rowspan: Annotated[int, Ge(ge=1)] = 1, colspan: Annotated[int, Ge(ge=1)] = 1, flags: tuple[Literal['neg', 'emphasis', 'muted'], ...] = (), math: bool = False, html: bool = False)[source]

Bases: BaseModel

One body/foot cell.

text is plain unicode — each renderer applies its own escaping. raw is only present when the owning column advertises raw: true; an explicit JSON null there means a missing value, which is distinct from the field being absent (tracked via pydantic’s fields-set).

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

text: str
raw: float | int | str | bool | None
rowspan: int
colspan: int
flags: tuple[CellFlag, ...]
math: bool
html: bool
class greater_tables.Column(*, key: str, name: tuple[str, ...], dtype: Literal['int', 'float', 'date', 'datetime', 'bool', 'string', 'object'], align: Literal['l', 'r', 'c'], role: Literal['stub', 'data'] = 'data', level: Annotated[int | None, Ge(ge=0)] = None, tag: Literal['ratio', 'year', 'date', 'raw'] | None = None, wrap: bool = False, break_depth: Annotated[int | None, Ge(ge=0)] = None, format: FormatSpec | None = None, raw: bool = False)[source]

Bases: BaseModel

Descriptor for one visual column, stubs first.

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

key: str
name: tuple[str, ...]
dtype: Dtype
align: Alignment
role: Role
level: int | None
tag: Tag | None
wrap: bool
break_depth: int | None
format: FormatSpec | None
raw: bool
class greater_tables.Fabricator(decorate: bool = False, pyarrow: bool = False, seed: int | None = None, now: Timestamp | str = Timestamp('2026-01-01 00:00:00'))[source]

Bases: object

Fabricate small synthetic DataFrames covering GT2’s fixture space.

Parameters:
  • decorate (bool) – Append type hints (” year”, “ ratio”, “ date”, “ time”) to generated metric names.

  • pyarrow (bool) – Convert fabricated frames to pyarrow-backed dtypes.

  • seed (int, optional) – RNG seed; one is drawn from entropy when omitted. Always pass a seed for fixtures.

  • now (pandas.Timestamp or str) – Reference timestamp for date/time columns. Fixed default keeps seeded output identical across runs.

Notes

Column type codes for data_spec:

d   date                    r   ratio (small floats)
f   float                   sN  string, N words
h   hash                    t   time (minute grid)
i   integer                 v   extreme-range float
l   log float (wide range)  wN  string, random length ~N words
p   path (filename)         x   TeX expression
                            y   year
metric_roots = ['absorption', 'acceleration', 'account', 'activation', 'adjustment', 'allocation', 'amplitude', 'approval', 'asset', 'atom', 'attrition', 'balance', 'band', 'binding', 'cancellation', 'capacitance', 'capital', 'cashflow', 'category', 'cell', 'charge', 'claim', 'commission', 'compound', 'concentration', 'conductivity', 'constraint', 'consumption', 'conversion', 'correlation', 'cost', 'count', 'coverage', 'credit', 'current', 'debt', 'decay', 'decibel', 'deductible', 'deficit', 'deflator', 'demand', 'density', 'development', 'diffusion', 'discount', 'distribution', 'dividend', 'dose', 'duration', 'earnings', 'efficiency', 'elasticity', 'employment', 'energy', 'entropy', 'enzyme', 'estimate', 'excess', 'exhaustion', 'expense', 'exposure', 'failure', 'field', 'flux', 'force', 'frequency', 'funding', 'gdp', 'gene', 'gradient', 'growth', 'half_life', 'incidence', 'income', 'index', 'indicator', 'inequality', 'inflation', 'inhibition', 'input', 'intensity', 'investment', 'kurtosis', 'lapse', 'layer', 'leverage', 'liability', 'limit', 'loss', 'luminosity', 'margin', 'mass', 'molecule', 'momentum', 'mortality', 'neutron', 'noise', 'operating', 'output', 'penalty', 'photon', 'policy', 'portfolio', 'potential', 'power', 'preference', 'premium', 'pressure', 'price', 'productivity', 'profit', 'protein', 'proton', 'provision', 'radiation', 'rate', 'ratio', 'reaction', 'recovery', 'reflection', 'refraction', 'renewal', 'reserve', 'residual', 'resistance', 'return', 'revenue', 'risk', 'sample', 'savings', 'scenario', 'score', 'sector', 'settlement', 'severity', 'shock', 'shortfall', 'signal', 'skewness', 'spread', 'strain', 'stress', 'subsidy', 'supply', 'tail', 'tariff', 'tax', 'temperature', 'tension', 'term', 'threshold', 'trade', 'trend', 'turbulence', 'unemployment', 'uptake', 'utility', 'utilization', 'valuation', 'variance', 'velocity', 'viscosity', 'volatility', 'voltage', 'volume', 'wage', 'wavelength', 'wealth', 'weight', 'yield']
static roll_columns(df: DataFrame, levels: int = -1) DataFrame[source]

Rotate the column MultiIndex levels by levels and re-sort.

static drop_singleton_levels(df: DataFrame) DataFrame[source]

Drop index/column levels holding a single value (keep at least one).

make(rows: int, data_spec, *, index_levels: int = 1, index_names=None, column_groups: int = 1, column_levels: int = 1, column_names=None, metric_name_spec='', missing: float = 0.0, simplify: bool = True, oversample: int = 1) DataFrame[source]

Fabricate a DataFrame to a specification.

Parameters:
  • rows – Number of body rows.

  • data_spec – Per-metric type codes (see class notes): a string like 's3sfid', a list of codes, or an int meaning that many floats.

  • index_levels – Row MultiIndex depth.

  • index_names – Level names; generated when omitted.

  • column_names – Level names; generated when omitted.

  • column_groups – Column MultiIndex shape: total columns = metrics × groups.

  • column_levels – Column MultiIndex shape: total columns = metrics × groups.

  • metric_name_spec'' for generated names, a list of names, or a type-code spec whose generated values become the names.

  • missing – Proportion of values per column replaced with NaN.

  • simplify – Drop index/column levels that hold a single value. Note this can reduce the requested level count on small frames.

  • oversample – Fabricate oversample × rows then keep the first rows — more distinct index values at small sizes.

Return type:

pandas.DataFrame

another() DataFrame[source]

Fabricate another DataFrame with the last make arguments.

random(rows: int = 0, columns: int = 0, index_levels: int = 0, omit: str = '') DataFrame[source]

Fabricate a DataFrame with randomly chosen shape and types.

Parameters:
  • rows – 0 means choose randomly.

  • columns – 0 means choose randomly.

  • index_levels – 0 means choose randomly.

  • omit – Type codes to exclude.

Return type:

pandas.DataFrame

metric_name(type_hint: str) str[source]

Return a one-word metric name, optionally decorated by type.

word() str[source]

Return the next word from the shuffled pool (cycles eventually).

tex() str[source]

Return the next TeX expression from the shuffled pool.

primes_for_product(n: int, v: int, shuffle: bool = False) list[int][source]

Return v distinct primes whose product is at least n.

Used to size index-level cardinalities so their product covers the requested row count.

class greater_tables.FormatSpec(*, kind: Literal['int', 'dec', 'pct', 'sci', 'gen', 'si', 'eng', 'year', 'date', 'str'], digits: Annotated[int | None, Ge(ge=0)] = None, group: bool = False, scale: float = 1.0, prefix: str | None = None, suffix: str | None = None, negative: Literal['minus', 'paren'] = 'minus', null: str = '—', zero: str | None = None, pattern: str | None = None)[source]

Bases: BaseModel

Resolved display format for one column.

Applicable by Python and by the JS walker (Intl.NumberFormat). The formatted text in cells is normative; this spec is informative — it lets a client re-render at different precision without redoing the engine’s column-scanning inference.

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

kind: FormatKind
digits: int | None
group: bool
scale: float
prefix: str | None
suffix: str | None
negative: Negative
null: str
zero: str | None
pattern: str | None
class greater_tables.HeadCell(*, text: str = '', colspan: Annotated[int, Ge(ge=1)] = 1, align: Literal['l', 'r', 'c'] | None = None, flags: tuple[Literal['neg', 'emphasis', 'muted'], ...] = (), math: bool = False, html: bool = False)[source]

Bases: BaseModel

One header cell; level is implicit in its row position within head.

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

text: str
colspan: int
align: Alignment | None
flags: tuple[CellFlag, ...]
math: bool
html: bool
class greater_tables.HouseStyle(max_rule_depth: int | Literal['auto'] | None = 'auto', max_vrule_depth: int | None = 1, max_prose_width_em: float = 18.0, max_table_width_em: float | None = None, text_ascii_rules: bool = False, tikz_scale: float = 1.0, tikz_column_sep: float = 1.0, tikz_row_sep: float = 0.25, tikz_container_env: str = 'table', tikz_latex: str | None = None, tikz_post_process: str = '', frame_rule: float = 1.0, head_rule: float = 1.0, stub_rule: float = 1.0, total_rule: float = 0.75, spanner_rule: float = 0.5, hrule_widths: tuple[float, float, float] = (0.5, 0.25, 0.25), vrule_widths: tuple[float, float, float] = (0.25, 0.25, 0.25))[source]

Bases: object

Renderer opinions — never serialized into the IR.

Rule weights are the greater_tables float scale, mapped to TikZ line styles via render.tikz.W2TIKZ (0 = don’t draw, 0.25 = ultra thin … 3 = ultra thick). HTML equivalents live as CSS custom properties in render.css.VARIABLES.

Parameters:
  • max_rule_depth (int | Literal['auto'] | None) – How deep the hierarchy is ruled. 'auto' (default) rules every break except the innermost stub level — a change there is just “next row”, so ruling it draws a line between every row; that gives no rules on a single-level index, depth 0 only on two levels, depths 0 and 1 on three. An int is literal: 0 rules only outermost changes, and on a single-level index it rules every row whose stub changed. None draws no internal rules at all.

  • max_vrule_depth (int | None) – The same for column-group verticals (None = none). Only render.policy.N_VRULE_SLOTS weights exist, so deeper breaks reuse the lightest.

  • max_prose_width_em (float) – How wide a single wrapping column may get. TikZ reads it directly; HTML emits it as --gt-prose-max. One column, not the table — the table is max_table_width_em.

  • max_table_width_em (float | None) – Total table width to aim for, in ems, or None (default) for no target. A target, not a guarantee: columns that must not wrap — numbers, dates, anything tagged or listed in TableSpec.unbreakable — hold their natural width whatever the budget says, so a table of nothing but numbers can exceed it. What the budget can do is narrow the prose columns, which it does with one shared quantile so every one of them carries the same chance of wrapping (render.widths). When even the floors do not fit, TikZ steps tikz_scale down a fixed ladder and says so through logging; HTML hands the number to the browser as a CSS max-width and lets it measure. Ems because every other geometry field here is ems: at 11pt, 8 inches is about 52em.

  • text_ascii_rules (bool) – Draw the text rendering with ASCII only — = and - for heavy and light rules, | for the stub divider, <<flag>> for the row gutter. Default False (box-drawing characters). Worth setting for a LaTeX verbatim block, where Pygments lexes U+2501 as an error token and styles it red, and for a console whose encoding or mono font cannot carry box drawing. Covers what the renderer draws, not cell text: values arrive already formatted, so FormatSpec.null (an em dash) and the SI suffix ladder (µ) still come through. Change those on the spec side if a build must be pure ASCII end to end.

max_rule_depth: int | Literal['auto'] | None = 'auto'
max_vrule_depth: int | None = 1
max_prose_width_em: float = 18.0
max_table_width_em: float | None = None
text_ascii_rules: bool = False
tikz_scale: float = 1.0
tikz_column_sep: float = 1.0
tikz_row_sep: float = 0.25
tikz_container_env: str = 'table'
tikz_latex: str | None = None
tikz_post_process: str = ''
frame_rule: float = 1.0
head_rule: float = 1.0
stub_rule: float = 1.0
total_rule: float = 0.75
spanner_rule: float = 0.5
hrule_widths: tuple[float, float, float] = (0.5, 0.25, 0.25)
vrule_widths: tuple[float, float, float] = (0.25, 0.25, 0.25)
class greater_tables.Row(*, cells: tuple[Cell | str, ...], break_depth: Annotated[int | None, Ge(ge=0)] = None, flags: tuple[Literal['total', 'subtotal', 'emphasis', 'muted'], ...] = ())[source]

Bases: BaseModel

One body/foot row.

cells holds only the uncovered columns — a stub cell with rowspan in an earlier row covers its column here, exactly as HTML <td rowspan> semantics. A cell that is plain text with no other attributes may be a bare string (the shorthand; canonicalized on dump).

break_depth is the shallowest stub level whose value changed versus the previous row (0 = outermost). House style maps depth to rule weight; the IR never names a weight.

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

cells: tuple[Cell | str, ...]
break_depth: int | None
flags: tuple[RowFlag, ...]
class greater_tables.TableDoc(*, ir_version: int = 1, generator: str | None = None, hash: Annotated[str | None, _PydanticGeneralMetadata(pattern='^[0-9a-f]{12}$')] = None, caption: str | Cell | None = None, label: str | None = None, notes: tuple[str, ...] = (), n_stub_levels: Annotated[int, Ge(ge=0)], n_head_levels: Annotated[int, Ge(ge=0)], columns: tuple[Column, ...], head: tuple[tuple[HeadCell, ...], ...], body: tuple[Row, ...], foot: tuple[Row, ...] = ())[source]

Bases: BaseModel

The table document — the versioned IR a semantic engine emits.

hash is stamped by greater_tables.engine.hashing.stamp() (sha256 over canonical bytes, hash and generator excluded).

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

ir_version: int
generator: str | None
hash: str | None
caption: str | Cell | None
label: str | None
notes: tuple[str, ...]
n_stub_levels: int
n_head_levels: int
columns: tuple[Column, ...]
head: tuple[tuple[HeadCell, ...], ...]
body: tuple[Row, ...]
foot: tuple[Row, ...]
classmethod read(source: str | bytes | dict[str, Any]) TableDoc[source]

Validate a foreign document, ignoring unknown fields.

Parameters:

source – JSON text/bytes or an already-parsed dict.

Returns:

The validated document.

Return type:

TableDoc

Raises:

ValueError – If ir_version is unsupported or the document is malformed.

class greater_tables.TableSpec(caption: str | None = None, label: str | None = None, notes: ~collections.abc.Sequence[str] = (), show_index: bool | None = None, header_row: bool = False, ratio_cols: str | ~collections.abc.Sequence[~typing.Hashable] | None = None, year_cols: str | ~collections.abc.Sequence[~typing.Hashable] | None = None, date_cols: str | ~collections.abc.Sequence[~typing.Hashable] | None = None, raw_cols: str | ~collections.abc.Sequence[~typing.Hashable] | None = None, html_cols: str | ~collections.abc.Sequence[~typing.Hashable] | None = None, unbreakable: str | ~collections.abc.Sequence[~typing.Hashable] | None = None, aligners: str | ~collections.abc.Mapping[~typing.Hashable, str] | None = None, formatters: ~collections.abc.Mapping[~typing.Hashable, ~greater_tables.ir.FormatSpec | ~collections.abc.Mapping | str | int | ~collections.abc.Callable[[~typing.Any], str]] = <factory>, float_format: ~greater_tables.ir.FormatSpec | ~collections.abc.Mapping | str | int | None = None, int_format: ~greater_tables.ir.FormatSpec | ~collections.abc.Mapping | str | int | None = None, ratio_format: ~greater_tables.ir.FormatSpec | ~collections.abc.Mapping | str | int | None = None, date_format: ~greater_tables.ir.FormatSpec | ~collections.abc.Mapping | str | int | None = None, table_float_format: ~greater_tables.ir.FormatSpec | ~collections.abc.Mapping | str | int | None = None, row_flags: ~collections.abc.Mapping[int, ~collections.abc.Sequence[str]] | ~collections.abc.Callable[[int, ~typing.Any], ~collections.abc.Sequence[str]] | None = None, cell_flags: ~collections.abc.Callable[[int, ~typing.Hashable, ~typing.Any], ~collections.abc.Sequence[str]] | None = None, math: str = 'auto', include_raw: bool | str | ~collections.abc.Sequence[~typing.Hashable] = False, max_rows: int | None = 200, sparsify: bool = True, sparsify_columns: bool = True, pef_lower: int = -3, pef_upper: int = 6, eng_digits: int = 3, eng_style: ~typing.Literal['si', 'exp'] = 'si')[source]

Bases: object

Semantic specification for one table.

Parameters:
  • caption – Table metadata; label is the Quarto cross-reference id.

  • label – Table metadata; label is the Quarto cross-reference id.

  • notes – Table metadata; label is the Quarto cross-reference id.

  • show_index (bool | None) – Show the row index as stub columns. None (default) auto-hides a trivial unnamed RangeIndex.

  • header_row (bool) – For list input: promote the first row to column headers.

  • ratio_cols – Semantic column tags: a list of labels, 'all', or a regex matched against column names.

  • year_cols – Semantic column tags: a list of labels, 'all', or a regex matched against column names.

  • date_cols – Semantic column tags: a list of labels, 'all', or a regex matched against column names.

  • raw_cols – Semantic column tags: a list of labels, 'all', or a regex matched against column names.

  • html_cols (str | collections.abc.Sequence[Hashable] | None) – String columns whose cells carry raw HTML (sets the cell html flag; renderers still gate on their own allow-html option).

  • unbreakable (str | collections.abc.Sequence[Hashable] | None) – Columns forced to wrap=False regardless of dtype.

  • aligners (str | collections.abc.Mapping[Hashable, str] | None) – 'lrc'-style string (positional) or mapping label → ‘l’/’r’/’c’.

  • formatters (collections.abc.Mapping[Hashable, greater_tables.ir.FormatSpec | collections.abc.Mapping | str | int | collections.abc.Callable[[Any], str]]) – Per-column format override, keyed by column label (a tuple label also matches on its leaf name). Values: a FormatSpec, a mapping of FormatSpec fields (the YAML form), a sugar string (csv-grid ',.1f' or GT '{x:.1%}' style), an int (decimal digits, with grouping), or a callable taking the value and returning a string. A callable owns its own NaN handling and does not reach the IR, so a client cannot re-render that column.

  • float_format – House defaults, one per kind — the 5.x default_*_str settings. Each overrides only its own kind’s built-in default and loses to an explicit formatters entry. date_format covers the date tag and both date and datetime dtypes. Setting float_format turns the float inference off entirely, including the automatic engineering/SI switch, so pef_* and eng_* stop applying. Callables are rejected: these are house rules that must be YAML-expressible.

  • int_format – House defaults, one per kind — the 5.x default_*_str settings. Each overrides only its own kind’s built-in default and loses to an explicit formatters entry. date_format covers the date tag and both date and datetime dtypes. Setting float_format turns the float inference off entirely, including the automatic engineering/SI switch, so pef_* and eng_* stop applying. Callables are rejected: these are house rules that must be YAML-expressible.

  • ratio_format – House defaults, one per kind — the 5.x default_*_str settings. Each overrides only its own kind’s built-in default and loses to an explicit formatters entry. date_format covers the date tag and both date and datetime dtypes. Setting float_format turns the float inference off entirely, including the automatic engineering/SI switch, so pef_* and eng_* stop applying. Callables are rejected: these are house rules that must be YAML-expressible.

  • date_format – House defaults, one per kind — the 5.x default_*_str settings. Each overrides only its own kind’s built-in default and loses to an explicit formatters entry. date_format covers the date tag and both date and datetime dtypes. Setting float_format turns the float inference off entirely, including the automatic engineering/SI switch, so pef_* and eng_* stop applying. Callables are rejected: these are house rules that must be YAML-expressible.

  • table_float_format (greater_tables.ir.FormatSpec | collections.abc.Mapping | str | int | None) – The sledgehammer: every untagged float column, overriding even an explicit formatters entry. Ratio, year and date columns have a kind, so it never reaches them. Default None, and deliberately absent from config.write_template — set it consciously at a call site, never in a config file where it would silently defeat per-column formats months later.

  • row_flags (collections.abc.Mapping[int, collections.abc.Sequence[str]] | collections.abc.Callable[[int, Any], collections.abc.Sequence[str]] | None) – Mapping row position (negative allowed) → flags, or a callable (position, row_series) -> flags. Vocabulary: total, subtotal, emphasis, muted.

  • cell_flags (collections.abc.Callable[[int, Hashable, Any], collections.abc.Sequence[str]] | None) – Callable (position, column_label, value) -> flags for per-cell emphasis/muted. The neg flag is stamped automatically.

  • math (str) – 'auto' detects $...$-wrapped string cells as math; 'off' disables.

  • include_raw (bool | str | collections.abc.Sequence[Hashable]) – Carry raw values in body cells: False (default), True/’data’ (numeric/date/bool data columns), or an explicit column list. Data columns only in every form — stub (index) columns never carry raw values, so a client that must sort on an index level wants df.reset_index() before build.

  • max_rows (int | None) – Truncate longer tables, appending a note. None = unlimited.

  • sparsify – Merge repeated stub values into rowspans / repeated header prefixes into colspans.

  • sparsify_columns – Merge repeated stub values into rowspans / repeated header prefixes into colspans.

  • pef_lower – Engineering-format bounds: a float column whose mean magnitude falls outside [10**pef_lower, 10**pef_upper] renders in engineering notation, as does one whose nonzero values span more than 10**pef_upper between largest and smallest (column-level decisions — 5.x switched per cell, mixing formats within a column).

  • pef_upper – Engineering-format bounds: a float column whose mean magnitude falls outside [10**pef_lower, 10**pef_upper] renders in engineering notation, as does one whose nonzero values span more than 10**pef_upper between largest and smallest (column-level decisions — 5.x switched per cell, mixing formats within a column).

  • eng_digits – What engineering format looks like when it fires: decimals, and 'si' suffixes (µ m k M G …) or 'exp' aligned exponents. Both are dead if float_format is set — that turns inference off.

  • eng_style – What engineering format looks like when it fires: decimals, and 'si' suffixes (µ m k M G …) or 'exp' aligned exponents. Both are dead if float_format is set — that turns inference off.

caption: str | None = None
label: str | None = None
notes: Sequence[str] = ()
show_index: bool | None = None
header_row: bool = False
ratio_cols: str | Sequence[Hashable] | None = None
year_cols: str | Sequence[Hashable] | None = None
date_cols: str | Sequence[Hashable] | None = None
raw_cols: str | Sequence[Hashable] | None = None
html_cols: str | Sequence[Hashable] | None = None
unbreakable: str | Sequence[Hashable] | None = None
aligners: str | Mapping[Hashable, str] | None = None
formatters: Mapping[Hashable, FormatSpec | Mapping | str | int | Callable[[Any], str]]
float_format: FormatSpec | Mapping | str | int | None = None
int_format: FormatSpec | Mapping | str | int | None = None
ratio_format: FormatSpec | Mapping | str | int | None = None
date_format: FormatSpec | Mapping | str | int | None = None
table_float_format: FormatSpec | Mapping | str | int | None = None
row_flags: Mapping[int, Sequence[str]] | Callable[[int, Any], Sequence[str]] | None = None
cell_flags: Callable[[int, Hashable, Any], Sequence[str]] | None = None
math: str = 'auto'
include_raw: bool | str | Sequence[Hashable] = False
max_rows: int | None = 200
sparsify: bool = True
sparsify_columns: bool = True
pef_lower: int = -3
pef_upper: int = 6
eng_digits: int = 3
eng_style: Literal['si', 'exp'] = 'si'
greater_tables.build(data, spec: TableSpec | None = None) TableDoc[source]

Build a stamped table document from data plus a semantic spec.

Parameters:
  • data – DataFrame, Series, list of row-lists (spec.header_row promotes the first), or a markdown pipe-table string (caption/label/alignment extracted and used where the spec leaves them unset).

  • spec – Semantic specification; defaults to TableSpec().

Returns:

Validated, hash-stamped document.

Return type:

TableDoc

Notes

A DataFrame with default RangeIndex columns is treated as headerless; an unnamed RangeIndex row index is auto-hidden unless spec.show_index says otherwise.

greater_tables.canonical_dict(doc: TableDoc, *, include_hash: bool = True) dict[str, Any][source]

Return the canonical plain-dict form of a document.

Deterministic field presence: structural fields always appear; optional fields appear only when they differ from their defaults; trivial cells collapse to bare strings; all strings are NFC-normalized. Key ordering is left to the JSON writer (sorted there).

Parameters:
  • doc – The document to canonicalize.

  • include_hash – When False, hash and generator are omitted — the form that content hashing operates on.

greater_tables.canonical_json(doc: TableDoc, *, include_hash: bool = True) bytes[source]

Serialize a document to canonical UTF-8 JSON bytes.

Sorted keys, compact separators, NFC strings (applied by greater_tables.ir.canonical_dict()), no NaN/Infinity — a raw value that is not JSON-representable is an engine bug, surfaced here.

Parameters:
  • doc – The document to serialize.

  • include_hash – When False, omit hash and generator (the hashed form).

greater_tables.doc_hash(doc: TableDoc) str[source]

Return the 12-hex-char sha256 content hash of a document.

greater_tables.load_spec(path: str | Path) TableSpec[source]

Load a TableSpec from a YAML mapping file.

notes arrives from YAML as a list and is converted to the tuple the dataclass holds, so a loaded spec compares equal to the Python-built one (and write_template(path, spec) round-trips).

greater_tables.load_style(path: str | Path) HouseStyle[source]

Load a HouseStyle from a YAML mapping file.

Tuple-valued fields (hrule_widths, vrule_widths) arrive from YAML as lists and are converted, so hrule_widths: [1, 0.5, 0] works.

greater_tables.quick_fab(rows: int = 10, data_spec: str = 's3sfid', *, seed: int | None = None, pyarrow: bool = False, **kwargs) DataFrame[source]

One-stop fabrication of a DataFrame (see Fabricator.make()).

greater_tables.render_html(doc: TableDoc, *, css: str = 'embed', inline_css: bool = False, allow_html: bool = True, style: HouseStyle | None = None) str[source]

Render a table document to an HTML fragment.

Parameters:
  • doc – The document.

  • css'embed' (default), 'once', or 'none'.

  • inline_css – Flatten the stylesheet onto style attributes (HTMX/email); implies css='none'.

  • allow_html – Honor cell html: true. Off ⇒ such cells render as escaped source.

  • style – House style. max_rule_depth and max_vrule_depth are the only fields this renderer reads — they decide which breaks get a gt-break-{slot} / gt-vbreak-{slot} class. Rule weights are CSS custom properties (--gt-rule-{slot}, --gt-vrule-{slot}), set in a stylesheet, not here.

greater_tables.render_text(doc: TableDoc, style: HouseStyle | None = None) str[source]

Render a table document to monospace text.

Two HouseStyle fields reach here. max_rule_depth sets how deep breaks are ruled — text has no way to express a rule weight, so every drawn break looks the same. text_ascii_rules swaps ASCII_CHARS for the default UNICODE_CHARS.

greater_tables.render_tikz(doc: TableDoc, style: HouseStyle | None = None) str[source]

Render a table document to a TikZ table (LaTeX source).

greater_tables.stamp(doc: TableDoc, generator: str | None = None) TableDoc[source]

Return a copy of doc with hash (and optionally generator) set.

Specification

User-facing table specification (semantics) and house style (opinions).

TableSpec carries everything the engine needs to build a TableDoc — semantic choices that ride with the table. HouseStyle carries renderer opinions (rule weights, fonts, spacing) that must never enter the IR; it is populated as renderers arrive.

class greater_tables.spec.HouseStyle(max_rule_depth: int | Literal['auto'] | None = 'auto', max_vrule_depth: int | None = 1, max_prose_width_em: float = 18.0, max_table_width_em: float | None = None, text_ascii_rules: bool = False, tikz_scale: float = 1.0, tikz_column_sep: float = 1.0, tikz_row_sep: float = 0.25, tikz_container_env: str = 'table', tikz_latex: str | None = None, tikz_post_process: str = '', frame_rule: float = 1.0, head_rule: float = 1.0, stub_rule: float = 1.0, total_rule: float = 0.75, spanner_rule: float = 0.5, hrule_widths: tuple[float, float, float] = (0.5, 0.25, 0.25), vrule_widths: tuple[float, float, float] = (0.25, 0.25, 0.25))[source]

Bases: object

Renderer opinions — never serialized into the IR.

Rule weights are the greater_tables float scale, mapped to TikZ line styles via render.tikz.W2TIKZ (0 = don’t draw, 0.25 = ultra thin … 3 = ultra thick). HTML equivalents live as CSS custom properties in render.css.VARIABLES.

Parameters:
  • max_rule_depth (int | Literal['auto'] | None) – How deep the hierarchy is ruled. 'auto' (default) rules every break except the innermost stub level — a change there is just “next row”, so ruling it draws a line between every row; that gives no rules on a single-level index, depth 0 only on two levels, depths 0 and 1 on three. An int is literal: 0 rules only outermost changes, and on a single-level index it rules every row whose stub changed. None draws no internal rules at all.

  • max_vrule_depth (int | None) – The same for column-group verticals (None = none). Only render.policy.N_VRULE_SLOTS weights exist, so deeper breaks reuse the lightest.

  • max_prose_width_em (float) – How wide a single wrapping column may get. TikZ reads it directly; HTML emits it as --gt-prose-max. One column, not the table — the table is max_table_width_em.

  • max_table_width_em (float | None) – Total table width to aim for, in ems, or None (default) for no target. A target, not a guarantee: columns that must not wrap — numbers, dates, anything tagged or listed in TableSpec.unbreakable — hold their natural width whatever the budget says, so a table of nothing but numbers can exceed it. What the budget can do is narrow the prose columns, which it does with one shared quantile so every one of them carries the same chance of wrapping (render.widths). When even the floors do not fit, TikZ steps tikz_scale down a fixed ladder and says so through logging; HTML hands the number to the browser as a CSS max-width and lets it measure. Ems because every other geometry field here is ems: at 11pt, 8 inches is about 52em.

  • text_ascii_rules (bool) – Draw the text rendering with ASCII only — = and - for heavy and light rules, | for the stub divider, <<flag>> for the row gutter. Default False (box-drawing characters). Worth setting for a LaTeX verbatim block, where Pygments lexes U+2501 as an error token and styles it red, and for a console whose encoding or mono font cannot carry box drawing. Covers what the renderer draws, not cell text: values arrive already formatted, so FormatSpec.null (an em dash) and the SI suffix ladder (µ) still come through. Change those on the spec side if a build must be pure ASCII end to end.

max_rule_depth: int | Literal['auto'] | None = 'auto'
max_vrule_depth: int | None = 1
max_prose_width_em: float = 18.0
max_table_width_em: float | None = None
text_ascii_rules: bool = False
tikz_scale: float = 1.0
tikz_column_sep: float = 1.0
tikz_row_sep: float = 0.25
tikz_container_env: str = 'table'
tikz_latex: str | None = None
tikz_post_process: str = ''
frame_rule: float = 1.0
head_rule: float = 1.0
stub_rule: float = 1.0
total_rule: float = 0.75
spanner_rule: float = 0.5
hrule_widths: tuple[float, float, float] = (0.5, 0.25, 0.25)
vrule_widths: tuple[float, float, float] = (0.25, 0.25, 0.25)
class greater_tables.spec.TableSpec(caption: str | None = None, label: str | None = None, notes: ~collections.abc.Sequence[str] = (), show_index: bool | None = None, header_row: bool = False, ratio_cols: str | ~collections.abc.Sequence[~typing.Hashable] | None = None, year_cols: str | ~collections.abc.Sequence[~typing.Hashable] | None = None, date_cols: str | ~collections.abc.Sequence[~typing.Hashable] | None = None, raw_cols: str | ~collections.abc.Sequence[~typing.Hashable] | None = None, html_cols: str | ~collections.abc.Sequence[~typing.Hashable] | None = None, unbreakable: str | ~collections.abc.Sequence[~typing.Hashable] | None = None, aligners: str | ~collections.abc.Mapping[~typing.Hashable, str] | None = None, formatters: ~collections.abc.Mapping[~typing.Hashable, ~greater_tables.ir.FormatSpec | ~collections.abc.Mapping | str | int | ~collections.abc.Callable[[~typing.Any], str]] = <factory>, float_format: ~greater_tables.ir.FormatSpec | ~collections.abc.Mapping | str | int | None = None, int_format: ~greater_tables.ir.FormatSpec | ~collections.abc.Mapping | str | int | None = None, ratio_format: ~greater_tables.ir.FormatSpec | ~collections.abc.Mapping | str | int | None = None, date_format: ~greater_tables.ir.FormatSpec | ~collections.abc.Mapping | str | int | None = None, table_float_format: ~greater_tables.ir.FormatSpec | ~collections.abc.Mapping | str | int | None = None, row_flags: ~collections.abc.Mapping[int, ~collections.abc.Sequence[str]] | ~collections.abc.Callable[[int, ~typing.Any], ~collections.abc.Sequence[str]] | None = None, cell_flags: ~collections.abc.Callable[[int, ~typing.Hashable, ~typing.Any], ~collections.abc.Sequence[str]] | None = None, math: str = 'auto', include_raw: bool | str | ~collections.abc.Sequence[~typing.Hashable] = False, max_rows: int | None = 200, sparsify: bool = True, sparsify_columns: bool = True, pef_lower: int = -3, pef_upper: int = 6, eng_digits: int = 3, eng_style: ~typing.Literal['si', 'exp'] = 'si')[source]

Bases: object

Semantic specification for one table.

Parameters:
  • caption – Table metadata; label is the Quarto cross-reference id.

  • label – Table metadata; label is the Quarto cross-reference id.

  • notes – Table metadata; label is the Quarto cross-reference id.

  • show_index (bool | None) – Show the row index as stub columns. None (default) auto-hides a trivial unnamed RangeIndex.

  • header_row (bool) – For list input: promote the first row to column headers.

  • ratio_cols – Semantic column tags: a list of labels, 'all', or a regex matched against column names.

  • year_cols – Semantic column tags: a list of labels, 'all', or a regex matched against column names.

  • date_cols – Semantic column tags: a list of labels, 'all', or a regex matched against column names.

  • raw_cols – Semantic column tags: a list of labels, 'all', or a regex matched against column names.

  • html_cols (str | collections.abc.Sequence[Hashable] | None) – String columns whose cells carry raw HTML (sets the cell html flag; renderers still gate on their own allow-html option).

  • unbreakable (str | collections.abc.Sequence[Hashable] | None) – Columns forced to wrap=False regardless of dtype.

  • aligners (str | collections.abc.Mapping[Hashable, str] | None) – 'lrc'-style string (positional) or mapping label → ‘l’/’r’/’c’.

  • formatters (collections.abc.Mapping[Hashable, greater_tables.ir.FormatSpec | collections.abc.Mapping | str | int | collections.abc.Callable[[Any], str]]) – Per-column format override, keyed by column label (a tuple label also matches on its leaf name). Values: a FormatSpec, a mapping of FormatSpec fields (the YAML form), a sugar string (csv-grid ',.1f' or GT '{x:.1%}' style), an int (decimal digits, with grouping), or a callable taking the value and returning a string. A callable owns its own NaN handling and does not reach the IR, so a client cannot re-render that column.

  • float_format – House defaults, one per kind — the 5.x default_*_str settings. Each overrides only its own kind’s built-in default and loses to an explicit formatters entry. date_format covers the date tag and both date and datetime dtypes. Setting float_format turns the float inference off entirely, including the automatic engineering/SI switch, so pef_* and eng_* stop applying. Callables are rejected: these are house rules that must be YAML-expressible.

  • int_format – House defaults, one per kind — the 5.x default_*_str settings. Each overrides only its own kind’s built-in default and loses to an explicit formatters entry. date_format covers the date tag and both date and datetime dtypes. Setting float_format turns the float inference off entirely, including the automatic engineering/SI switch, so pef_* and eng_* stop applying. Callables are rejected: these are house rules that must be YAML-expressible.

  • ratio_format – House defaults, one per kind — the 5.x default_*_str settings. Each overrides only its own kind’s built-in default and loses to an explicit formatters entry. date_format covers the date tag and both date and datetime dtypes. Setting float_format turns the float inference off entirely, including the automatic engineering/SI switch, so pef_* and eng_* stop applying. Callables are rejected: these are house rules that must be YAML-expressible.

  • date_format – House defaults, one per kind — the 5.x default_*_str settings. Each overrides only its own kind’s built-in default and loses to an explicit formatters entry. date_format covers the date tag and both date and datetime dtypes. Setting float_format turns the float inference off entirely, including the automatic engineering/SI switch, so pef_* and eng_* stop applying. Callables are rejected: these are house rules that must be YAML-expressible.

  • table_float_format (greater_tables.ir.FormatSpec | collections.abc.Mapping | str | int | None) – The sledgehammer: every untagged float column, overriding even an explicit formatters entry. Ratio, year and date columns have a kind, so it never reaches them. Default None, and deliberately absent from config.write_template — set it consciously at a call site, never in a config file where it would silently defeat per-column formats months later.

  • row_flags (collections.abc.Mapping[int, collections.abc.Sequence[str]] | collections.abc.Callable[[int, Any], collections.abc.Sequence[str]] | None) – Mapping row position (negative allowed) → flags, or a callable (position, row_series) -> flags. Vocabulary: total, subtotal, emphasis, muted.

  • cell_flags (collections.abc.Callable[[int, Hashable, Any], collections.abc.Sequence[str]] | None) – Callable (position, column_label, value) -> flags for per-cell emphasis/muted. The neg flag is stamped automatically.

  • math (str) – 'auto' detects $...$-wrapped string cells as math; 'off' disables.

  • include_raw (bool | str | collections.abc.Sequence[Hashable]) – Carry raw values in body cells: False (default), True/’data’ (numeric/date/bool data columns), or an explicit column list. Data columns only in every form — stub (index) columns never carry raw values, so a client that must sort on an index level wants df.reset_index() before build.

  • max_rows (int | None) – Truncate longer tables, appending a note. None = unlimited.

  • sparsify – Merge repeated stub values into rowspans / repeated header prefixes into colspans.

  • sparsify_columns – Merge repeated stub values into rowspans / repeated header prefixes into colspans.

  • pef_lower – Engineering-format bounds: a float column whose mean magnitude falls outside [10**pef_lower, 10**pef_upper] renders in engineering notation, as does one whose nonzero values span more than 10**pef_upper between largest and smallest (column-level decisions — 5.x switched per cell, mixing formats within a column).

  • pef_upper – Engineering-format bounds: a float column whose mean magnitude falls outside [10**pef_lower, 10**pef_upper] renders in engineering notation, as does one whose nonzero values span more than 10**pef_upper between largest and smallest (column-level decisions — 5.x switched per cell, mixing formats within a column).

  • eng_digits – What engineering format looks like when it fires: decimals, and 'si' suffixes (µ m k M G …) or 'exp' aligned exponents. Both are dead if float_format is set — that turns inference off.

  • eng_style – What engineering format looks like when it fires: decimals, and 'si' suffixes (µ m k M G …) or 'exp' aligned exponents. Both are dead if float_format is set — that turns inference off.

caption: str | None = None
label: str | None = None
notes: Sequence[str] = ()
show_index: bool | None = None
header_row: bool = False
ratio_cols: str | Sequence[Hashable] | None = None
year_cols: str | Sequence[Hashable] | None = None
date_cols: str | Sequence[Hashable] | None = None
raw_cols: str | Sequence[Hashable] | None = None
html_cols: str | Sequence[Hashable] | None = None
unbreakable: str | Sequence[Hashable] | None = None
aligners: str | Mapping[Hashable, str] | None = None
formatters: Mapping[Hashable, FormatSpec | Mapping | str | int | Callable[[Any], str]]
float_format: FormatSpec | Mapping | str | int | None = None
int_format: FormatSpec | Mapping | str | int | None = None
ratio_format: FormatSpec | Mapping | str | int | None = None
date_format: FormatSpec | Mapping | str | int | None = None
table_float_format: FormatSpec | Mapping | str | int | None = None
row_flags: Mapping[int, Sequence[str]] | Callable[[int, Any], Sequence[str]] | None = None
cell_flags: Callable[[int, Hashable, Any], Sequence[str]] | None = None
math: str = 'auto'
include_raw: bool | str | Sequence[Hashable] = False
max_rows: int | None = 200
sparsify: bool = True
sparsify_columns: bool = True
pef_lower: int = -3
pef_upper: int = 6
eng_digits: int = 3
eng_style: Literal['si', 'exp'] = 'si'

YAML configuration: load a TableSpec (project defaults or per-table).

YAML is this project’s single config format (pyproject.toml aside). A file is a plain mapping of field names; unknown keys raise so typos surface. Callable fields (row_flags, cell_flags) are code, not config.

Two files, because the two objects mean different things: a spec file holds semantics that travel with the table (formats, column tags), a style file holds renderer opinions that do not (rule depth, TikZ geometry). Keeping them apart is the same split as greater_tables.spec; a single file with two sections would blur it.

greater_tables.config.load_spec(path: str | Path) TableSpec[source]

Load a TableSpec from a YAML mapping file.

notes arrives from YAML as a list and is converted to the tuple the dataclass holds, so a loaded spec compares equal to the Python-built one (and write_template(path, spec) round-trips).

greater_tables.config.load_style(path: str | Path) HouseStyle[source]

Load a HouseStyle from a YAML mapping file.

Tuple-valued fields (hrule_widths, vrule_widths) arrive from YAML as lists and are converted, so hrule_widths: [1, 0.5, 0] works.

greater_tables.config.spec_to_yaml(spec: TableSpec) str[source]

Serialize a TableSpec’s effective values to YAML.

Raises:

ValueError – If row_flags or cell_flags holds a callable — code has no YAML form, and emitting a function repr would produce a file that cannot be loaded back.

greater_tables.config.write_template(path: str | Path, spec: TableSpec | None = None) Path[source]

Write a YAML TableSpec template, or dump one spec’s effective values.

With spec=None every field appears commented out with a one-line hint — a starting point to copy from. Pass a spec and its actual settings are written uncommented, so the round trip is write_template('house.yaml', gt.spec) → edit → config_path=.

table_float_format is never written: it overrides even per-column formatters, which makes it right for a deliberate call site and wrong in a file that outlives the reason you set it.

Facade

GT — the greater_tables constructor-compatible facade.

Thin, not bug-for-bug: the common blog/Quarto call GT(df, caption=..., ratio_cols=..., formatters=...) works and renders via the GT2 engine; exotic 5.x config knobs are accepted with a warning and ignored. Everything is lazy cached — constructing a GT computes nothing, and TikZ is never computed on the HTML path (the 5.x eager-TikZ waste is gone by construction).

formatters is passed straight through: it is a real TableSpec field, not a translated shim. One deliberate difference from 5.x — a bare int now groups ({'paid': 2}1,234.57, where 5.x meant 1234.57), because GT2 groups whenever it chooses the format for you. Write '.2f' for the ungrouped form.

class greater_tables.compat.GT(data=None, caption=None, label=None, aligners=None, formatters=None, unbreakable=None, ratio_cols=None, year_cols=None, date_cols=None, raw_cols=None, show_index=None, config_path=None, style=None, mimes=None, **overrides)[source]

Bases: object

Table facade: data + spec in, renderings out (all lazy, all cached).

Parameters mirror the greater_tables constructor where they still make sense; formatters accepts the 5.x shapes (callable, '{x:...}' format string, or int = decimal digits, now grouped). config_path loads YAML TableSpec defaults which explicit arguments override. style takes a HouseStyle and is forwarded to every renderer. mimes pins what _repr_mimebundle_ publishes — None (default) detects the target, an explicit tuple such as ('text/plain',) publishes exactly that. It is a whole-document policy, so the intended form is render = partial(GT, mimes=('text/plain',)). Additional keyword arguments naming TableSpec fields pass through; unknown ones warn and are ignored (migration aid), and a HouseStyle field name warns with the fix.

property doc: TableDoc[source]

The table document (the IR) — built on first access.

property html: str[source]

HTML fragment (stylesheet embedded).

property text: str[source]

Monospace text rendering (the conformance oracle’s view).

property tikz: str[source]

TikZ/LaTeX rendering (table environment + tikzpicture).

The IR

Table-document IR: frozen pydantic models, ir_version 1.

The IR is the contract between the semantic engine and every renderer (Python HTML, TikZ, text, and the JS walker). It carries semantics only: no widths, no rule weights, no CSS, no fonts.

Two access paths with different strictness:

  • Write (producer): construct models directly; unknown fields raise (extra='forbid'), so producer typos surface immediately.

  • Read (consumer): TableDoc.read() strips unknown fields before validation, so documents from a newer producer with additive advisory fields remain readable (“must ignore” rule).

Canonical serialization lives in canonical_dict(): deterministic field presence (defaults omitted), NFC-normalized strings, and the bare-string cell shorthand. greater_tables.engine.hashing turns that dict into canonical bytes and the content hash.

class greater_tables.ir.Cell(*, text: str, raw: float | int | str | bool | None = None, rowspan: Annotated[int, Ge(ge=1)] = 1, colspan: Annotated[int, Ge(ge=1)] = 1, flags: tuple[Literal['neg', 'emphasis', 'muted'], ...] = (), math: bool = False, html: bool = False)[source]

Bases: BaseModel

One body/foot cell.

text is plain unicode — each renderer applies its own escaping. raw is only present when the owning column advertises raw: true; an explicit JSON null there means a missing value, which is distinct from the field being absent (tracked via pydantic’s fields-set).

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

text: str
raw: float | int | str | bool | None
rowspan: int
colspan: int
flags: tuple[CellFlag, ...]
math: bool
html: bool
class greater_tables.ir.Column(*, key: str, name: tuple[str, ...], dtype: Literal['int', 'float', 'date', 'datetime', 'bool', 'string', 'object'], align: Literal['l', 'r', 'c'], role: Literal['stub', 'data'] = 'data', level: Annotated[int | None, Ge(ge=0)] = None, tag: Literal['ratio', 'year', 'date', 'raw'] | None = None, wrap: bool = False, break_depth: Annotated[int | None, Ge(ge=0)] = None, format: FormatSpec | None = None, raw: bool = False)[source]

Bases: BaseModel

Descriptor for one visual column, stubs first.

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

key: str
name: tuple[str, ...]
dtype: Dtype
align: Alignment
role: Role
level: int | None
tag: Tag | None
wrap: bool
break_depth: int | None
format: FormatSpec | None
raw: bool
class greater_tables.ir.FormatSpec(*, kind: Literal['int', 'dec', 'pct', 'sci', 'gen', 'si', 'eng', 'year', 'date', 'str'], digits: Annotated[int | None, Ge(ge=0)] = None, group: bool = False, scale: float = 1.0, prefix: str | None = None, suffix: str | None = None, negative: Literal['minus', 'paren'] = 'minus', null: str = '—', zero: str | None = None, pattern: str | None = None)[source]

Bases: BaseModel

Resolved display format for one column.

Applicable by Python and by the JS walker (Intl.NumberFormat). The formatted text in cells is normative; this spec is informative — it lets a client re-render at different precision without redoing the engine’s column-scanning inference.

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

kind: FormatKind
digits: int | None
group: bool
scale: float
prefix: str | None
suffix: str | None
negative: Negative
null: str
zero: str | None
pattern: str | None
class greater_tables.ir.HeadCell(*, text: str = '', colspan: Annotated[int, Ge(ge=1)] = 1, align: Literal['l', 'r', 'c'] | None = None, flags: tuple[Literal['neg', 'emphasis', 'muted'], ...] = (), math: bool = False, html: bool = False)[source]

Bases: BaseModel

One header cell; level is implicit in its row position within head.

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

text: str
colspan: int
align: Alignment | None
flags: tuple[CellFlag, ...]
math: bool
html: bool
class greater_tables.ir.Row(*, cells: tuple[Cell | str, ...], break_depth: Annotated[int | None, Ge(ge=0)] = None, flags: tuple[Literal['total', 'subtotal', 'emphasis', 'muted'], ...] = ())[source]

Bases: BaseModel

One body/foot row.

cells holds only the uncovered columns — a stub cell with rowspan in an earlier row covers its column here, exactly as HTML <td rowspan> semantics. A cell that is plain text with no other attributes may be a bare string (the shorthand; canonicalized on dump).

break_depth is the shallowest stub level whose value changed versus the previous row (0 = outermost). House style maps depth to rule weight; the IR never names a weight.

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

cells: tuple[Cell | str, ...]
break_depth: int | None
flags: tuple[RowFlag, ...]
class greater_tables.ir.TableDoc(*, ir_version: int = 1, generator: str | None = None, hash: Annotated[str | None, _PydanticGeneralMetadata(pattern='^[0-9a-f]{12}$')] = None, caption: str | Cell | None = None, label: str | None = None, notes: tuple[str, ...] = (), n_stub_levels: Annotated[int, Ge(ge=0)], n_head_levels: Annotated[int, Ge(ge=0)], columns: tuple[Column, ...], head: tuple[tuple[HeadCell, ...], ...], body: tuple[Row, ...], foot: tuple[Row, ...] = ())[source]

Bases: BaseModel

The table document — the versioned IR a semantic engine emits.

hash is stamped by greater_tables.engine.hashing.stamp() (sha256 over canonical bytes, hash and generator excluded).

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

ir_version: int
generator: str | None
hash: str | None
caption: str | Cell | None
label: str | None
notes: tuple[str, ...]
n_stub_levels: int
n_head_levels: int
columns: tuple[Column, ...]
head: tuple[tuple[HeadCell, ...], ...]
body: tuple[Row, ...]
foot: tuple[Row, ...]
classmethod read(source: str | bytes | dict[str, Any]) TableDoc[source]

Validate a foreign document, ignoring unknown fields.

Parameters:

source – JSON text/bytes or an already-parsed dict.

Returns:

The validated document.

Return type:

TableDoc

Raises:

ValueError – If ir_version is unsupported or the document is malformed.

greater_tables.ir.canonical_dict(doc: TableDoc, *, include_hash: bool = True) dict[str, Any][source]

Return the canonical plain-dict form of a document.

Deterministic field presence: structural fields always appear; optional fields appear only when they differ from their defaults; trivial cells collapse to bare strings; all strings are NFC-normalized. Key ordering is left to the JSON writer (sorted there).

Parameters:
  • doc – The document to canonicalize.

  • include_hash – When False, hash and generator are omitted — the form that content hashing operates on.

Engine

build(data, spec) -> TableDoc — the semantic engine orchestrator.

Accepts a DataFrame, Series, list of rows, or a markdown pipe table. Never mutates its input. Everything semantic is decided here — dtypes, tags, formats, alignment, hierarchy, flags — and nothing stylistic.

greater_tables.engine.build.build(data, spec: TableSpec | None = None) TableDoc[source]

Build a stamped table document from data plus a semantic spec.

Parameters:
  • data – DataFrame, Series, list of row-lists (spec.header_row promotes the first), or a markdown pipe-table string (caption/label/alignment extracted and used where the spec leaves them unset).

  • spec – Semantic specification; defaults to TableSpec().

Returns:

Validated, hash-stamped document.

Return type:

TableDoc

Notes

A DataFrame with default RangeIndex columns is treated as headerless; an unnamed RangeIndex row index is auto-hidden unless spec.show_index says otherwise.

Column dtype discovery, semantic tags, alignment, and wrap hints.

Arrow-aware via pandas.api.types predicates — dtypes are trusted, never coerced (the greater_tables float-coercion loop does not come along).

greater_tables.engine.dtypes.alignment(col: Hashable, *, is_stub: bool, dtype: Literal['int', 'float', 'date', 'datetime', 'bool', 'string', 'object'], tag: Literal['ratio', 'year', 'date', 'raw'] | None, aligners: dict[Hashable, str]) Literal['l', 'r', 'c'][source]

Alignment ladder: user > stub-left > year-center > raw-left > date-center > numeric-right > left.

greater_tables.engine.dtypes.column_dtype(ser: Series) Literal['int', 'float', 'date', 'datetime', 'bool', 'string', 'object'][source]

Classify a series into an IR dtype.

Datetime columns whose values all sit at midnight classify as date; any time-of-day component makes them datetime.

greater_tables.engine.dtypes.resolve_cols(selector: str | Sequence[Hashable] | None, columns: list[Hashable]) set[Hashable][source]

Resolve a column selector to a set of labels from columns.

A list matches labels exactly (and leaf names for tuple labels); 'all' matches everything; any other string is a regex searched against the leaf name and the full label.

greater_tables.engine.dtypes.semantic_tag(col: Hashable, tags: dict[str, set[Hashable]]) Literal['ratio', 'year', 'date', 'raw'] | None[source]

Return the winning semantic tag for a column (GT ladder order).

greater_tables.engine.dtypes.wrap_hint(dtype: Literal['int', 'float', 'date', 'datetime', 'bool', 'string', 'object'], tag: Literal['ratio', 'year', 'date', 'raw'] | None, col: Hashable, unbreakable: set[Hashable]) bool[source]

Prose (string/object, untagged) columns may wrap; everything else not.

Format sugar parsing, column-statistics inference, and application.

The resolution ladder (user > semantic tag > dtype) produces FormatSpec data objects — never closures, so the greater_tables late-binding formatter bug cannot recur. Application is a pure function of (value, spec).

Sugar accepted, both prior vocabularies:

  • csv-grid spec strings: ','? '.N'? then one of f d % e g s, plus 'year', 'eng', 'si', and strftime patterns for dates.

  • GT f-strings: '{x:,.1f}', '{x:.1%}' — the python format mini-spec.

  • An int means that many decimal digits.

g is the one kind whose digits counts significant figures rather than decimal places, because that is what python’s g presentation type means; '.3g' renders 1234.5 as 1.23e+03 and 0.0012345 as 0.00123. Uppercase G/E are still unparsed — see dev/pending.md.

greater_tables.engine.formats.apply_format(value, fs: FormatSpec | None) str[source]

Format one value. Pure; NaN-safe; never raises on odd input.

greater_tables.engine.formats.infer_numeric_format(ser: Series, *, pef_lower: int = -3, pef_upper: int = 6, digits: int = 3, style: str = 'si') FormatSpec[source]

Choose a float column’s format from its statistics (GT salvage).

Digits bucket by mean magnitude (<1 → 5, <10 → 3, <20000 → 2, else 0). Engineering notation on either trigger: mean magnitude outside [10**pef_lower, 10**pef_upper] (the GT rule), or nonzero values spanning more than 10**pef_upper orders-of-magnitude ratio (the csv-grid rule — no single fixed precision serves such a column), rendered with digits decimals in style ('si' suffixes or 'exp' aligned exponents). Column-level decision — GT 5.x switched per cell, which mixed formats within a column.

This is the guess, and it only runs when the caller has not stated a house rule: TableSpec.float_format (or table_float_format) short-circuits it in greater_tables.engine.build._resolve_format(), which is what makes a document’s floats consistent across tables.

greater_tables.engine.formats.parse_sugar(value: FormatSpec | Mapping | str | int) FormatSpec[source]

Normalize a user format override to a FormatSpec.

Accepts a FormatSpec, a mapping of its fields (the YAML form — the only way a config file can reach negative='paren', scale, prefix, suffix, null or zero), a csv-grid spec string (',.1f', '.3g'), a GT f-string ('{x:.1%}', '{x:.3g}'), a strftime pattern ('%d %b %Y'), the words 'year'/'eng'/'si', or an int (decimal digits, grouped). g maps to kind='gen', where digits means significant figures.

Raises:
  • ValueError – If the sugar string is not recognized.

  • pydantic.ValidationError – If a mapping names a field FormatSpec does not have.

Table structure: row break depths, stub rowspans, head colspans.

The single implementation of the hierarchy logic (greater_tables derived it independently per backend). Everything here is pure functions over plain Python sequences — no pandas mutation, no rendering concerns.

greater_tables.engine.structure.break_depths(tuples: Sequence[tuple]) list[int | None][source]

Per row, the shallowest level whose value changed vs. the row above.

Row 0 is None (top edge is structural). A row identical to its predecessor is also None — no boundary at all.

greater_tables.engine.structure.column_break_depths(names: Sequence[tuple]) dict[int, int][source]

Data-column positions carrying a column-group boundary at their left edge.

Depth = shallowest head level that changes between column j-1 and j. Only genuine group boundaries are reported (depth ≤ n_levels − 2); leaf-level changes are just “next column”.

greater_tables.engine.structure.head_groups(names: Sequence[tuple], level: int, merge: bool) list[tuple[str, int]][source]

Header cells for one level: (text, colspan) runs over data columns.

Adjacent columns group only while their full prefix through level matches — the same leaf label under different parents never merges. The deepest level is never merged (distinct columns stay distinct); callers pass merge=False there and when sparsify_columns is off.

greater_tables.engine.structure.stub_layout(depths: Sequence[int | None], n_levels: int, sparsify: bool) list[dict[int, int]][source]

Which stub cells each row emits, and with what rowspan.

Returns one dict per row mapping stub level → rowspan for the levels that start a run at that row (HTML <td rowspan> semantics: covered rows omit the cell entirely). Without sparsify every row emits every level with span 1.

Canonical bytes and content hashing for table documents.

Determinism contract: same document content ⇒ same bytes ⇒ same hash, on any machine, any run. No time salt, no counters. hash and generator are excluded from the hashed form so stamping does not perturb the digest.

greater_tables.engine.hashing.canonical_json(doc: TableDoc, *, include_hash: bool = True) bytes[source]

Serialize a document to canonical UTF-8 JSON bytes.

Sorted keys, compact separators, NFC strings (applied by greater_tables.ir.canonical_dict()), no NaN/Infinity — a raw value that is not JSON-representable is an engine bug, surfaced here.

Parameters:
  • doc – The document to serialize.

  • include_hash – When False, omit hash and generator (the hashed form).

greater_tables.engine.hashing.doc_hash(doc: TableDoc) str[source]

Return the 12-hex-char sha256 content hash of a document.

greater_tables.engine.hashing.stamp(doc: TableDoc, generator: str | None = None) TableDoc[source]

Return a copy of doc with hash (and optionally generator) set.

Renderers

Python HTML renderer: TableDoc → fragment.

Mirrors js/src/gt-render.js structure and class stamping EXACTLY — the DOM-transcript conformance goldens (tests/golden/transcript) enforce the agreement, so any change here must be paired with the walker and the goldens regenerated. Class helpers below are direct ports of the walker’s.

Escaping: every text is HTML-escaped unless the cell carries html: true and allow_html is on (the default here — the producer set that flag deliberately via TableSpec.html_cols; the walker’s default is the opposite because it consumes foreign documents).

Stylesheet policy (dev/design.md): css='embed' puts the ~2 KB sheet in every fragment (idempotent — duplicates are no-ops, and a saved notebook whose first cell was cleared keeps its styles); 'once' embeds only the first time per process; 'none' for documents that include gt.css themselves. inline_css=True flattens the same RULES dict onto style attributes for HTMX-fragment/email use — no classes required by the host.

greater_tables.render.html.render_html(doc: TableDoc, *, css: str = 'embed', inline_css: bool = False, allow_html: bool = True, style: HouseStyle | None = None) str[source]

Render a table document to an HTML fragment.

Parameters:
  • doc – The document.

  • css'embed' (default), 'once', or 'none'.

  • inline_css – Flatten the stylesheet onto style attributes (HTMX/email); implies css='none'.

  • allow_html – Honor cell html: true. Off ⇒ such cells render as escaped source.

  • style – House style. max_rule_depth and max_vrule_depth are the only fields this renderer reads — they decide which breaks get a gt-break-{slot} / gt-vbreak-{slot} class. Rule weights are CSS custom properties (--gt-rule-{slot}, --gt-vrule-{slot}), set in a stylesheet, not here.

greater_tables.render.html.reset_css_once() None[source]

Reset the css='once' latch (new kernel/document boundary).

Shared renderer policy: which structural breaks get a rule, and how heavy.

The IR states where the hierarchy breaks (Row.break_depth, Column.break_depth); house style decides which of those breaks are drawn and with what weight. This module is the one implementation, called by html.py, tikz.py and text.py and ported line for line into js/src/gt-render.js.

It exists because three independent copies of the same comparison had already drifted: text.py drew a depth-3 rule the other three renderers suppressed, at five or more stub levels — the conformance oracle disagreeing with the renderers it exists to check.

Both functions return a weight slot, not a boolean, so the depth-to-weight clamp lives in one place: there are only N_RULE_SLOTS weights defined anywhere (HouseStyle.hrule_widths, --gt-rule-0/1/2, gt-break-0/1/2), and a break deeper than the last slot reuses the lightest weight rather than producing a class no stylesheet knows or an IndexError.

greater_tables.render.policy.rule_slot(break_depth: int | None, n_stub_levels: int, max_rule_depth: int | str | None = 'auto') int | None[source]

Weight slot for one row break, or None when no rule is drawn.

Parameters:
  • break_depthRow.break_depth — the shallowest stub level whose value changed from the row above, or None when nothing changed (or for the first row of a section). None never rules, under any setting.

  • n_stub_levelsTableDoc.n_stub_levels.

  • max_rule_depthHouseStyle.max_rule_depth. 'auto' (default) additionally caps at n_stub_levels - 2, which suppresses the innermost level — a change there is just “next row”, so ruling it draws a line between every row. An int is literal: it caps at that depth and nothing else, so 0 on a single-level index rules every row whose stub changed. None draws no internal rules at all.

Returns:

Slot in 0 .. N_RULE_SLOTS - 1 — the index into HouseStyle.hrule_widths, the gt-break-{slot} class, and the --gt-rule-{slot} custom property — or None for no rule.

Return type:

int or None

Raises:

ValueError – If max_rule_depth is a string other than 'auto'.

greater_tables.render.policy.vrule_slot(break_depth: int | None, max_vrule_depth: int | None = 1) int | None[source]

Weight slot for one column-group break, or None for no rule.

Column break depths already have the leaf level filtered out at build time (engine.structure.column_break_depths), so there is no n_stub_levels-style term here — only the user cap and the slot clamp.

The CSS contract: one RULES dict → committed assets/gt.css.

Single source of truth for the class vocabulary shared by the Python HTML renderer and the JS walker (conformance transcripts enforce the agreement). The [notebook-html] inline flattener consumes the same dict, so the sheet and inline styles cannot diverge.

Principles (dev/design.md): selectors double the wrapper class (.gt.gt) to beat JupyterLab/Quarto host rules without !important; per-table scalars ride as custom properties with defaults declared on .gt; colors default to currentColor/transparent so an embedded fragment has no color opinion; no widths anywhere — gt-nowrap/gt-wrap express intent and the browser measures.

Semantic classes:

  • alignment gt-l gt-r gt-c (right implies tabular numerals)

  • role gt-stub gt-data; gt-stub-end marks the last stub cell of a row (carries the stub/body vertical rule, spanning cells carry it for covered rows automatically)

  • wrapping gt-nowrap gt-wrap

  • row breaks gt-break-0/1/2 — border-top on the row’s own cells only, so rowspan-covered stub columns are skipped and the rule starts exactly at the changed column

  • column-group breaks gt-vbreak-0/1 (border-left)

  • flags gt-total gt-subtotal gt-emph gt-muted gt-neg; gt-math

greater_tables.render.css.css_text() str[source]

Serialize RULES to the stylesheet, deterministically.

greater_tables.render.css.write_asset() Path[source]

Regenerate the committed assets/gt.css (explicit recipe).

Notebook/Quarto integration helpers.

The mimebundle always carries text/plain: it is the one type every measured consumer renders (JupyterLab, Quarto html/latex/gfm, Sphinx html/latex), so a table degrades to a monospace rendering rather than to silence or to <GT object at 0x…>. What joins it depends on the target — text/html normally, text/latex (the TikZ rendering) under a Quarto PDF render, never both. Keying on the target is what structurally fixes greater_tables 5.x’s compute-TikZ-then-discard-it-per-repr waste: an HTML render now computes no TikZ at all.

text/html is excluded on the PDF path deliberately. Pandoc prefers html when it is offered, and its LaTeX writer then discards the raw html, so a {html, latex, plain} bundle renders nothing; omitting html means a TikZ failure degrades to text instead of to an empty page.

Detection reads QUARTO_FIG_FORMAT, the only channel Quarto offers. It is pdf on the PDF path and png for html and gfm alike, so html and gfm are indistinguishable from the environment. A gfm build that needs plain only has to say so with mimes — see quarto_target().

greater_tables.render.notebook.in_quarto() bool[source]

True inside a quarto render (any QUARTO_* env variable set).

greater_tables.render.notebook.mimebundle(doc: TableDoc, *, css: str = 'embed', tikz: bool | None = None, style: HouseStyle | None = None, mimes: Iterable[str] | None = None) dict[str, str][source]

MIME bundle for _repr_mimebundle_.

text/plain is always present unless mimes excludes it. With both tikz and mimes left at None the published keys are:

target

keys

QUARTO_FIG_FORMAT == 'pdf'

text/latex, text/plain

any other Quarto render, or none

text/html, text/plain

Parameters:
  • doc – The document.

  • css – Stylesheet policy for the HTML entry.

  • tikz – Force text/latex in or out, bypassing target detection; None (default) detects. Forcing it in keeps text/html — it means “also give me TikZ”, not “this is a PDF render”. Use mimes for exact control.

  • style – HouseStyle, forwarded to every renderer (HTML reads only the rule depth caps; TikZ reads everything).

  • mimes – Publish exactly these types, suppressing detection entirely and overriding tikz. Members are validated against KNOWN_MIMES. This is the escape hatch for a target GT cannot detect — notably Quarto --to gfm, where pandoc prefers html and the caller must ask for ('text/plain',).

greater_tables.render.notebook.quarto_target() str | None[source]

Which Quarto output is being built: 'pdf', 'other', or None.

QUARTO_FIG_FORMAT is the proxy: pdf on the PDF path, png for both html and gfm. 'other' therefore means “a Quarto render that is not PDF” and deliberately not “html” — the environment cannot tell html from gfm, and naming it html would hide a real ambiguity. None means no Quarto render was detected at all.

greater_tables.render.notebook.validate_mimes(mimes: Iterable[str]) tuple[str, ...][source]

Check an explicit MIME request, returning it as a tuple.

Raises ValueError on anything outside KNOWN_MIMES: a typo would otherwise publish a bundle no consumer can use, and fail silently.

Text renderer — the conformance oracle.

Reads only the TableDoc. Anything this renderer cannot draw is data missing from the IR, which is the point: it keeps the IR honest. Output is deterministic monospace text with box-drawing rules.

Semantics made visible: break depths draw partial horizontal rules starting at the changed stub column (which depths qualify is render.policy, shared with every other renderer); rowspan/colspan cells span; row flags appear in a right gutter; math cells render as $tex$; emphasis/muted cells as *text*/~text~.

Every non-ASCII character the renderer emits lives in Charset, so HouseStyle.text_ascii_rules can swap the whole set at once — for a LaTeX verbatim block, or a console whose encoding or mono font cannot carry box drawing. The ASCII set keeps the heavy/light distinction (= against -) rather than collapsing both to one character: which rule is a frame and which is a group break is information the oracle should not lose.

class greater_tables.render.text.Charset(heavy: str, light: str, vrule: str, flag_open: str, flag_close: str)[source]

Bases: object

The characters the text renderer draws rules and markers with.

vrule must be one character wide and the flag delimiters live in a right gutter, so swapping sets never moves a column: widths are solved from cell content, and STUB_GAP is 3 either way.

heavy: str
light: str
vrule: str
flag_open: str
flag_close: str
greater_tables.render.text.render_text(doc: TableDoc, style: HouseStyle | None = None) str[source]

Render a table document to monospace text.

Two HouseStyle fields reach here. max_rule_depth sets how deep breaks are ruled — text has no way to express a rule weight, so every drawn break looks the same. text_ascii_rules swaps ASCII_CHARS for the default UNICODE_CHARS.

TikZ renderer: TableDoc → tikzpicture in a table environment.

The single implementation (greater_tables carried three, two dead), rebuilt from the live make_tikz mechanics against the IR:

  • a matrix of nodes with a zero-height spacer row that fixes column widths, header rows in bold (stub header cells bold-italic), and a trailing phantom column used as a stable anchor rail;

  • rules drawn as \path segments anchored on node corners, shifted by half the row/column separation into the inter-cell gap — row break rules start exactly at the changed stub column, mirroring the HTML renderer;

  • colspan support (new vs. 5.x): spanning cells sit as empty matrix cells and their label is overlaid centered on the span via calc midpoints — real spanners, not blanked repeats;

  • character-based width estimation (the salvaged TextLength glyph table) lives here and only here — TeX genuinely needs geometry, HTML does not.

Cells flagged math re-wrap as $...$; everything else is TeX-escaped. The output expects the \I strut macro (TIKZ_PREAMBLE) — Etcher’s standalone template defines it; Quarto documents include it once in the preamble.

greater_tables.render.tikz.render_tikz(doc: TableDoc, style: HouseStyle | None = None) str[source]

Render a table document to a TikZ table (LaTeX source).

Character-based display-width estimation (greater_tables TextLength salvage).

Lives in the TikZ renderer’s orbit only — TeX genuinely needs geometry ahead of rendering; the HTML paths let the browser measure. Widths are em estimates from a serif glyph table; $...$ spans are token-counted TeX.

The table is Times metrics, which is what Etcher compiles against (newtxtext): measured there, the estimate is exact — median actual / estimate 1.000 over the fixture corpus, p99 1.007. A document set in another face is a different measurement: Latin Modern, the LaTeX default that a plain Quarto PDF uses, runs about 11% wider for text (digits only 1.6%), so columns come out slightly tight and a long heading may hyphenate. Either load a Times-metric serif alongside the tables, or accept that. Re-measure any font with dev/tools/measure_glyphs.py.

Glyph widths are held as integer milli-ems and accumulated as integers, converted to ems only at the end. Summing the float ems directly is not reproducible across interpreters: CPython 3.12 gave sum() Neumaier compensated summation, so ‘apples’ totalled 2.5549999999999997 on 3.11 and 2.555 on 3.13 — one ULP apart, but on opposite sides of the 2 dp rounding the TikZ renderer applies, which moved a committed column width by 0.01em. Integer accumulation is exact on every version, so the TeX goldens are interpreter-independent. Callers doing width arithmetic (summing columns, allocating a budget) should therefore work in milli-ems throughout — display_milli() and token_milli() — and divide only to print.

Two measures, because a column has two kinds of floor: display_milli() is the width the text wants on one line, token_milli() the width of its widest unbreakable token. TeX will not hyphenate inside a word, so a wrapping column narrower than its longest token gives an overfull hbox.

greater_tables.render.textlength.display_milli(s: str) int[source]

Display width in milli-ems — the exact, integer accumulation.

greater_tables.render.textlength.text_display_len(s: str) float[source]

Estimate display width in ems: plain text, $math$, HTML entities.

greater_tables.render.textlength.token_display_len(s: str) float[source]

Em width of the widest unbreakable token — a wrap column’s floor.

greater_tables.render.textlength.token_milli(s: str) int[source]

Widest whitespace-delimited token in milli-ems.

A $...$ span counts as one token: math is set as a box and does not break at spaces the way surrounding prose does.

Column-width allocation under a total-width budget.

A port of the csv-grid solver the JS walker also carries (solveWidths in js/src/gt-render.js, itself copied from csv-grid). The allocation math is identical; what differs is entirely the caller’s business:

  • csv-grid narrows a column by truncating cells, so a column’s floor is a minimum readable width and narrowing destroys content;

  • a typeset table narrows a column by wrapping it, so the floor is the widest unbreakable token — TeX will not hyphenate inside a word — and narrowing costs height, not content.

Same solver, different floors. This module takes floors as given and never asks which world it is in.

Units are plain integers and the module does not care what they mean: milli-ems for TikZ, characters for a text renderer. Integers in, integers out, so an allocation is identical on every interpreter — the same discipline, and the same reason, as render/textlength.py.

The budget is text width only. Column separations, padding and rules are the caller’s geometry: subtract them before calling, add them back after.

Two modes:

equal-risk (default)

Find the one quantile q that every column is measured at, so each takes the width its q-th percentile cell needs. Every column then carries the same probability of a cell overflowing its width — nothing is singled out and made to look mangled. Monotone in q, so a bisection finds it.

coverage

Maximize the number of cells that fit outright, by spending the budget where it buys the most cells per unit width (a water-fill along each column’s concave envelope). Equalizes marginal cost rather than probability: a column with a fat tail is left short so several tight columns can complete.

class greater_tables.render.widths.Allocation(widths: tuple[int, ...], status: Literal['natural', 'solved', 'over'])[source]

Bases: object

Solved column widths and how the budget worked out.

status is 'natural' when nothing had to be narrowed, 'solved' when narrowing brought the table inside the budget, and 'over' when the floors alone exceed it — no allocation can help and the caller must scale, rotate, or accept the overflow.

widths: tuple[int, ...]
status: Literal['natural', 'solved', 'over']
property total: int

Total text width; add the caller’s separations for the real one.

greater_tables.render.widths.solve_widths(samples: Sequence[Sequence[int]], floors: Sequence[int], budget: int, mode: Literal['equal-risk', 'coverage'] = 'equal-risk') Allocation[source]

Allocate budget across columns.

Parameters:
  • samples – Per column, the width of every cell in it, in any order. A column with no samples is held at its floor.

  • floors – Per column, the width below which it must not be narrowed. A column that must not be narrowed at all passes its natural width here, and then simply rides through the allocation as a constant.

  • budget – Total text width available.

  • mode'equal-risk' or 'coverage'; see the module docstring.

Returns:

Widths in column order, and the status of the fit.

Return type:

Allocation

Etcher: compile TikZ LaTeX to PDF (and optionally SVG).

greater_tables salvage, modernized: tectonic is the default engine (fast, self-caching — the 5.x .fmt precompilation dance is gone entirely), with classic pdflatex as a fallback. Success is judged by the PDF existing, not the exit code — MiKTeX’s pdflatex exits 1 on recoverable issues while still producing output. House rules honored: no hardcoded tool paths, no .bat side files, no os.startfile, pathlib throughout, logging not print. Outputs cache by content hash.

class greater_tables.render.etcher.Etcher(tex: str, *, file_name: str = '', base_path: str | Path | None = None, tex_engine: str = 'tectonic', pdf2svg: str = 'pdf2svg', font_size: int = 11)[source]

Bases: object

Compile a TeX blob containing a tikzpicture to PDF/SVG files.

Parameters:
  • tex – LaTeX source containing one tikzpicture (e.g. render_tikz output; any surrounding table environment is stripped — standalone cannot host floats).

  • file_name – Output stem; content hash when omitted (deterministic caching).

  • base_path – Working directory; outputs land in base_path/'tikz'.

  • tex_engine'tectonic' (default) or 'pdflatex' — name or full path.

  • pdf2svg – Converter executable for compile(svg=True).

  • font_size – Standalone document point size.

compile(*, svg: bool = False) Path[source]

Compile to PDF (and SVG when requested); return the output path.

display()[source]

Show the compiled SVG inline (Jupyter; lazy IPython import).

Fixtures

Fabricate synthetic pandas DataFrames for tests and fixtures.

Port of greater_tables fabrications.py with fixes recorded in the 1.0.0 CHANGELOG entry. Fully deterministic under a seed: datetime columns derive from a fixed now reference (default 2026-01-01), never the wall clock — this is what makes golden-file fixtures possible.

class greater_tables.fabricator.Fabricator(decorate: bool = False, pyarrow: bool = False, seed: int | None = None, now: Timestamp | str = Timestamp('2026-01-01 00:00:00'))[source]

Bases: object

Fabricate small synthetic DataFrames covering GT2’s fixture space.

Parameters:
  • decorate (bool) – Append type hints (” year”, “ ratio”, “ date”, “ time”) to generated metric names.

  • pyarrow (bool) – Convert fabricated frames to pyarrow-backed dtypes.

  • seed (int, optional) – RNG seed; one is drawn from entropy when omitted. Always pass a seed for fixtures.

  • now (pandas.Timestamp or str) – Reference timestamp for date/time columns. Fixed default keeps seeded output identical across runs.

Notes

Column type codes for data_spec:

d   date                    r   ratio (small floats)
f   float                   sN  string, N words
h   hash                    t   time (minute grid)
i   integer                 v   extreme-range float
l   log float (wide range)  wN  string, random length ~N words
p   path (filename)         x   TeX expression
                            y   year
metric_roots = ['absorption', 'acceleration', 'account', 'activation', 'adjustment', 'allocation', 'amplitude', 'approval', 'asset', 'atom', 'attrition', 'balance', 'band', 'binding', 'cancellation', 'capacitance', 'capital', 'cashflow', 'category', 'cell', 'charge', 'claim', 'commission', 'compound', 'concentration', 'conductivity', 'constraint', 'consumption', 'conversion', 'correlation', 'cost', 'count', 'coverage', 'credit', 'current', 'debt', 'decay', 'decibel', 'deductible', 'deficit', 'deflator', 'demand', 'density', 'development', 'diffusion', 'discount', 'distribution', 'dividend', 'dose', 'duration', 'earnings', 'efficiency', 'elasticity', 'employment', 'energy', 'entropy', 'enzyme', 'estimate', 'excess', 'exhaustion', 'expense', 'exposure', 'failure', 'field', 'flux', 'force', 'frequency', 'funding', 'gdp', 'gene', 'gradient', 'growth', 'half_life', 'incidence', 'income', 'index', 'indicator', 'inequality', 'inflation', 'inhibition', 'input', 'intensity', 'investment', 'kurtosis', 'lapse', 'layer', 'leverage', 'liability', 'limit', 'loss', 'luminosity', 'margin', 'mass', 'molecule', 'momentum', 'mortality', 'neutron', 'noise', 'operating', 'output', 'penalty', 'photon', 'policy', 'portfolio', 'potential', 'power', 'preference', 'premium', 'pressure', 'price', 'productivity', 'profit', 'protein', 'proton', 'provision', 'radiation', 'rate', 'ratio', 'reaction', 'recovery', 'reflection', 'refraction', 'renewal', 'reserve', 'residual', 'resistance', 'return', 'revenue', 'risk', 'sample', 'savings', 'scenario', 'score', 'sector', 'settlement', 'severity', 'shock', 'shortfall', 'signal', 'skewness', 'spread', 'strain', 'stress', 'subsidy', 'supply', 'tail', 'tariff', 'tax', 'temperature', 'tension', 'term', 'threshold', 'trade', 'trend', 'turbulence', 'unemployment', 'uptake', 'utility', 'utilization', 'valuation', 'variance', 'velocity', 'viscosity', 'volatility', 'voltage', 'volume', 'wage', 'wavelength', 'wealth', 'weight', 'yield']
cache: deque
static roll_columns(df: DataFrame, levels: int = -1) DataFrame[source]

Rotate the column MultiIndex levels by levels and re-sort.

static drop_singleton_levels(df: DataFrame) DataFrame[source]

Drop index/column levels holding a single value (keep at least one).

make(rows: int, data_spec, *, index_levels: int = 1, index_names=None, column_groups: int = 1, column_levels: int = 1, column_names=None, metric_name_spec='', missing: float = 0.0, simplify: bool = True, oversample: int = 1) DataFrame[source]

Fabricate a DataFrame to a specification.

Parameters:
  • rows – Number of body rows.

  • data_spec – Per-metric type codes (see class notes): a string like 's3sfid', a list of codes, or an int meaning that many floats.

  • index_levels – Row MultiIndex depth.

  • index_names – Level names; generated when omitted.

  • column_names – Level names; generated when omitted.

  • column_groups – Column MultiIndex shape: total columns = metrics × groups.

  • column_levels – Column MultiIndex shape: total columns = metrics × groups.

  • metric_name_spec'' for generated names, a list of names, or a type-code spec whose generated values become the names.

  • missing – Proportion of values per column replaced with NaN.

  • simplify – Drop index/column levels that hold a single value. Note this can reduce the requested level count on small frames.

  • oversample – Fabricate oversample × rows then keep the first rows — more distinct index values at small sizes.

Return type:

pandas.DataFrame

another() DataFrame[source]

Fabricate another DataFrame with the last make arguments.

random(rows: int = 0, columns: int = 0, index_levels: int = 0, omit: str = '') DataFrame[source]

Fabricate a DataFrame with randomly chosen shape and types.

Parameters:
  • rows – 0 means choose randomly.

  • columns – 0 means choose randomly.

  • index_levels – 0 means choose randomly.

  • omit – Type codes to exclude.

Return type:

pandas.DataFrame

metric_name(type_hint: str) str[source]

Return a one-word metric name, optionally decorated by type.

word() str[source]

Return the next word from the shuffled pool (cycles eventually).

tex() str[source]

Return the next TeX expression from the shuffled pool.

primes_for_product(n: int, v: int, shuffle: bool = False) list[int][source]

Return v distinct primes whose product is at least n.

Used to size index-level cardinalities so their product covers the requested row count.

greater_tables.fabricator.quick_fab(rows: int = 10, data_spec: str = 's3sfid', *, seed: int | None = None, pyarrow: bool = False, **kwargs) DataFrame[source]

One-stop fabrication of a DataFrame (see Fabricator.make()).