diff --git a/README-remember.md b/README-remember.md index b16f139..8ad5381 100644 --- a/README-remember.md +++ b/README-remember.md @@ -1,3 +1,5 @@ + + Here's how you subclass. diff --git a/README.md b/README.md index 914c684..8839198 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,121 @@ +sort out the variety of readmes... this is the main one + +# v 3.0 update + +* config files +* unified col width and info dataframe +* de-texing +* cli for config and writeout a csv etc. +* tests + +# TODO + +* Ratio cols with multi index columns +* % in tex output - never allow comments? +* center / left / right table output -> CSS +* ?Option to hide index +* Bring over the roll your own logger + +*** + +# from GPT + + +Absolutely—here’s a structured summary of everything we’ve covered, organized by topic. + +--- + +## 🧱 Project Structure & Philosophy + +* Your `GreaterTables` class formats a `pandas.DataFrame` to **HTML, text, or LaTeX**. +* The class is **immutable**: formatting is fixed at construction time, like a pure value object. +* You avoid branchy, incremental APIs (like `ggplot`) and prefer creating fresh objects. +* You wanted a way to handle growing config complexity—→ led to a **YAML config + Pydantic schema** design. + +--- + +## 📁 Project Layout + +``` +greater_tables_project/ +├── greater_tables/ +│ ├── __init__.py +│ ├── gtconfig.py ← config model + loader +│ ├── gtcore.py ← GreaterTables class +│ └── defaults/ +│ └── config_template.yaml +├── tests/ +├── pyproject.toml +``` + +* `GTConfigModel` = schema + default source of truth +* `GTConfig` = singleton loader and validator +* `config_template.yaml` = editable fallback + documentation base + +--- + +## 🔧 Config Management + +* All defaults and types are declared in `GTConfigModel` (Pydantic). +* Config is **loaded from YAML**, validated by `GTConfigModel`. +* You can **generate** a valid config file from the model using `.model_dump() → YAML`. +* Singleton pattern (`GTConfig.__new__`) caches the config at runtime. + +### Helpers + +* `GTConfig().get(overrides=...)` gives a safe, override-able config +* `write_template(path)` writes a default config YAML for user to edit + +--- + +## 🛠 Git Workflow (Solo Dev, Linear) + +* Use **tags** (`git tag v0.2.0`) to label stable versions +* Use **`git reset --hard `** to roll back and discard later commits +* Avoid branches entirely—keep a **single linear history** +* Tags let you bounce around safely, with names instead of hashes +* Releases on GitHub are tags + metadata, optional for publishing + +--- + +## ⚙️ CLI Tool + +* Built with `click`, with subcommands: + + * `gt render data.csv --format html` + * `gt write-template` +* Reads any Pandas-supported file (`.csv`, `.feather`, `.pkl`, etc.) +* Outputs to console or to file +* Uses current config by default, or override with `--config path.yaml` + +--- + +## 🧠 Design Principles You’re Following + +| Principle | Your Approach | +| ---------------------------- | ------------------------------------------- | +| Immutability | `GT(df, config)` is fixed once created | +| Separation of concerns | `GTConfigModel` holds defaults/types | +| Config as code/documentation | `config_template.yaml` generated from model | +| CLI-first mindset | `click` used to expose functionality | +| Linear Git workflow | Tags for rollback, no branches | + +--- + +Let me know if you want me to generate: + +* a Markdown doc for contributors +* a `.bat` script to roll back to a tag +* test scaffolding or release automation + +You're in great shape. Gum-level perfection achieved. + + + +*** + +# OLD + # Greater Tables Creating presentation quality tables from pandas dataframes is frustrating. It is hard to left-align text and right-align numbers using pandas `display` or `df.to_html`. The `great_tables` package does a really nice job with pandas and polars dataframes but does not support indexes or TeX output. diff --git a/doc-build-test.bat b/doc-build-test.bat deleted file mode 100644 index b839525..0000000 --- a/doc-build-test.bat +++ /dev/null @@ -1,41 +0,0 @@ -REM USE doc-test instead!! -echo use doc test... -rem @echo off -rem setlocal - -rem :: Define paths -rem set REPO_URL=https://github.com/mynl/greater_tables_project -rem set BUILD_DIR=C:\tmp\greater_tables_docs -rem set VENV_DIR=%BUILD_DIR%\venv - -rem :: Remove existing directory if it exists -rem if exist "%BUILD_DIR%" rd /s /q "%BUILD_DIR%" - -rem :: Clone the latest development repo -rem git clone --depth 1 %REPO_URL% "%BUILD_DIR%" -rem if %errorlevel% neq 0 exit /b %errorlevel% - -rem pushd "%BUILD_DIR%" - -rem :: Create virtual environment -rem python -m venv "%VENV_DIR%" -rem if %errorlevel% neq 0 exit /b %errorlevel% - -rem :: Activate virtual environment -rem call "%VENV_DIR%\Scripts\activate" - -rem :: Upgrade pip and install dependencies from pyproject.toml -rem python -m pip install --upgrade pip -rem pip install --upgrade build setuptools -rem pip install . -rem pip install ".[doc]" || pip install sphinx # Ensure Sphinx is installed - -rem :: Build the documentation -rem sphinx-build -b html docs docs/_build/html -rem if %errorlevel% neq 0 exit /b %errorlevel% - -rem :: Deactivate virtual environment -rem deactivate - -rem echo Documentation build complete: %BUILD_DIR%\docs\_build\html -rem endlocal diff --git a/greater_tables/TODO.md b/greater_tables/TODO.md deleted file mode 100644 index f0e5ca2..0000000 --- a/greater_tables/TODO.md +++ /dev/null @@ -1,6 +0,0 @@ -# TODO - -* Ratio cols with multi index columns -* % in tex output - never allow comments? -* center / left / right table output -> CSS -* ?Option to hide index \ No newline at end of file diff --git a/greater_tables/__init__.py b/greater_tables/__init__.py index f2cdc87..329fa0f 100644 --- a/greater_tables/__init__.py +++ b/greater_tables/__init__.py @@ -1,6 +1,7 @@ -__version__ = '2.0.0' +__version__ = '3.0.0' __project__ = 'greater_tables' __author__ = 'Stephen J Mildenhall' -from . greater_tables import * -from . utilities import * +from . gtcore import GT +# from . gtbreaks import Breakability + diff --git a/greater_tables/cli.py b/greater_tables/cli.py new file mode 100644 index 0000000..d691f95 --- /dev/null +++ b/greater_tables/cli.py @@ -0,0 +1,50 @@ +import click +import pandas as pd +from pathlib import Path +from .gtconfig import GTConfig, write_template +from .gtcore import GreaterTables + +@click.group() +def cli(): + """Greater Tables CLI tool""" + pass + +@cli.command() +@click.argument("input_file", type=click.Path(exists=True)) +@click.option("--output", "-o", type=click.Path(), help="Write rendered output to file") +@click.option("--format", "-f", type=click.Choice(["html", "text", "latex"]), default="html") +@click.option("--config", type=click.Path(), help="Path to a YAML config file") +def render(input_file, output, format, config): + """Render a table from a data file.""" + path = Path(input_file) + ext = path.suffix.lower() + + if ext == ".csv": + df = pd.read_csv(path) + elif ext == ".feather": + df = pd.read_feather(path) + elif ext == ".pkl": + df = pd.read_pickle(path) + else: + raise click.UsageError(f"Unsupported extension: {ext}") + + cfg = GTConfig(Path(config) if config else None).get() + gt = GreaterTables(df, config=cfg) + + rendered = ( + gt.render_html() if format == "html" + else gt.render_text() if format == "text" + else gt.render_latex() + ) + + if output: + Path(output).write_text(rendered, encoding="utf-8") + else: + print(rendered) + +@cli.command() +@click.argument("path", type=click.Path(), default="config.yaml") +def write_template(path): + """Write default config to the given path.""" + write_template(Path(path)) + click.echo(f"Config written to {path}") diff --git a/greater_tables/gtconfig.py b/greater_tables/gtconfig.py new file mode 100644 index 0000000..91256a6 --- /dev/null +++ b/greater_tables/gtconfig.py @@ -0,0 +1,171 @@ +""" +Configuration model and utilities for GreaterTables. + +Defines the `GTConfigModel` schema using Pydantic, which acts as the single +source of truth for default values, validation, and structure of all table-rendering options. + +Also includes functions for writing editable config templates and loading from YAML. +""" + + +from pathlib import Path +from typing import Optional, Union, Literal +import yaml + +from pydantic import BaseModel, Field, ValidationError, ConfigDict +import yaml + + +class GTConfigModel(BaseModel): + """ + Configuration model for GreaterTables. + + This class defines all configurable options for controlling the formatting + and rendering of tables in HTML, text, and LaTeX outputs. + + Each field has a default value and is validated using Pydantic. You can load + configuration from a YAML file or create it programmatically. Use this model + as the authoritative source of valid configuration fields. + + :Usage: + + >>> from greater_tables.gtconfig import GTConfigModel + >>> cfg = GTConfigModel(font_size="1.2em", caption_align="left") + + :see also: ``GTConfig`` for loading from YAML with overrides. + ``gt write-template`` CLI command to generate a default config file. + """ + # immutable + model_config = ConfigDict(frozen=True) + default_integer_str: str = Field( + "{x:,d}", description="Format f-string for integers. Example: '{x:,d}'" + ) + default_float_str: str = Field( + "{x:,.3f}", description="Format f-string for floats. Example: '{x:,.3f}'" + ) + default_date_str: str = Field( + "%Y-%m-%d", description="Format string for dates (no braces or 'x'). Example: '%Y-%m-%d'" + ) + default_ratio_str: str = Field( + "{x:.1%}", description="Format f-string for ratios. Example: '{x:.1%}'" + ) + default_formatter: Optional[str] = Field( + None, description="Optional fallback formatter f-string" + ) + + table_float_format: Optional[str] = Field( + None, description="Float format string for the entire table; overrides column-specific formats" + ) + table_hrule_width: int = Field( + 1, description="Width of top, bottom, and header horizontal rules" + ) + table_vrule_width: int = Field( + 1, description="Width of vertical rule separating index from body" + ) + hrule_widths: Optional[tuple[int, int, int]] = Field( + (0, 0, 0), description="Tuple of three ints for horizontal rule widths (for multiindex use)" + ) + vrule_widths: Optional[tuple[int, int, int]] = Field( + (0, 0, 0), description="Tuple of three ints for vertical rule widths (for multiindex columns)" + ) + + sparsify: bool = Field( + True, description="If True, sparsify index columns (recommended)" + ) + sparsify_columns: bool = Field( + True, description="If True, sparsify column headers using colspans" + ) + + spacing: str = Field( + "medium", description="Shorthand for cell padding. One of: 'tight', 'medium', 'wide'" + ) + padding_trbl: Optional[tuple[int, int, int, int]] = Field( + None, description="Manual padding in the order (top, right, bottom, left)" + ) + + tikz_scale: float = Field( + 1.0, description="Scaling factor applied to LaTeX TikZ tables" + ) + font_body: float = Field( + 0.9, description="Font size for body text (in em units)" + ) + font_head: float = Field( + 1.0, description="Font size for header text (in em units)" + ) + font_caption: float = Field( + 1.1, description="Font size for caption text (in em units)" + ) + font_bold_index: bool = Field( + False, description="If True, make index columns bold" + ) + + pef_precision: int = Field( + 3, description="Precision for engineering format (digits after decimal)" + ) + pef_lower: int = Field( + -3, description="Lower threshold: apply engineering format if abs(x) < 10**pef_lower" + ) + pef_upper: int = Field( + 6, description="Upper threshold: apply engineering format if abs(x) > 10**pef_upper" + ) + + cast_to_floats: bool = Field( + True, description="If True, cast non-integer, non-date columns to float where possible" + ) + header_row: bool = Field( + True, description="If True, use the first row as header; False disables header row" + ) + # tabs: Optional[Union[list[float], float, int]] = Field( + # None, description="Column widths in characters or ems; None triggers auto-calculation" + # ) + equal: bool = Field( + False, description="If True, force equal column widths (may be ignored if conflicting)" + ) + + caption_align: str = Field( + "center", description="Alignment of the caption text" + ) + large_ok: bool = Field( + False, description="If True, allow full rendering of large tables without truncation" + ) + + max_str_length: int = Field( + -1, description="Maximum length for stringified objects (e.g. nested DataFrames); -1 = unlimited" + ) + + max_table_width: int = Field( + 200, description="Maximum table width for markdown/text output mode" + ) + table_width_mode: Literal["explicit", "natural", "breakable", "minimum"] = Field( + "explicit", + description=( + "Mode for determining table width. " + "'explicit': fixed width using max_table_width; " + "'natural': each cell fits its full content; " + "'breakable': wrap breakable strings; " + "'minimum': also wraps dates or float-like cells" + ) + ) + table_width_header_adjust: float = Field( + 0.1, description="Proportion of width allocated to headers to balance content width" + ) + table_width_header_relax: float = Field( + 10.0, description="Extra characters allowed per column heading to help header wrapping" + ) + debug: bool = Field(False, description="Run in debug mode with more reporting, include internal ID in caption and use colored output lines") + + def write_template(self, path: Path): + """Generate a clean default config file at the given path.""" + path = Path(path) + yaml_str = yaml.dump(self.model_dump(), sort_keys=False) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(yaml_str, encoding="utf-8") + + +def write_template(path: Path): + """Generate a clean default config file at the given path.""" + path = Path(path) + cfg = GTConfigModel() + yaml_str = yaml.dump(cfg.model_dump(), sort_keys=False) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(yaml_str, encoding="utf-8") diff --git a/greater_tables/greater_tables.py b/greater_tables/gtcore.py similarity index 97% rename from greater_tables/greater_tables.py rename to greater_tables/gtcore.py index 0ca4f10..bf32814 100644 --- a/greater_tables/greater_tables.py +++ b/greater_tables/gtcore.py @@ -1,9 +1,14 @@ -# -*- coding: utf-8 -*- +""" +Core rendering logic for GreaterTables. + +Defines the `GreaterTables` class, which formats and renders pandas DataFrames +to HTML, plain text, or LaTeX output using a validated configuration model. + +This is the main entry point for rendering logic. See `gtconfig.py` for configuration schema. +""" -# table formatting again from collections import namedtuple from decimal import InvalidOperation -from enum import IntEnum from io import StringIO from itertools import groupby import logging @@ -22,6 +27,8 @@ from pandas.api.types import is_datetime64_any_dtype, is_integer_dtype, \ from rich import box from rich.table import Table +from . gtenums import Breakability +from . gtformats import GT_Format, TableFormat from . hasher import df_short_hash # turn this fuck-fest off @@ -52,51 +59,6 @@ logger.info(f'Logger Setup; {__name__} module recompiled.') # temp = None -class Breakability(IntEnum): - """To track if a column should or should not be broken (wrapped).""" - - NEVER = 0 - DATE = 3 - MAYBE = 5 - ACCEPTABLE = 10 - - -# specify text mode -Line = namedtuple('Line', ['begin', 'hline', 'sep', 'end', 'index_sep']) -DataRow = namedtuple('DataRow', ['begin', 'sep', 'end', 'index_sep']) -TableFormat = namedtuple('TableFormat', [ - 'lineabove', - 'linebelowheader', - 'linebetweenrows', - 'linebelow', - 'headerrow', - 'datarow', - 'padding', - 'with_header_hide' -]) - -# generic text format -GT_Format = TableFormat( - lineabove=Line('┍', '━', '┯', '┑', '┳'), - linebelowheader=Line('┝', '━', '┿', '┥', '╋'), - linebetweenrows=Line('├', '─', '┼', '┤', '╂'), - linebelow=Line('┕', '━', '┷', '┙', '┻'), - headerrow=DataRow('│', '│', '│', '┃'), - datarow=DataRow('│', '│', '│', '┃'), - padding=1, - with_header_hide=None -) - -# GT_Format = TableFormat( -# lineabove=Line('\u250d', '\u2501', '\u252f', '\u2511', '\u2533'), -# linebelowheader=Line('\u251d', '\u2501', '\u253f', '\u2525', '\u254b'), -# linebetweenrows=Line('\u251c', '\u2500', '\u253c', '\u2524', '\u2502'), -# linebelow=Line('\u2515', '\u2501', '\u2537', '\u2519', '\u253b'), -# headerrow=DataRow('\u2502', '\u2502', '\u2502', '\u2503'), -# datarow=DataRow('\u2502', '\u2502', '\u2502', '\u2503'), -# padding=1, -# with_header_hide=None -# ) class GT(object): @@ -274,41 +236,41 @@ class GT(object): date_cols=None, raw_cols=None, show_index=True, - default_integer_str='{x:,d}', - default_float_str='{x:,.3f}', - default_date_str='%Y-%m-%d', - default_ratio_str='{x:.1%}', - default_formatter=None, - table_float_format=None, - table_hrule_width=1, - table_vrule_width=1, - hrule_widths=None, - vrule_widths=None, - sparsify=True, # index sparsification - almost certainly want this! - sparsify_columns=True, # column sparsification with colspans - spacing='medium', # tight, medium, wide - padding_trbl=None, # tuple of four ints for padding - tikz_scale=1.0, - font_body=0.9, - font_head=1.0, - font_caption=1.1, - font_bold_index=False, - pef_precision=3, - pef_lower=-3, - pef_upper=6, - cast_to_floats=True, - header_row=True, - tabs=None, - equal=False, - caption_align='center', - large_ok=False, - max_str_length=-1, - str_table_fmt='mixed_grid', - table_width_mode='explicit', - table_width_header_adjust=0.1, - table_width_header_relax=10, - max_table_width=200, - debug=False): + # --> config + # default_integer_str='{x:,d}', + # default_float_str='{x:,.3f}', + # default_date_str='%Y-%m-%d', + # default_ratio_str='{x:.1%}', + # default_formatter=None, + # table_float_format=None, + # table_hrule_width=1, + # table_vrule_width=1, + # hrule_widths=None, + # vrule_widths=None, + # sparsify=True, # index sparsification - almost certainly want this! + # sparsify_columns=True, # column sparsification with colspans + # spacing='medium', # tight, medium, wide + # padding_trbl=None, # tuple of four ints for padding + # tikz_scale=1.0, + # font_body=0.9, + # font_head=1.0, + # font_caption=1.1, + # font_bold_index=False, + # pef_precision=3, + # pef_lower=-3, + # pef_upper=6, + # cast_to_floats=True, + # header_row=True, + # tabs=None, + # equal=False, + # caption_align='center', + # large_ok=False, + # max_str_length=-1, + # str_table_fmt='mixed_grid', # no longer used + # table_width_mode='explicit', + # table_width_header_adjust=0.1, + # table_width_header_relax=10, + ): # deal with alternative input modes if df is None: diff --git a/greater_tables/gtcore2.py b/greater_tables/gtcore2.py new file mode 100644 index 0000000..c2dba18 --- /dev/null +++ b/greater_tables/gtcore2.py @@ -0,0 +1,2768 @@ +""" +Core rendering logic for GreaterTables. + +Defines the `GreaterTables` class, which formats and renders pandas DataFrames +to HTML, plain text, or LaTeX output using a validated configuration model. + +This is the main entry point for rendering logic. See `gtconfig.py` for configuration schema. +""" + +from collections import namedtuple +from decimal import InvalidOperation +from io import StringIO +from itertools import groupby +import logging +from pathlib import Path +import re +import sys +from textwrap import wrap +import warnings + +from bs4 import BeautifulSoup +from cachetools import LRUCache +import numpy as np +import pandas as pd +from pandas.api.types import is_datetime64_any_dtype, is_integer_dtype, \ + is_float_dtype # , is_numeric_dtype +from rich import box +from rich.table import Table + +from . gtenums import Breakability, Alignment +from . gtformats import GT_Format, TableFormat +from . hasher import df_short_hash + +# turn this fuck-fest off +pd.set_option('future.no_silent_downcasting', True) +# pandas complaining about casting columns eg putting object in float column +warnings.simplefilter(action='ignore', category=FutureWarning) + + +# GPT recommended approach +logger = logging.getLogger(__name__) +# Disable log propagation to prevent duplicates +logger.propagate = False +if logger.hasHandlers(): + # Clear existing handlers + logger.handlers.clear() +# SET DEGBUGGER LEVEL +LEVEL = logging.WARNING # DEBUG or INFO, WARNING, ERROR, CRITICAL +logger.setLevel(LEVEL) +handler = logging.StreamHandler(sys.stderr) +handler.setLevel(LEVEL) +formatter = logging.Formatter( + '%(asctime)s | %(levelname)s | %(funcName)-15s | %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) +logger.info(f'Logger Setup; {__name__} module recompiled.') + + +class GT(object): + """ + Create a greater_tables formatting object. + + Provides html and latex output in quarto/Jupyter accessible manner. + Wraps AND COPIES the dataframe df. WILL NOT REFLECT CHANGES TO DF. + + Recommended usage is to subclass GT (or use functools.partial) and set + defaults suitable to your particular + application. In that way you can maintain a "house-style" + + Process + -------- + + **Input transformation** + + * ``pd.Series`` converted to ``DataFrame`` + * ``list`` converted to ``DataFrame``, optionally using row 0 as + ``config.header_row`` + * A string is assumed to be a pipe-separated markdown table which is + converted to a ``DataFrame`` setting aligners per the alignment row + * All other input types are an error + + The input ``df`` must have unique column names. It is then copied into + ``self.df`` which will be changed and ``self.raw_df`` for reference. + The copy is hashed for the table name. + + **Mangling** + + * If show_index, the index is reset and kept, so that all columns are on an + config.equal footing + * The index change levels are computed to determine LaTeX hrules + * ratio year, and raw columns converted to a list (can be input as a single + string name) + * Columns, except raw columns, are cast to floats + * Column types by index determined + * default formatter function set (wrapping input, if any) + * Aligner column input decoded into aligner values + (``grt-left,grt-right,grt-center``); index aligners separated + * Formatters decoded, strings mapped to lambda functions as f-string + formatters, integers as number of decimals + * Tab values expanded into an iterable + * Dataframe at this point (index reset, cast) saved to + ``df_pre_applying_formatters`` + * Determine formatters (``df_formatters`` property, a list of column index + formatting functions: + * Make the default float formatter if entered (callable, string, number; + wrapped in try/except) + * Determine each column's format type and add function + * Run ``apply_formatters`` to apply all format choices to ``df``. This + function handles index columns slightly differently, but results in the + formatters being applied to each column. + * Sparsify if requested and if multiindex + * Result is a dataframe with all object column types and values that + reflect the formatting choices. + + + Parameters + ----------- + + :param df: target DataFrame or list of lists or markdown table string + :param caption: table caption, optional (GT will look for gt_caption + attribute of df and use that) + :param label: TeX label (used in \\label{} command). For markdown + tables with #tbl:... in the caption it is extracted automatically. + :param aligners: None or dict (type or colname) -> left | center | + right + :param formatters: None or dict (type or colname) -> format function + for the column; formatters trump ratio_cols + :param unbreakable: None or list of columns to be considered unbreakable + :param ratio_cols: None, or "all" or list of column names treated as + ratios. Set defaults in derived class suitable to application. + :param year_cols: None, or "all" or list of column names treated as + years (no commas, no decimals). Set defaults in derived class suitable + to application. + :param date_cols: None, or "all" or list of column names treated as + dates. Set defaults in derived class suitable to application. + :param raw_cols: None, or "all" or list of column names that are NOT + cast to floats. Set defaults in derived class suitable to application. + :param show_index: if True, show the index columns, default True + :param config.default_integer_str: format f-string for integers, default + value '{x:,d}' + :param config.default_float_str: format f-string for floats, default + value '{x:,.3f}' + :param config.default_date_str: format f-string for dates, default '%Y-%m-%d'. + NOTE: no braces or x! + :param config.default_ratio_str: format f-string for ratios, default '{x:.1%}' + :param config.table_float_format: None or format string for floats in the + table format function, applied to entire table, default None + :param config.table_hrule_width: width of the table top, botton and header + hrule, default 1 + :param config.table_vrule_width: width of the table vrule, separating the + index from the body, default 1 + :param config.hrule_widths: None or tuple of three ints for hrule widths + (for use with multiindexes) + :param config.vrule_widths: None or tuple of three ints for vrule widths + (for use when columns have multiindexes) + :param config.sparsify: if True, config.sparsify the index columns, you almost always + want this to be true! + :param config.sparsify_columns: if True, config.sparsify the columns, default True, + generally a better look, headings centered in colspans + :param config.spacing: 'tight', 'medium', 'wide' to quickly set cell padding. + Medium is default (2, 10, 2, 10). + :param config.padding_trbl: None or tuple of four ints for padding, in order + top, right, bottom, left. + :param config.tikz_scale: scale factor applied to tikz LaTeX tables. + :param config.font_body: font size for body text, default 0.9. Units in em. + :param config.font_head: font size for header text, default 1.0. Units in em. + :param config.font_caption: font size for caption text, default 1.1. + Units in em. + :param config.font_bold_index: if True, make the index columns bold, + default False. + :param config.pef_precision: precision (digits after period) for pandas + engineering format, default 3. + :param config.pef_lower: apply engineering format to floats with absolute + value < 10**config.pef_lower; default -3. + :param config.pef_upper: apply engineering format to floats with absolute + value > 10**config.pef_upper; default 6. + :param config.cast_to_floats: if True, try to cast all non-integer, non-date + columns to floats + :param config.header_row: True: use first row as headers; False no headings. + Default True + :param config.tabs: None or list of column widths in characters or a common + int or float width. (It is converted into em; one character is about + 0.5em on average; digits are exactly 0.5em.) If None, will be calculated. + Default None. + :param config.equal: if True, set all column widths config.equal. Default False. Maybe + ignored, depending on computed ideal column widths. + :param config.caption_align: for the caption + :param config.large_ok: signal that you are intentionally applying to a large + dataframe. Sub-classes may restrict or apply .head() to df. + :param config.max_str_length: maximum displayed length of object types, that + are cast to strings. Eg if you have nested DataFrames! + :param str_table_fmt: table border format used for string output + (markdown), default mixed_grid DEPRECATED?? + :param config.table_width_mode: + 'explicit': set using config.max_table_width + 'natural': each cell on one line (can be very wide with long strings) + 'breakable': wrap breakable cells (text strings) at word boundaries + to fit longest word + 'minimum': wrap breakable and ok-to-break (dates) cells + :param config.table_width_header_adjust: additional proportion of table width + used to balance header columns. + :param config.table_width_header_relax: extra spaces allowed per column heading + to facilitate better column header wrapping. + :param config.max_table_width: max table width used for markdown string output, + default 200; width is never less than minimum width. Padding (3 chars + per row plus 1) consumed out of config.max_table_width in string output mode. + :param config.debug: if True, add id to caption and use colored lines in table, + default False. + """ + + # TeX control sequence display widths (heuristic) + TEX_SIMPLE_GLYPHS = { + 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta', + 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi', 'omicron', 'pi', 'rho', + 'sigma', 'tau', 'upsilon', 'phi', 'chi', 'psi', 'omega', 'infty', + 'sum', 'prod', 'int', 'cup', 'cap', 'vee', 'wedge', 'forall', 'exists', + 'neg', 'leq', 'geq', 'neq', 'approx', 'to', 'leftarrow', 'rightarrow' + } + TEX_WIDE = {'frac', 'sqrt', 'sum', 'int', 'prod'} + TEX_SPACING = {'quad', 'qquad', ',', ';', ' ', '!'} + + def __init__( + self, + df, + *, + caption='', + label='', + aligners: dict[str, callable] | None = None, + formatters: dict[str, callable] | None = None, + tabs: Optional[Union[list[float], float, int]] | None = None, + unbreakable=None, + ratio_cols=None, + year_cols=None, + date_cols=None, + raw_cols=None, + show_index=True, + # + config: GTConfigModel | None = None, + config_path: Path | None = None, + **overrides, + ): + if config and config_path: + raise ValueError("Pass either 'config' or 'config_path', not both.") + + if config: + base_config = config + elif config_path: + try: + raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) + base_config = GTConfigModel.model_validate(raw) + except (ValidationError, OSError) as e: + raise ValueError(f"Failed to load config from {config_path}") from e + else: + base_config = GTConfigModel() + + # access through config + self.config = base_config.model_copy(update=overrides) + + # deal with alternative input modes for df: None, DataFrame, Series, markdown text table + if df is None: + # don't want None to fail + df = pd.DataFrame([]) + if isinstance(df, pd.DataFrame): + # usual use case + pass + elif isinstance(df, pd.Series): + df = df.to_frame() + elif isinstance(df, list): + df = pd.DataFrame(df) + # override this selection come what may + show_index = False + if config.header_row: + # Set first row as column names + df.columns = df.iloc[0] + # Drop first row and reset index + df = df[1:].reset_index(drop=True) + elif isinstance(df, str): + df = df.strip() + if df == '': + df = pd.DataFrame([]) + else: + df, aligners, caption, label = GT.md_to_df(df) + show_index = False + else: + raise ValueError( + 'df must be a DataFrame, a list of lists, or a markdown table string') + + if len(df) > 50 and not config.large_ok: + raise ValueError( + 'Large dataframe (>50 rows) and config.large_ok not set to true...do you know what you are doing?') + + if not df.columns.is_unique: + raise ValueError('df column names are not unique') + + # extract value BEFORE copying, copying does not carry these attributes over + if caption != '': + self.caption = caption + else: + # used by querex etc. + self.caption = getattr(df, 'gt_caption', '') + self.label = label + self.df = df.copy(deep=True) # the object being formatted + self.raw_df = df.copy(deep=True) + # if not column_names: + # get rid of column names + # self.df.columns.names = [None] * self.df.columns.nlevels + self.df_id = df_short_hash(self.df) + # TODO: update / change + self.str_table_fmt = str_table_fmt + # TODO: implement + config.table_width_mode = config.table_width_mode.lower() + if config.table_width_mode not in ('explicit', 'natural', 'breakable', 'minimum'): + raise ValueError(f'Inadmissible options {config.table_width_mode} for config.table_width_mode.') + # self.table_width_mode = table_width_mode + # self.table_width_header_adjust = table_width_header_adjust + # self.table_width_header_relax = table_width_header_relax + # self.max_table_width = max_table_width + # self.debug = debug + if self.caption != '' and self.config.debug: + self.caption += f' (id: {self.df_id})' + # self.max_str_length = max_str_length + # before messing + self.show_index = show_index + self.nindex = self.df.index.nlevels if self.show_index else 0 + self.ncolumns = self.df.columns.nlevels + self.ncols = self.df.shape[1] + self.dt = self.df.dtypes + + # reset index to put all columns on an config.equal footing, but note number ofindex cols + with warnings.catch_warnings(): + if self.show_index: + warnings.simplefilter( + "ignore", category=pd.errors.PerformanceWarning) + self.df = self.df.reset_index( + drop=False, col_level=self.df.columns.nlevels - 1) + # want the new index to be ints - that is not default if old was multiindex + self.df.index = np.arange(self.df.shape[0], dtype=int) + self.index_change_level = GT.changed_column( + self.df.iloc[:, :self.nindex]) + if self.ncolumns > 1: + # will be empty rows above the index headers + self.index_change_level = pd.Series( + [i[-1] for i in self.index_change_level]) + + self.column_change_level = GT.changed_level(self.raw_df.columns) + + # determine ratio columns + if ratio_cols is not None and not self.df.columns.is_unique: + logger.warning( + 'Ratio cols specified with non-unique column names: ignoring request.') + self.ratio_cols = [] + else: + if ratio_cols is None: + self.ratio_cols = [] + elif ratio_cols == 'all': + self.ratio_cols = [i for i in self.df.columns] + elif ratio_cols is not None and not isinstance(ratio_cols, (tuple, list)): + self.ratio_cols = self.cols_from_regex( + ratio_cols) # [ratio_cols] + else: + self.ratio_cols = ratio_cols + + # determine year columns + if year_cols is not None and not self.df.columns.is_unique: + logger.warning( + 'Year cols specified with non-unique column names: ignoring request.') + self.year_cols = [] + else: + if year_cols is None: + self.year_cols = [] + elif year_cols is not None and not isinstance(year_cols, (tuple, list)): + self.year_cols = self.cols_from_regex(year_cols) # [year_cols] + else: + self.year_cols = year_cols + + # determine date columns + if date_cols is not None and not self.df.columns.is_unique: + logger.warning( + 'Year cols specified with non-unique column names: ignoring request.') + self.date_cols = [] + else: + if date_cols is None: + self.date_cols = [] + elif date_cols is not None and not isinstance(date_cols, (tuple, list)): + self.date_cols = self.cols_from_regex(date_cols) # [date_cols] + else: + self.date_cols = date_cols + + # determine columns NOT to cast to floats + if raw_cols is not None and not self.df.columns.is_unique: + logger.warning( + 'Year cols specified with non-unique column names: ignoring request.') + self.raw_cols = [] + else: + if raw_cols is None: + self.raw_cols = [] + elif raw_cols is not None and not isinstance(raw_cols, (tuple, list)): + self.raw_cols = self.cols_from_regex(raw_cols) # [raw_cols] + else: + self.raw_cols = raw_cols + + # figure the default formatter (used in conjunction with raw columns) + if config.default_formatter is None: + self.default_formatter = self.default_formatter + else: + assert callable( + config.default_formatter), 'config.default_formatter must be callable' + + def wrapped_config.default_formatter(x): + try: + return config.default_formatter(x) + except ValueError: + return str(x) + self.default_formatter = wrapped_config.default_formatter + + # cast as much as possible to floats + with warnings.catch_warnings(): + warnings.simplefilter( + "ignore", category=pd.errors.PerformanceWarning) + if config.cast_to_floats: + for i, c in enumerate(self.df.columns): + if c in self.raw_cols or c in self.date_cols: + continue + old_type = self.df.dtypes[c] + if not np.any((is_integer_dtype(self.df.iloc[:, i]), + is_datetime64_any_dtype(self.df.iloc[:, i]))): + try: + self.df.iloc[:, i] = self.df.iloc[:, + i].astype(float) + logger.debug( + f'coerce {i}={c} from {old_type} to float') + except (ValueError, TypeError): + logger.debug( + f'coercing {i}={c} from {old_type} to float FAILED') + + # massage unbreakable + if unbreakable is None: + unbreakable = [] + elif isinstance(unbreakable, str): + unbreakable = [unbreakable] + + # now can determine types and infer the break penalties (for column sizes) + self.float_col_indices = [] + self.integer_col_indices = [] + self.date_col_indices = [] + self.object_col_indices = [] # not actually used, but for neatness + self.break_penalties = [] + # manage non-unique col names here + logger.debug('FIGURING TYPES') + for i, cn in enumerate(self.df.columns): # range(self.df.shape[1]): + ser = self.df.iloc[:, i] + if cn in self.date_cols: + logger.debug(f'col {i}/{cn} specified as date col') + self.date_col_indices.append(i) + self.break_penalties.append( + Breakability.NEVER if cn in unbreakable else Breakability.DATE) + elif is_datetime64_any_dtype(ser): + logger.debug(f'col {i} = {self.df.columns[i]} is DATE') + self.date_col_indices.append(i) + self.break_penalties.append( + Breakability.NEVER if cn in unbreakable else Breakability.DATE) + elif is_integer_dtype(ser): + logger.debug(f'col {i} = {self.df.columns[i]} is INTEGER') + self.integer_col_indices.append(i) + self.break_penalties.append( + Breakability.NEVER if cn in unbreakable else Breakability.NEVER) + elif is_float_dtype(ser): + logger.debug(f'col {i} = {self.df.columns[i]} is FLOAT') + self.float_col_indices.append(i) + self.break_penalties.append( + Breakability.NEVER if cn in unbreakable else Breakability.NEVER) + else: + logger.debug(f'col {i} = {self.df.columns[i]} is OBJECT') + self.object_col_indices.append(i) + c = ser.name + if c in self.year_cols or c in self.ratio_cols: + self.break_penalties.append( + Breakability.NEVER if cn in unbreakable else Breakability.NEVER) + else: + self.break_penalties.append( + Breakability.NEVER if cn in unbreakable else Breakability.ACCEPTABLE) + + # figure out column and index alignment + if aligners is not None and np.any(self.df.columns.duplicated()): + logger.warning( + 'aligners specified with non-unique column names: ignoring request.') + aligners = None + if aligners is None: + # not using + aligners = [] + elif isinstance(aligners, str): + # lrc for each column + aligners = {c: a for c, a in zip(self.df.columns, aligners)} + self.df_aligners = [] + + lrc = {'l': 'grt-left', 'r': 'grt-right', 'c': 'grt-center'} + # TODO: index aligners + for i, c in enumerate(self.df.columns): + # test aligners BEFORE index! + if c in aligners: + self.df_aligners.append(lrc.get(aligners[c], 'grt-center')) + elif i < self.nindex: + # index -> left + self.df_aligners.append('grt-left') + elif c in self.year_cols: + self.df_aligners.append('grt-center') + elif c in self.raw_cols: + # these are strings + self.df_aligners.append('grt-left') + elif i in self.date_col_indices: + # center dates, why not! + self.df_aligners.append('grt-center') + elif c in self.ratio_cols or i in self.float_col_indices or i in self.integer_col_indices: + # number -> right + self.df_aligners.append('grt-right') + else: + # all else, left + self.df_aligners.append('grt-left') + + self.df_idx_aligners = self.df_aligners[:self.nindex] + + if formatters is None: + self.default_formatters = {} + else: + self.default_formatters = {} + for k, v in formatters.items(): + if callable(v): + self.default_formatters[k] = v + elif type(v) == str: + self.default_formatters[k] = lambda x: v.format(x=x) + elif type(v) == int: + fmt = f'{{x:.{v}f}}' + self.default_formatters[k] = lambda x: fmt.format(x=x) + else: + raise ValueError( + 'formatters must be dict of callables or ints or format strings {x:...}') + + # store defaults + # self.default_integer_str = default_integer_str + # VERY rarely used; for floats in cols that are not floats + # self.default_float_str = default_float_str + # self.default_date_str = default_date_str.replace( + # '{x:', '').replace('}', '') + # self.default_ratio_str = default_ratio_str + # self.pef_precision = pef_precision + # self.pef_lower = pef_lower + # self.pef_upper = pef_upper + self._pef = None + # self.table_float_format = table_float_format + # self.default_float_formatter = None + # self.hrule_widths = hrule_widths or (0, 0, 0) + # if not isinstance(self.config.hrule_widths, (list, tuple)): + # self.config.hrule_widths = (self.config.hrule_widths,) + # self.vrule_widths = vrule_widths or (0, 0, 0) + # if not isinstance(self.config.hrule_widths, (list, tuple)): + # self.config.hrule_widths = (self.config.hrule_widths, ) + # self.table_hrule_width = table_hrule_width + # self.table_vrule_width = table_vrule_width + # self.font_body = font_body + # self.font_head = font_head + # self.font_caption = font_caption + # self.tikz_scale = tikz_scale + # self.font_bold_index = font_bold_index + # self.caption_align = caption_align + # self.sparsify_columns = sparsify_columns + if tabs is None: + self.tabs = None + elif isinstance(tabs, (int, float)): + self.tabs = (tabs,) + elif isinstance(tabs, (np.ndarray, list, tuple)): + self.tabs = tabs # Already iterable, self.config.tabs = as is + else: + self.tabs = [tabs] # Fallback for anything else + # self.equal = equal + + if config.padding_trbl is None: + if config.spacing == 'tight': + config.padding_trbl = (0, 5, 0, 5) + elif config.spacing == 'medium': + config.padding_trbl = (2, 10, 2, 10) + elif config.spacing == 'wide': + config.padding_trbl = (4, 15, 4, 15) + else: + raise ValueError( + 'config.spacing must be tight, medium, or wide or tuple of four ints.') + try: + self.padt, self.padr, self.padb, self.padl = config.padding_trbl + except ValueError: + # pydantics will see to this... + logger.error( + f'config.padding_trbl {config.padding_trbl=}, must be four ints, defaulting to medium padding') + self.padt, self.padr, self.padb, self.padl = 2, 10, 2, 10 + + # because of the problem of non-unique indexes use a list and + # not a dict to pass the formatters to to_html + self._df_formatters = None + self.df_style = '' + self.df_html = '' + self._clean_html = '' + self._clean_tex = '' + self._rich_table = None + # finally config.sparsify and then apply formaters + # this radically alters the df, so keep a copy for now... + self.df_pre_applying_formatters = self.df.copy() + self.df = self.apply_formatters(self.df) + # cache for various things... + self._cache = LRUCache(20) + # config.sparsify + if config.sparsify and self.nindex > 1: + self.df = GT.sparsify(self.df, self.df.columns[:self.nindex]) + # for c in self.df.columns[:self.nindex]: + # # config.sparsify returns some other stuff... + # self.df[c], _ = GT.config.sparsify(self.df[c]) + + def __repr__(self): + """Basic representation.""" + return f"GT(df_id={self.df_id})" + + def __str__(self): + """String representation, for print().""" + return self.to_string_custom() + + def _repr_html_(self): + """ + Apply format to self.df. + + ratio cols like in constructor + """ + return self.html + + def _repr_latex_(self): + """Generate a LaTeX tabular representation.""" + # return '' + # latex = self.df.to_latex(caption=self.caption, formatters=self._df_formatters) + if self._clean_tex == '': + self._clean_tex = self.make_tikz() + logger.info('CREATED LATEX') + return self._clean_tex + + def cols_from_regex(self, regex): + """Return columns of self.df matching regex""" + return [col for col in self.df.columns if isinstance(col, str) and re.search(regex, col)] + + def cache_get(self, key): + """Retrieve item from cache.""" + return self._cache.get(key, None) + + def cache_set(self, key, value): + """Add item to cache.""" + self._cache[key] = value + + # define the default and easy formatters =================================================== + def default_ratio_formatter(self, x): + """Ratio formatter.""" + try: + return self.config.default_ratio_str.format(x=x) + except ValueError: + return str(x) + + def default_date_formatter(self, x): + """Date formatter that works for strings too.""" + if pd.isna(x): + return "" + try: + dt = pd.to_datetime(x, errors='coerce') + if pd.isna(dt): + return str(x) + return dt.strftime(self.config.default_date_str) + except Exception: + logger.error("date error with %s", x) + return str(x) + + def default_integer_formatter(self, x): + """Integer formatter.""" + try: + return self.config.default_integer_str.format(x=x) + except ValueError: + return str(x) + + def default_year_formatter(self, x): + """Year formatter.""" + try: + return f'{int(x):d}' + except ValueError: + return str(x) + + def default_raw_formatter(self, x): + """Formatter for columns flagged as raw.""" + return str(x) + + # def config.default_formatter(self, x): + # """Universal formatter for other types.""" + # try: + # # werid wrinkle here: float('infinity') -> np.inf!! + # f = float(x) + # if self.default_float_formatter: + # return self.default_float_formatter(f) + # try: + # i = int(x) + # except ValueError: + # try: + # i = int(f) + # except OverflowError: + # # this came up! Passed the work "Infinity" + # return str(x) + # if i == f: + # return self.config.default_integer_str.format(x=i) + # else: + # # TODO BEEF UP? + # return self.config.default_float_str.format(x=f) + # except (TypeError, ValueError): + # if self.config.max_str_length < 0: + # return str(x) + # else: + # return str(x)[:self.config.max_str_length] + + def default_formatter(self, x): + """Default universal formatter for other types (GTP re-write of above cluster).""" + try: + f = float(x) + except (TypeError, ValueError): + s = str(x) + return s if self.config.max_str_length < 0 else s[:self.config.max_str_length] + + if self.default_float_formatter: + return self.default_float_formatter(f) + + if np.isinf(f) or np.isnan(f): # clearer handling of weird float cases + return str(x) + + if f.is_integer(): + return self.config.default_integer_str.format(x=int(f)) + else: + return self.config.default_float_str.format(x=f) + + def pef(self, x): + """Pandas engineering format.""" + if self._pef is None: + self._pef = pd.io.formats.format.EngFormatter(accuracy=self.config.pef_precision, use_eng_prefix=True) # noqa + return self._pef(x) + + def make_float_formatter(self, ser): + """ + Make a float formatter suitable for the Series ser. + + Obeys these rules: + * All elements in the column are formatted consistently + * ... + + TODO flesh out... at some point shd use pef?! + + """ + amean = ser.abs().mean() + # mean = ser.mean() + amn = ser.abs().min() + amx = ser.abs().max() + # smallest = ser.abs().min() + # sd = ser.sd() + # p10, p50, p90 = np.quantile(ser, [0.1, .5, 0.9], method='inverted_cdf') + # pl = 10. ** self.config.pef_lower + # pu = 10. ** self.config.pef_upper + pl, pu = 10. ** self.config.pef_lower, 10. ** self.config.pef_upper + if amean < 1: + precision = 5 + elif amean < 10: + precision = 3 + elif amean < 20000: + precision = 2 + else: + precision = 0 + fmt = f'{{x:,.{precision}f}}' + logger.debug(f'{ser.name=}, {amean=}, {fmt=}') + if amean < pl or amean > pu or amx / max(1, amn) > pu: + # go with eng + def ff(x): + try: + return self.pef(x) + except (ValueError, TypeError, InvalidOperation): + return str(x) + else: + def ff(x): + try: + return fmt.format(x=x) + # well and good but results in ugly differences + # by entries in a column + # if x == int(x) and np.abs(x) < pu: + # return f'{x:,.0f}.' + # else: + # return fmt.format(x=x) + except (ValueError, TypeError): + return str(x) + return ff + + @ property + def df_formatters(self): + """ + Make and return the list of formatters. + + Created one per column. Int, date, objects use defaults, but + for float cols the formatter is created custom to the details of + each column. + """ + if self._df_formatters is None: + # because of non-unique indexes, index by position not name + if self.config.table_float_format is not None: + if callable(self.config.table_float_format): + # wrap in error protections + def ff(x): + try: + return self.config.table_float_format(x=x) + except ValueError: + return str(x) + except Exception as e: + logger.error(f'Custom float function raised {e=}') + self.default_float_formatter = ff + else: + if type(self.config.table_float_format) != str: + raise ValueError( + 'config.table_float_format must be a string or a function') + fmt = self.config.table_float_format + + def ff(x): + try: + return fmt.format(x=x) + except ValueError: + return str(x) + except Exception as e: + logger.error( + f'Custom float format string raised {e=}') + self.default_float_formatter = ff + else: + self.default_float_formatter = False + + self._df_formatters = [] + for i, c in enumerate(self.df.columns): + # set a default, note here can have + # non-unique index so work with position i + if c in self.default_formatters: + self._df_formatters.append(self.default_formatters[c]) + elif c in self.ratio_cols: + # print(f'{i} ratio') + self._df_formatters.append(self.default_ratio_formatter) + elif c in self.year_cols: + self._df_formatters.append(self.default_year_formatter) + elif c in self.raw_cols: + self._df_formatters.append(self.default_raw_formatter) + elif i in self.date_col_indices: + self._df_formatters.append(self.default_date_formatter) + elif i in self.integer_col_indices: + # print(f'{i} int') + self._df_formatters.append(self.default_integer_formatter) + elif i in self.float_col_indices: + # trickier approach... + self._df_formatters.append( + self.default_float_formatter or self.make_float_formatter(self.df.iloc[:, i])) + else: + # print(f'{i} default') + self._df_formatters.append(self.config.default_formatter) + # self._df_formatters is now a list of length config.equal to cols in df + if len(self._df_formatters) != self.df.shape[1]: + raise ValueError( + f'Something wrong: {len(self._df_formatters)=} != {self.df.shape=}') + return self._df_formatters + + def make_column_width_df(self): + """ + Return dataframe of width information. + + Returned dataframe has columns for + + * natural width, all on one line = max len by col + * min width = max length given breaks + * break type of column + * alignment of column + * index natural width + * index min width + """ + df = self.df + n_row, n_col = df.shape + + # The width if content didn't wrap (single line) + # Series=dict colname->max width of cells in column + natural_width = df.map(lambda x: len(x.strip())).max(axis=0).to_dict() + + # re.split(r'(?<=[\s.,:;!?()\[\]{}\-\\/|])\s*', text) + # (?<=...) is a lookbehind to preserve the break character with the left-hand fragment. + # [\s.,:;!?()\[\]{}\-\\/|] matches common punctuation and separators: + # \s = whitespace + # . , : ; ! ? = terminal punctuation + # () [] {} = brackets + # \- = dash + # \\/| = slash, backslash, pipe + pat = r'(?<=[.,;:!?)\]}\u2014\u2013])\s+|--+\s+|\s+' + iso_date_split = r'(?<=\b\d{4})-(?=\d{2}-\d{2})' + pat = f'{pat}|{iso_date_split}' + + # Calculate ideal (no wrap) and minimum possible widths for all columns + # The absolute minimum width each column can take (e.g., longest word for text) + min_acceptable_width = {} + for col_name in df.columns: + min_acceptable_width[col_name] = ( + df[col_name].str + .split(pat=pat, regex=True, expand=True) + .fillna('') + .map(len) + .max(axis=1) + .max() + ) + # ans will be the col_width_df + ans = pd.DataFrame({ + 'alignment': [i[4:] for i in self.df_aligners], + 'break_penalties': self.break_penalties, + 'breakability': [x.name for x in self.break_penalties], + 'natural_width': natural_width.values(), + 'min_acceptable_width': min_acceptable_width.values(), + }, index=df.columns) + ans['break_acceptable'] = np.where( + ans.break_penalties == Breakability.ACCEPTABLE, ans.min_acceptable_width, ans.natural_width) + # DUH - this is min_acceptable_width + # ans['break_dates'] = np.where(ans.break_penalties==Breakability.DATE, ans.min_acceptable_width, ans.break_acceptable) + + natural, acceptable, min_acceptable = ans.iloc[:, 3:].sum() + PADDING = 2 # per column + if self.config.table_width_mode == 'explicit': + # target width INCLUDES padding and column marks | + target_width = self.config.max_table_width - (PADDING + 1) * n_col - 1 + logger.info(f'Col padding effect {self.config.max_table_width=} ==> {target_width=}') + elif self.config.table_width_mode == 'natural': + target_width = natural + (PADDING + 1) * n_col + 1 + elif self.config.table_width_mode == 'breakable': + target_width = acceptable + (PADDING + 1) * n_col + 1 + elif self.config.table_width_mode == 'minimum': + target_width = min_acceptable + (PADDING + 1) * n_col + 1 + + # extra space for the headers to relax, if useful + if self.config.table_width_header_adjust > 0: + max_extra = int(self.config.table_width_header_adjust * target_width) + else: + max_extra = 0 + + if target_width > natural: + # everything gets its natural width + ans['recommended'] = ans['natural_width'] + space = target_width - natural + logger.info('Space for NATURAL! Spare space = %s', space) + elif target_width > acceptable: + # strings wrap + ans['recommended'] = ans['break_acceptable'] + # use up extra on the ACCEPTABLE cols + space = target_width - acceptable + logger.info( + 'Using breaks acceptable (dates not wrapped), spare space = %s', space) + elif target_width > min_acceptable: + # strings and dates wrap + ans['recommended'] = ans['min_acceptable_width'] + # use up extra on dates first, then strings + space = target_width - min_acceptable + logger.info( + 'Breaking all breakable (incl dates), spare space = %s', space) + else: + # OK severely too small + ans['recommended'] = ans['min_acceptable_width'] + logger.info( + 'Desired width too small for pleasant formatting, table will be too wide.') + space = target_width - min_acceptable + + input_df = None + if space >= 0: + # Allocate the excess ------------------------------ + # Fancy col headings currently only for 1-d index + # TODO NOTE: use config.sparsify logic you have for index applied to df.T + # to sort the columns!! + if df.columns.nlevels == 1: + # Step 1: baseline comes in from code above + ans['raw_rec'] = ans['recommended'] + + # Step 2: get rid of intra-line breaks + if max_extra > 0: + adj, input_df = self.header_adjustment( + df, ans['recommended'], space, max_extra) + # create new col and populate per GPT + ans['header_tweak'] = pd.Series(adj) + else: + ans['header_tweak'] = 0 + ans['recommended'] = ans['recommended'] + ans['header_tweak'] + ans['natural_w_header'] = ans['recommended'] + else: + # avoid a failure blow + ans['raw_rec'] = np.nan + ans['header_tweak'] = np.nan + ans['natural_w_header'] = np.nan + # Step 3: distribute remaining slack proportionally + remaining = target_width - ans['recommended'].sum() + if remaining > 0: + slack = ans['natural_width'] - ans['recommended'] + total_slack = slack.clip(lower=0).sum() + if total_slack > 0: + fractions = slack.clip(lower=0) / total_slack + ans['recommended'] += np.floor(fractions * + remaining).astype(int) + ans['recommended'] = np.maximum( + ans['recommended'], ans['natural_w_header']) + + # Ensure final constraint + ans['recommended'] = ans['recommended'].astype(int) + logger.info("Raw rec: %s\tTweaks: %s\tActual: %s\tTarget: %s\tOver/(U): %s", + ans['raw_rec'].sum(), + ans['header_tweak'].sum(), + ans['recommended'].sum(), + target_width, + ans['recommended'].sum() - target_width + ) + ans = ans[[ + 'alignment', + 'break_penalties', + 'breakability', + 'natural_width', + 'break_acceptable', + 'min_acceptable_width', + 'raw_rec', + 'header_tweak', + 'natural_w_header', + 'recommended', + ]] + # in all cases... + # need recommended to be > 0 + ans['recommended'] = np.maximum(ans['recommended'], 1) + self.cache_set('column_width_df', ans) + # info about the header adjustment + self.cache_set('input_df', input_df) + + return ans + + @staticmethod + def header_adjustment(df, min_widths, space, max_extra): + """ + Fine-adjust heading for optimal config.spacing. + + Return a dict with per-column recommended width adjustments to avoid + intra-word breaks and reduce overall header height. + + Parameters: + df: DataFrame with 1-level string column names + min_widths: dict of column name -> minimal acceptable width + space: amount of space available to be allocated + max_extra: max extra characters to consider allocating per column + + Returns: + dict: column -> additional width to allocate + """ + colnames = list(df.columns) + adjustments = {col: 0 for col in colnames} + num_lines = 0 + + def has_intra_word_break(text: str, width: int) -> bool: + """ + Determine if textwrap.wrap breaks any words in the given text. + + Gemini - GPT code did not work, even after seveal iterations. + This is a nice approach to the problem. + + Args: + text: The input string. + width: The maximum width for wrapping. + + Returns: + True if any word is broken across lines, False otherwise. + """ + nonlocal num_lines + wrapped_lines = wrap(text, width=width) + num_lines = len(wrapped_lines) + original_words = text.split() + + reconstructed_text_from_wrapped = " ".join(wrapped_lines) + reconstructed_words = reconstructed_text_from_wrapped.split() + + # If the number of words differs, it means some words were split. + # This catches cases where a word might be split and then later re-joined + # due to subsequent wrapping logic, leading to a different number of words. + if len(original_words) != len(reconstructed_words): + return True + + # Compare word by word. If any word from the original doesn't exactly match + # a word from the reconstructed list, it implies a split. + for i in range(len(original_words)): + if original_words[i] != reconstructed_words[i]: + return True + + return False + + # First pass: avoid ugly intraword breaks + # make dict of col -> longest word length + min_acceptable = {c: v for c, v in + zip(colnames, map(lambda x: max(len(i) for i in re.split(r'[ \-/]', x)), colnames))} + options = [] + for col in colnames: + if not isinstance(col, str): + continue + base_width = min_widths[col] + if not has_intra_word_break(col, base_width): + options.append([col, 0, num_lines]) + # nothing to be gained, move to next col + continue + extra0 = max(0, min_acceptable[col] - base_width) + if extra0 > max_extra: + # ok, can't flatten word because it is too long + extra0 = 0 + elif extra0 == max_extra: + # go with that + adjustments[col] = max_extra + continue + # see if col can be flattened within max_extra chars, starting + # at extra0, which is enough to avoid intraword breaks + for extra in range(extra0, max_extra + 1): + if not has_intra_word_break(col, base_width + extra): + options.append([col, extra, num_lines]) + if adjustments[col] == 0: + # take first, but compute rest... + adjustments[col] = extra + # temporary diagnostic DEBUG information - comment in prod + # from IPython.display import display + # config.debug = pd.Series([col, min_acceptable[col], base_width, has_intra_word_break(col, base_width), extra0, max_extra, + # wrap(col, base_width), extra], + # index=['col name', 'min acceptable', 'base_width (from data)', 'intra word break', 'extra0', 'max_extra', 'split', 'selected extra']).to_frame('Value') + # display(config.debug) + # make df[col name, amount of extra space for col, resulting number of lines] + # this is needed as input for the optimal heading function (next) + input_df = pd.DataFrame(options, columns=['col', 'extra', 'num_lines']) + # min amount to avoid intra work breaks + avoid_intra = input_df.groupby('col').min().extra.sum() + if avoid_intra >= space: + # that's all we can do + print("NO FURTHER IMPROVEMENTS") + else: + # can try for a better solution + sol = GT.optimal_heading(input_df, space) + adjustments.update(sol[1]) + logger.info('best solution: %s', sol) + # global temp + # temp = input_df + return adjustments, input_df + + @staticmethod + def optimal_heading(input_df: pd.DataFrame, total_es_budget: int) -> tuple[int, dict[str, int]]: + """ + Optimize extra config.spacing for best heading. + + Finds the best way to allocate extra space to minimize max_lines in heading. + + Gemini solution. + + Args: + input_df: DataFrame with 'col', 'extra', 'num_lines'. + total_es_budget: The total extra space to allocate. + + Returns: + A tuple: (min_max_lines, optimal_extra_allocation_per_column). + + .. _table_layout_optimization: + + Table Layout Optimization + ========================= + + This document describes the algorithm implemented in the :py:func:`find_best_layout` function, which aims to optimize the allocation of a fixed amount of extra space (`ES`) among table columns to minimize the overall table height (i.e., the maximum number of lines used by any single column). + + Problem Statement + ----------------- + + Given a set of table columns, each with a known relationship between allocated "extra space" and the resulting "number of lines" it occupies when wrapped, and a total budget of extra space, the goal is to find an allocation of this extra space to each column such that the maximum number of lines among all columns is minimized. + + For example, a column named "location category (float)" might take 3 lines with 0 extra space, but perhaps only 2 lines with 2 extra space, and 1 line with 5 extra space. The relationship is provided in a Pandas DataFrame with columns `col`, `extra`, and `num_lines`. + + Algorithm: Binary Search on the Answer + ------------------------------------- + + The problem exhibits a monotonic property: if a table layout can be achieved with a maximum height of `X` lines, it can also be achieved with any maximum height `Y > X` lines (by simply using the same or more `extra` space). This property makes binary search on the *minimum possible maximum lines* an efficient solution. + + The algorithm proceeds as follows: + + 1. **Preprocessing the Input Data:** + The input `pandas.DataFrame` is processed to create a convenient lookup structure. For each unique column, a sorted list of `(extra_space, num_lines)` tuples is created. This allows for quick identification of the minimum `extra` space required for a given `column` to fit within a `target_max_lines`. + + .. code-block:: python + + unique_cols = input_df['col'].unique().tolist() + col_extra_num_lines_options = {} + for col_name in unique_cols: + col_data = input_df[input_df['col'] == col_name].sort_values(by='extra') + col_extra_num_lines_options[col_name] = list(zip(col_data['extra'], col_data['num_lines'])) + + 2. **Defining the Search Space (Bounds for `max_lines`):** + The binary search operates on the possible values for the `optimal_max_lines`. + * **Lower Bound (`L`):** The absolute minimum number of lines observed across all columns and all `extra` space options in the input data. This represents the theoretical minimum height a column could ever achieve. + * **Upper Bound (`R`):** The absolute maximum number of lines observed across all columns and all `extra` space options in the input data. This represents the worst-case height, which is always achievable. + + .. code-block:: python + + all_num_lines = input_df['num_lines'].unique() + if len(all_num_lines) == 0: + return 0, {} # Handle empty DataFrame case + L = all_num_lines.min() + R = all_num_lines.max() + + 3. **The `check(target_max_lines)` Function:** + This is the core helper function for the binary search. Given a `target_max_lines` (a candidate for the overall maximum height), it determines if it's *possible* to achieve this height for *all* columns simultaneously, without exceeding the `total_es_budget`. + + For each column: + * It iterates through its `(extra_space, num_lines)` options (which are sorted by `extra_space`). + * It finds the *smallest* `extra_space` value for which the corresponding `num_lines` is less than or config.equal to `target_max_lines`. + * If no such `extra_space` is found for a column (meaning even with the maximum available `extra` for that column, it still exceeds `target_max_lines`), then `target_max_lines` is not achievable, and the function returns `False`. + * Otherwise, it sums up these minimum required `extra_space` values across all columns. + * If the total `extra_space` required is less than or config.equal to `total_es_budget`, the function returns `True` (meaning `target_max_lines` is achievable). Otherwise, it returns `False`. + + .. code-block:: python + + def check(target_max_lines: int) -> bool: + current_extra_needed = 0 + for col_name in unique_cols: + min_extra_for_col = float('inf') + found_suitable_extra = False + for extra_val, num_lines_val in col_extra_num_lines_options[col_name]: + if num_lines_val <= target_max_lines: + min_extra_for_col = extra_val + found_suitable_extra = True + break # Found the minimum extra for this column + + if not found_suitable_extra: + return False # This target_max_lines is too low for this column + + current_extra_needed += min_extra_for_col + + return current_extra_needed <= total_es_budget + + 4. **Binary Search Loop:** + The main binary search loop iteratively narrows down the range `[L, R]`. + * In each iteration, it calculates the `mid_max_lines = L + (R - L) // 2`. + * It then calls the `check(mid_max_lines)` function. + * If `check(mid_max_lines)` returns `True` (meaning `mid_max_lines` is achievable): + * `mid_max_lines` becomes a candidate for the `optimal_max_lines`. We record the current allocation that achieved it. + * We try to achieve an even smaller `max_lines` by setting `R = mid_max_lines - 1`. + * If `check(mid_max_lines)` returns `False` (meaning `mid_max_lines` is not achievable): + * We need to allow for more lines, so we set `L = mid_max_lines + 1`. + + The loop continues until `L > R`, at which point `optimal_max_lines` will hold the smallest possible maximum height, and `best_allocation` will store the corresponding `extra_space` allocation for each column. + + .. code-block:: python + + optimal_max_lines = R + best_allocation = {} + + while L <= R: + mid_max_lines = L + (R - L) // 2 + + # Recalculate allocation within the loop to store the specific 'extra' values + temp_current_extra_needed = 0 + temp_current_allocation = {} + possible = True + for col_name in unique_cols: + min_extra_for_col = float('inf') + found_suitable_extra = False + for extra_val, num_lines_val in col_extra_num_lines_options[col_name]: + if num_lines_val <= mid_max_lines: + min_extra_for_col = extra_val + found_suitable_extra = True + break + + if not found_suitable_extra: + possible = False + break + + temp_current_extra_needed += min_extra_for_col + temp_current_allocation[col_name] = min_extra_for_col + + if possible and temp_current_extra_needed <= total_es_budget: + optimal_max_lines = mid_max_lines + best_allocation = temp_current_allocation.copy() + R = mid_max_lines - 1 + else: + L = mid_max_lines + 1 + + The function returns the `optimal_max_lines` and the `best_allocation` dictionary, mapping each column name to the minimal `extra_space` it needs to achieve that optimal height. + + Why this approach is effective: + ------------------------------ + + * **Optimal Solution:** The binary search guarantees finding the absolute minimum possible `max_lines` because it systematically explores the entire solution space. + * **Efficiency:** The `check` function runs in time proportional to the number of columns times the average number of `extra` options per column. The binary search itself performs `log(range_of_num_lines)` iterations. This makes the overall complexity efficient for typical table sizes. + * **Flexibility:** It does not assume any particular mathematical function relating `extra` space to `num_lines`. It works with arbitrary discrete relationships provided in the input DataFrame, as long as `num_lines` is non-increasing as `extra` increases (which is the natural expectation for this problem). + + + """ + # Pre-processing + unique_cols = input_df['col'].unique().tolist() + + col_extra_num_lines_options = {} + for col_name in unique_cols: + col_data = input_df[input_df['col'] == + col_name].sort_values(by='extra') + col_extra_num_lines_options[col_name] = list( + zip(col_data['extra'], col_data['num_lines'])) + + def check(target_max_lines: int) -> bool: + current_extra_needed = 0 + for col_name in unique_cols: + min_extra_for_col = float('inf') + found_suitable_extra = False + for extra_val, num_lines_val in col_extra_num_lines_options[col_name]: + if num_lines_val <= target_max_lines: + min_extra_for_col = extra_val + found_suitable_extra = True + break + + if not found_suitable_extra: + return False + + current_extra_needed += min_extra_for_col + + return current_extra_needed <= total_es_budget + + all_num_lines = input_df['num_lines'].unique() + + # Corrected line: Check length of the numpy array + if len(all_num_lines) == 0: + return 0, {} + + L = all_num_lines.min() + R = all_num_lines.max() + + optimal_max_lines = R + best_allocation = {} + + while L <= R: + mid_max_lines = L + (R - L) // 2 + + temp_current_extra_needed = 0 + temp_current_allocation = {} + possible = True + for col_name in unique_cols: + min_extra_for_col = float('inf') + found_suitable_extra = False + for extra_val, num_lines_val in col_extra_num_lines_options[col_name]: + if num_lines_val <= mid_max_lines: + min_extra_for_col = extra_val + found_suitable_extra = True + break + + if not found_suitable_extra: + possible = False + break + + temp_current_extra_needed += min_extra_for_col + temp_current_allocation[col_name] = min_extra_for_col + + if possible and temp_current_extra_needed <= total_es_budget: + optimal_max_lines = mid_max_lines + best_allocation = temp_current_allocation.copy() + R = mid_max_lines - 1 + else: + L = mid_max_lines + 1 + + return optimal_max_lines, best_allocation + + def to_string_custom(self): + """Print to string using new functionality.""" + if self.df.empty: + return "" + + cw_df = self.make_column_width_df() + cw = cw_df['recommended'] + aligners = cw_df['alignment'] + txt = GT.to_text_table(self.df, cw, aligners, index_levels=self.nindex) + return txt + + def to_string_tabulate(self): + """(Old) string representation using tabulate but with new col widther.""" + if self.df.empty: + return "" + + cw_df = self.make_column_width_df() + cw = list(cw_df['recommended']) + aligners = list(cw_df['alignment']) + txt = self.df.to_markdown( + index=False, # NEVER show index; it's subsumed into self.df + colalign=aligners, + tablefmt=self.str_table_fmt, + maxcolwidths=cw, + maxheadercolwidths=cw, + ) + return txt + + def make_style(self, config.tabs): + """Write out custom CSS for the table.""" + if self.config.debug: + head_tb = '#0ff' + body_b = '#f0f' + h0 = '#f00' + h1 = '#b00' + h2 = '#900' + bh0 = '#f00' + bh1 = '#b00' + v0 = '#0f0' + v1 = '#0a0' + v2 = '#090' + else: + head_tb = '#000' + body_b = '#000' + h0 = '#000' + h1 = '#000' + h2 = '#000' + bh0 = '#000' + bh1 = '#000' + v0 = '#000' + v1 = '#000' + v2 = '#000' + table_hrule = self.config.table_hrule_width + table_vrule = self.config.table_vrule_width + # for local use + padt, padr, padb, padl = self.padt, self.padr, self.padb, self.padl + + style = [f''' +') + logger.info('CREATED CSS') + return '\n'.join(style) + + def make_html(self): + """Convert a pandas DataFrame to an HTML table.""" + index_name_to_level = dict( + zip(self.raw_df.index.names, range(self.nindex))) + index_change_level = self.index_change_level.map(index_name_to_level) + # this is easier and computed in the init + column_change_level = self.column_change_level + + # Start table + html = [f''] + if self.label != "": + pass + # TODO put in achor tag somehow!! + if self.caption != '': + html.append(f'') + + # Process header: allow_duplicates=True means can create cols with the same name + bit = self.df.T.reset_index(drop=False, allow_duplicates=True) + idx_header = bit.iloc[:self.nindex, :self.ncolumns] + columns = bit.iloc[self.nindex:, :self.ncolumns] + + colw, tabs = GT.estimate_column_widths( + self.df, self.config.max_table_width, nc_index=self.nindex, scale=1, equal=self.config.equal) + if self.config.debug: + print(f'Make html Input {self.config.tabs=}\nComputed {tabs=}') + if self.config.tabs is not None: + if len(tabs) == len(self.config.tabs): + tabs = self.config.tabs + elif len(self.config.tabs) == 1: + tabs = self.config.tabs * len(tabs) + else: + logger.error( + f'{self.config.tabs=} must be None, a single number, or a list of numbers of the correct length. Ignoring.') + # print('HTML ' + ', '.join([f'{c:,.2f}' for c in tabs])) + + # set column widths; tabs returns lengths of strings in each column + # for proportional fonts, average char is 0.4 to 0.5 em but numbers with + # tabular-nums are fixed 0.5, so use that + # scale: want tables about 150-200 char wide, 1 char = 0.5 px size of font + # so what 75-100 em wide in total + # add the padding + # TODO FONT SIZE + # /4 works well for the tests (handles dates) but seems a bit illogical... + # guessing font size... + tabs = np.array(tabs) + (self.padl + self.padr) / 12 + # em_per_char = 0.5; true exactly for tabular-nums + em_per_char = 0.6 + tabs = tabs * em_per_char + # this gets stripped out by quarto, so make part of style + html.append('') + for w in tabs: + html.append(f'') + html.append('') + + # TODO Add header aligners + # this is TRANSPOSED!! + if self.config.sparsify_columns: + html.append("") + for i in range(self.ncolumns): + # one per row of columns m index, usually only 1 + html.append("") + if self.show_index: + for j, r in enumerate(idx_header.iloc[:, i]): + # columns one per level of index + html.append(f'') + # if not for col span issue you could just to this: + # for j in range(self.ncols): + # hrule = f'grt-bhrule-{i}' if i < self.ncolumns - 1 else '' + # if j == 0: + # # start with the first column come what may + # vrule = f'grt-vrule-index' + # elif j >= self.column_change_level[i]: + # vrule = f'grt-vrule-{column_change_level[cum_col]}' + # else: + # vrule = '' + # html.append(f'') + # here, the groupby needs to consider all levels at and above i + # this concats all the levels + # need :i+1 to get down to the ith level + cum_col = 0 # keep track of where we are up to + for j, (nm, g) in enumerate(groupby(columns.iloc[:, :i + 1]. + apply(lambda x: ':::'.join(str(i) for i in x), axis=1))): + # ::: needs to be something that does not appear in the col names + # need to combine for groupby but be able to split off the last level + # picks off the name of the bottom level + nm = nm.split(':::')[-1] + hrule = f'grt-bhrule-{i}' if i < self.ncolumns - 1 else '' + colspan = sum(1 for _ in g) + if 0 < j: + vrule = f'grt-vrule-{column_change_level[cum_col]}' + elif j == 0 and self.show_index: + # start with the first column if showing index + vrule = f'grt-vrule-index' + else: + vrule = '' + if j == 0 and not self.show_index: + # first column, no index, left align label + html.append( + f'') + else: + html.append( + f'') + cum_col += colspan + html.append("") + html.append("") + else: + html.append("") + for i in range(self.ncolumns): + # one per row of columns m index, usually only 1 + html.append("") + if self.show_index: + for j, r in enumerate(idx_header.iloc[:, i]): + # columns one per level of index + html.append(f'') + for j, r in enumerate(columns.iloc[:, i]): + # one per column of dataframe + # figure how high up mindex the vrules go + # all headings get hrules, it's the vrules that are tricky + hrule = f'grt-bhrule-{i}' if i < self.ncolumns - 1 else '' + if 0 < j < self.ncols and i >= column_change_level[j]: + vrule = f'grt-vrule-{column_change_level[j]}' + elif j == 0 and self.show_index: + # start with the first column come what may + vrule = f'grt-vrule-index' + else: + vrule = '' + html.append( + f'') + html.append("") + html.append("") + + bold_idx = 'grt-bold' if self.config.font_bold_index else '' + html.append("") + for i, (n, r) in enumerate(self.df.iterrows()): + # one per row of dataframe + html.append("") + hrule = '' + if self.show_index: + for j, c in enumerate(r.iloc[:self.nindex]): + # dx = data in index + # if this is the level that changes for this row + # will use a top rule hence omit i = 0 which already has an hrule + if i > 0 and hrule == '' and j == index_change_level[i]: + hrule = f'grt-hrule-{j}' + # html.append(f'') + col_id = f'grt-c-{j}' + html.append( + f'') + for j, c in enumerate(r.iloc[self.nindex:]): + # first col left handled by index/body divider + if 0 < j < self.ncols: + vrule = f'grt-vrule-{column_change_level[j]}' + elif j == 0 and self.show_index: + # start with the first column come what may + vrule = f'grt-vrule-index' + else: + vrule = '' + # html.append(f'') + col_id = f'grt-c-{j+self.nindex}' + html.append( + f'') + html.append("") + html.append("") + text = '\n'.join(html) + self.df_html = GT.clean_html_tex(text) + logger.info('CREATED HTML') + self.df_style = self.make_style(tabs) + + return self.df_html + + def clean_style(self, soup): + """Minify CSS inside \n", + "
{self.caption}
{r}{nm}{nm}{nm}
{r}{r}
{c}{c}{c}{c}
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
c0cause administration arrivetelevision four quality
i0
2020-06-24issue2024-08-17
2020-06-25hospital account direction2024-08-18
2020-06-26community create trouble enjoy2024-08-19
2020-06-27thank although best why month team class admin...2024-08-20
2020-06-28difference natural discussion report want test2024-08-21
2020-06-29year structure position poor company word road...2024-08-22
2020-06-30contain growth response win part enough if pro...2024-08-23
2020-07-01statement picture cut phone2024-08-24
2020-07-02perform force remember certainly their least l...2024-08-25
2020-07-03national common2024-08-26
\n", + "" + ], + "text/plain": [ + "c0 cause administration arrive \\\n", + "i0 \n", + "2020-06-24 issue \n", + "2020-06-25 hospital account direction \n", + "2020-06-26 community create trouble enjoy \n", + "2020-06-27 thank although best why month team class admin... \n", + "2020-06-28 difference natural discussion report want test \n", + "2020-06-29 year structure position poor company word road... \n", + "2020-06-30 contain growth response win part enough if pro... \n", + "2020-07-01 statement picture cut phone \n", + "2020-07-02 perform force remember certainly their least l... \n", + "2020-07-03 national common \n", + "\n", + "c0 television four quality \n", + "i0 \n", + "2020-06-24 2024-08-17 \n", + "2020-06-25 2024-08-18 \n", + "2020-06-26 2024-08-19 \n", + "2020-06-27 2024-08-20 \n", + "2020-06-28 2024-08-21 \n", + "2020-06-29 2024-08-22 \n", + "2020-06-30 2024-08-23 \n", + "2020-07-01 2024-08-24 \n", + "2020-07-02 2024-08-25 \n", + "2020-07-03 2024-08-26 " + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 20.6 ms (started: 2025-06-12 23:16:45 +01:00)\n" + ] + } + ], + "source": [ + "TDF.make(10, 's10d', index='d', col_index='s')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05eb4656-4fa6-4440-bee2-1231e2ac7d49", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc1eb636-7cf4-4feb-8f08-93ec1434f2d9", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "9da1ef85-0175-43ce-be14-726f7ae9141d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 1.67 ms (started: 2025-06-12 11:49:21 +01:00)\n" + ] + } + ], + "source": [ + "config = gtc.GTConfigModel()\n", + "gt = gtc.GTTest(None, config=config)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "7084f834-8891-49ba-a9fd-15eb293b6dc2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 6.2 ms (started: 2025-06-12 11:47:12 +01:00)\n" + ] + } + ], + "source": [ + "gtc.write_template('\\\\tmp\\\\config.yaml')" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "6dd5f72c-415b-4a08-8415-18c571aeb28e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 1.6 ms (started: 2025-06-12 18:47:25 +01:00)\n" + ] + } + ], + "source": [ + "import re\n", + "from pathlib import Path" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "1a22d386-d6c9-4811-a72b-c5902bd7480f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['default_integer_str', 'default_float_str', 'default_date_str', 'default_ratio_str', 'default_formatter', 'table_float_format', 'table_hrule_width', 'table_vrule_width', 'hrule_widths', 'vrule_widths', 'sparsify', 'sparsify_columns', 'spacing', 'padding_trbl', 'tikz_scale', 'font_body', 'font_head', 'font_caption', 'font_bold_index', 'pef_precision', 'pef_lower', 'pef_upper', 'cast_to_floats', 'header_row', 'tabs', 'equal', 'caption_align', 'large_ok', 'max_str_length', 'max_table_width', 'table_width_mode', 'table_width_header_adjust', 'table_width_header_relax', 'debug'])" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 4 ms (started: 2025-06-12 18:38:44 +01:00)\n" + ] + } + ], + "source": [ + "gtc.GTConfigModel.model_fields.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "c6dbd8e4-ef52-42bf-b149-21499b051ca7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 3.47 ms (started: 2025-06-12 18:42:34 +01:00)\n" + ] + } + ], + "source": [ + "pattern = re.compile('|'.join(f'({x})' for x in gtc.GTConfigModel.model_fields.keys()))" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "270ec5fa-4744-439c-92e3-d5f563369462", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 1.75 ms (started: 2025-06-12 18:45:54 +01:00)\n" + ] + } + ], + "source": [ + "keys = '|'.join(gtc.GTConfigModel.model_fields.keys())\n", + "pattern = re.compile(f'({keys})')" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "77e0f075-8894-4d3d-9b75-3ec3e416d298", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 1.29 ms (started: 2025-06-12 18:45:55 +01:00)\n" + ] + } + ], + "source": [ + "txt = 'self.default_integer_str and stuff.header_row and tabs'" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "e53197ea-fb16-441e-814d-9d4904d4ea47", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'self.config.default_integer_str and stuff.config.header_row and config.tabs'" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 3.83 ms (started: 2025-06-12 18:46:17 +01:00)\n" + ] + } + ], + "source": [ + "pattern.sub(lambda m: f'config.{m.group(0)}', txt)" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "ac8abfff-0409-4c40-b1a0-5a4d1ae7d0ff", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 4.62 ms (started: 2025-06-12 18:47:30 +01:00)\n" + ] + } + ], + "source": [ + "p = Path('greater_tables/gtcore.py')\n", + "p.exists()" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "c7967b91-d4ac-4962-9ab2-62e3b88cab1f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 62.9 ms (started: 2025-06-12 18:47:56 +01:00)\n" + ] + } + ], + "source": [ + "txt = p.read_text(encoding='utf-8')\n", + "txt = pattern.sub(lambda m: f'config.{m.group(0)}', txt)" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "e6f9de04-3e0f-4b9d-9d1a-354f97e0ed94", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "123189" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 4.58 ms (started: 2025-06-12 18:49:40 +01:00)\n" + ] + } + ], + "source": [ + "Path('greater_tables/gtcore2.py').write_text(txt, encoding='utf-8')" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "55405b3b-1fe3-4131-beb2-d4e088092a5c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'%Y-%m-%d'" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 2.98 ms (started: 2025-06-12 11:45:42 +01:00)\n" + ] + } + ], + "source": [ + "gt.config.default_date_str " + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "57d04589-2d5f-444a-be64-a56c71626386", + "metadata": {}, + "outputs": [ + { + "ename": "ValidationError", + "evalue": "1 validation error for GTConfigModel\ndefault_date_str\n Instance is frozen [type=frozen_instance, input_value='error', input_type=str]\n For further information visit https://errors.pydantic.dev/2.11/v/frozen_instance", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mValidationError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[16]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43mgt\u001b[49m\u001b[43m.\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m.\u001b[49m\u001b[43mdefault_date_str\u001b[49m = \u001b[33m'\u001b[39m\u001b[33merror\u001b[39m\u001b[33m'\u001b[39m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\miniconda3\\envs\\working313\\Lib\\site-packages\\pydantic\\main.py:997\u001b[39m, in \u001b[36mBaseModel.__setattr__\u001b[39m\u001b[34m(self, name, value)\u001b[39m\n\u001b[32m 995\u001b[39m setattr_handler(\u001b[38;5;28mself\u001b[39m, name, value)\n\u001b[32m 996\u001b[39m \u001b[38;5;66;03m# if None is returned from _setattr_handler, the attribute was set directly\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m997\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m (setattr_handler := \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_setattr_handler\u001b[49m\u001b[43m(\u001b[49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m)\u001b[49m) \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 998\u001b[39m setattr_handler(\u001b[38;5;28mself\u001b[39m, name, value) \u001b[38;5;66;03m# call here to not memo on possibly unknown fields\u001b[39;00m\n\u001b[32m 999\u001b[39m \u001b[38;5;28mself\u001b[39m.__pydantic_setattr_handlers__[name] = setattr_handler\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\miniconda3\\envs\\working313\\Lib\\site-packages\\pydantic\\main.py:1033\u001b[39m, in \u001b[36mBaseModel._setattr_handler\u001b[39m\u001b[34m(self, name, value)\u001b[39m\n\u001b[32m 1030\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(attr, cached_property):\n\u001b[32m 1031\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m _SIMPLE_SETATTR_HANDLERS[\u001b[33m'\u001b[39m\u001b[33mcached_property\u001b[39m\u001b[33m'\u001b[39m]\n\u001b[32m-> \u001b[39m\u001b[32m1033\u001b[39m \u001b[43m_check_frozen\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mcls\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1035\u001b[39m \u001b[38;5;66;03m# We allow properties to be set only on non frozen models for now (to match dataclasses).\u001b[39;00m\n\u001b[32m 1036\u001b[39m \u001b[38;5;66;03m# This can be changed if it ever gets requested.\u001b[39;00m\n\u001b[32m 1037\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(attr, \u001b[38;5;28mproperty\u001b[39m):\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\miniconda3\\envs\\working313\\Lib\\site-packages\\pydantic\\main.py:92\u001b[39m, in \u001b[36m_check_frozen\u001b[39m\u001b[34m(model_cls, name, value)\u001b[39m\n\u001b[32m 89\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 90\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m92\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m ValidationError.from_exception_data(\n\u001b[32m 93\u001b[39m model_cls.\u001b[34m__name__\u001b[39m, [{\u001b[33m'\u001b[39m\u001b[33mtype\u001b[39m\u001b[33m'\u001b[39m: error_type, \u001b[33m'\u001b[39m\u001b[33mloc\u001b[39m\u001b[33m'\u001b[39m: (name,), \u001b[33m'\u001b[39m\u001b[33minput\u001b[39m\u001b[33m'\u001b[39m: value}]\n\u001b[32m 94\u001b[39m )\n", + "\u001b[31mValidationError\u001b[39m: 1 validation error for GTConfigModel\ndefault_date_str\n Instance is frozen [type=frozen_instance, input_value='error', input_type=str]\n For further information visit https://errors.pydantic.dev/2.11/v/frozen_instance" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 218 ms (started: 2025-06-12 11:45:50 +01:00)\n" + ] + } + ], + "source": [ + "gt.config.default_date_str = 'error'" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "ad11d45e-64a3-4efe-a2d7-a48aef8efa0a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 4.99 ms (started: 2025-06-12 19:18:01 +01:00)\n" + ] + } + ], + "source": [ + "import greater_tables.gtenums as gte" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "id": "8d11d440-e5f4-4569-a5b8-2f9eea4fe86b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 1.34 ms (started: 2025-06-12 19:18:57 +01:00)\n" + ] + } + ], + "source": [ + "a = gte.Alignment\n", + "b = gte.Alignment" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "2d6e9eb6-a1af-435a-9239-94e9b050d826", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 3.26 ms (started: 2025-06-12 19:19:45 +01:00)\n" + ] + } + ], + "source": [ + "a.LEFT.value == 'l'" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "7130f05d-c9ee-4ff3-8ac8-f21a9d6cf717", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
╭──────────────────────────────── <class 'greater_tables.gtconfig.GTConfigModel'> ────────────────────────────────╮\n",
+       " Configuration model for GreaterTables.                                                                          \n",
+       "                                                                                                                 \n",
+       " ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ \n",
+       "  GTConfigModel(                                                                                               \n",
+       "  default_integer_str='{x:,d}',                                                                            \n",
+       "  default_float_str='{x:,.3f}',                                                                            \n",
+       "  default_date_str='%Y-%m-%d',                                                                             \n",
+       "  default_ratio_str='{x:.1%}',                                                                             \n",
+       "  default_formatter=None,                                                                                  \n",
+       "  table_float_format=None,                                                                                 \n",
+       "  table_hrule_width=1,                                                                                     \n",
+       "  table_vrule_width=1,                                                                                     \n",
+       "  hrule_widths=None,                                                                                       \n",
+       "  vrule_widths=None,                                                                                       \n",
+       "  sparsify=True,                                                                                           \n",
+       "  sparsify_columns=True,                                                                                   \n",
+       "  spacing='medium',                                                                                        \n",
+       "  padding_trbl=None,                                                                                       \n",
+       "  tikz_scale=1.0,                                                                                          \n",
+       "  font_body=0.9,                                                                                           \n",
+       "  font_head=1.0,                                                                                           \n",
+       "  font_caption=1.1,                                                                                        \n",
+       "  font_bold_index=False,                                                                                   \n",
+       "  pef_precision=3,                                                                                         \n",
+       "  pef_lower=-3,                                                                                            \n",
+       "  pef_upper=6,                                                                                             \n",
+       "  cast_to_floats=True,                                                                                     \n",
+       "  header_row=True,                                                                                         \n",
+       "  tabs=None,                                                                                               \n",
+       "  equal=False,                                                                                             \n",
+       "  caption_align='center',                                                                                  \n",
+       "  large_ok=False,                                                                                          \n",
+       "  max_str_length=-1,                                                                                       \n",
+       "  max_table_width=200,                                                                                     \n",
+       "  table_width_mode='explicit',                                                                             \n",
+       "  table_width_header_adjust=0.1,                                                                           \n",
+       "  table_width_header_relax=10.0,                                                                           \n",
+       "  debug=False                                                                                              \n",
+       "  )                                                                                                            \n",
+       " ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ \n",
+       "                                                                                                                 \n",
+       "             caption_align = 'center'                                                                            \n",
+       "            cast_to_floats = True                                                                                \n",
+       "                     debug = False                                                                               \n",
+       "          default_date_str = '%Y-%m-%d'                                                                          \n",
+       "         default_float_str = '{x:,.3f}'                                                                          \n",
+       "         default_formatter = None                                                                                \n",
+       "       default_integer_str = '{x:,d}'                                                                            \n",
+       "         default_ratio_str = '{x:.1%}'                                                                           \n",
+       "                     equal = False                                                                               \n",
+       "                 font_body = 0.9                                                                                 \n",
+       "           font_bold_index = False                                                                               \n",
+       "              font_caption = 1.1                                                                                 \n",
+       "                 font_head = 1.0                                                                                 \n",
+       "                header_row = True                                                                                \n",
+       "              hrule_widths = None                                                                                \n",
+       "                  large_ok = False                                                                               \n",
+       "            max_str_length = -1                                                                                  \n",
+       "           max_table_width = 200                                                                                 \n",
+       "     model_computed_fields = {}                                                                                  \n",
+       "              model_config = {'frozen': True}                                                                    \n",
+       "               model_extra = None                                                                                \n",
+       "              model_fields = {                                                                                   \n",
+       "                                 'default_integer_str': FieldInfo(                                               \n",
+       "                                     annotation=str,                                                             \n",
+       "                                     required=False,                                                             \n",
+       "                                     default='{x:,d}',                                                           \n",
+       "                                     description=\"Format f-string for integers. Example: '{x:,d}'\"               \n",
+       "                                 ),                                                                              \n",
+       "                                 'default_float_str': FieldInfo(                                                 \n",
+       "                                     annotation=str,                                                             \n",
+       "                                     required=False,                                                             \n",
+       "                                     default='{x:,.3f}',                                                         \n",
+       "                                     description=\"Format f-string for floats. Example: '{x:,.3f}'\"               \n",
+       "                                 ),                                                                              \n",
+       "                                 'default_date_str': FieldInfo(                                                  \n",
+       "                                     annotation=str,                                                             \n",
+       "                                     required=False,                                                             \n",
+       "                                     default='%Y-%m-%d',                                                         \n",
+       "                                     description=\"Format string for dates (no braces or 'x'). Example:           \n",
+       "                             '%Y-%m-%d'\"                                                                         \n",
+       "                                 ),                                                                              \n",
+       "                                 'default_ratio_str': FieldInfo(                                                 \n",
+       "                                     annotation=str,                                                             \n",
+       "                                     required=False,                                                             \n",
+       "                                     default='{x:.1%}',                                                          \n",
+       "                                     description=\"Format f-string for ratios. Example: '{x:.1%}'\"                \n",
+       "                                 ),                                                                              \n",
+       "                                 'default_formatter': FieldInfo(                                                 \n",
+       "                                     annotation=Union[str, NoneType],                                            \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=None,                                                               \n",
+       "                                     description='Optional fallback formatter f-string'                          \n",
+       "                                 ),                                                                              \n",
+       "                                 'table_float_format': FieldInfo(                                                \n",
+       "                                     annotation=Union[str, NoneType],                                            \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=None,                                                               \n",
+       "                                     description='Float format string for the entire table; overrides            \n",
+       "                             column-specific formats'                                                            \n",
+       "                                 ),                                                                              \n",
+       "                                 'table_hrule_width': FieldInfo(                                                 \n",
+       "                                     annotation=int,                                                             \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=1,                                                                  \n",
+       "                                     description='Width of top, bottom, and header horizontal rules'             \n",
+       "                                 ),                                                                              \n",
+       "                                 'table_vrule_width': FieldInfo(                                                 \n",
+       "                                     annotation=int,                                                             \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=1,                                                                  \n",
+       "                                     description='Width of vertical rule separating index from body'             \n",
+       "                                 ),                                                                              \n",
+       "                                 'hrule_widths': FieldInfo(                                                      \n",
+       "                                     annotation=Union[tuple[int, int, int], NoneType],                           \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=None,                                                               \n",
+       "                                     description='Tuple of three ints for horizontal rule widths (for multiindex \n",
+       "                             use)'                                                                               \n",
+       "                                 ),                                                                              \n",
+       "                                 'vrule_widths': FieldInfo(                                                      \n",
+       "                                     annotation=Union[tuple[int, int, int], NoneType],                           \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=None,                                                               \n",
+       "                                     description='Tuple of three ints for vertical rule widths (for multiindex   \n",
+       "                             columns)'                                                                           \n",
+       "                                 ),                                                                              \n",
+       "                                 'sparsify': FieldInfo(                                                          \n",
+       "                                     annotation=bool,                                                            \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=True,                                                               \n",
+       "                                     description='If True, sparsify index columns (recommended)'                 \n",
+       "                                 ),                                                                              \n",
+       "                                 'sparsify_columns': FieldInfo(                                                  \n",
+       "                                     annotation=bool,                                                            \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=True,                                                               \n",
+       "                                     description='If True, sparsify column headers using colspans'               \n",
+       "                                 ),                                                                              \n",
+       "                                 'spacing': FieldInfo(                                                           \n",
+       "                                     annotation=str,                                                             \n",
+       "                                     required=False,                                                             \n",
+       "                                     default='medium',                                                           \n",
+       "                                     description=\"Shorthand for cell padding. One of: 'tight', 'medium', 'wide'\" \n",
+       "                                 ),                                                                              \n",
+       "                                 'padding_trbl': FieldInfo(                                                      \n",
+       "                                     annotation=Union[tuple[int, int, int, int], NoneType],                      \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=None,                                                               \n",
+       "                                     description='Manual padding in the order (top, right, bottom, left)'        \n",
+       "                                 ),                                                                              \n",
+       "                                 'tikz_scale': FieldInfo(                                                        \n",
+       "                                     annotation=float,                                                           \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=1.0,                                                                \n",
+       "                                     description='Scaling factor applied to LaTeX TikZ tables'                   \n",
+       "                                 ),                                                                              \n",
+       "                                 'font_body': FieldInfo(                                                         \n",
+       "                                     annotation=float,                                                           \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=0.9,                                                                \n",
+       "                                     description='Font size for body text (in em units)'                         \n",
+       "                                 ),                                                                              \n",
+       "                                 'font_head': FieldInfo(                                                         \n",
+       "                                     annotation=float,                                                           \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=1.0,                                                                \n",
+       "                                     description='Font size for header text (in em units)'                       \n",
+       "                                 ),                                                                              \n",
+       "                                 'font_caption': FieldInfo(                                                      \n",
+       "                                     annotation=float,                                                           \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=1.1,                                                                \n",
+       "                                     description='Font size for caption text (in em units)'                      \n",
+       "                                 ),                                                                              \n",
+       "                                 'font_bold_index': FieldInfo(                                                   \n",
+       "                                     annotation=bool,                                                            \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=False,                                                              \n",
+       "                                     description='If True, make index columns bold'                              \n",
+       "                                 ),                                                                              \n",
+       "                                 'pef_precision': FieldInfo(                                                     \n",
+       "                                     annotation=int,                                                             \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=3,                                                                  \n",
+       "                                     description='Precision for engineering format (digits after decimal)'       \n",
+       "                                 ),                                                                              \n",
+       "                                 'pef_lower': FieldInfo(                                                         \n",
+       "                                     annotation=int,                                                             \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=-3,                                                                 \n",
+       "                                     description='Lower threshold: apply engineering format if abs(x) <          \n",
+       "                             10**pef_lower'                                                                      \n",
+       "                                 ),                                                                              \n",
+       "                                 'pef_upper': FieldInfo(                                                         \n",
+       "                                     annotation=int,                                                             \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=6,                                                                  \n",
+       "                                     description='Upper threshold: apply engineering format if abs(x) >          \n",
+       "                             10**pef_upper'                                                                      \n",
+       "                                 ),                                                                              \n",
+       "                                 'cast_to_floats': FieldInfo(                                                    \n",
+       "                                     annotation=bool,                                                            \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=True,                                                               \n",
+       "                                     description='If True, cast non-integer, non-date columns to float where     \n",
+       "                             possible'                                                                           \n",
+       "                                 ),                                                                              \n",
+       "                                 'header_row': FieldInfo(                                                        \n",
+       "                                     annotation=bool,                                                            \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=True,                                                               \n",
+       "                                     description='If True, use the first row as header; False disables header    \n",
+       "                             row'                                                                                \n",
+       "                                 ),                                                                              \n",
+       "                                 'tabs': FieldInfo(                                                              \n",
+       "                                     annotation=Union[list[float], float, int, NoneType],                        \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=None,                                                               \n",
+       "                                     description='Column widths in characters or ems; None triggers              \n",
+       "                             auto-calculation'                                                                   \n",
+       "                                 ),                                                                              \n",
+       "                                 'equal': FieldInfo(                                                             \n",
+       "                                     annotation=bool,                                                            \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=False,                                                              \n",
+       "                                     description='If True, force equal column widths (may be ignored if          \n",
+       "                             conflicting)'                                                                       \n",
+       "                                 ),                                                                              \n",
+       "                                 'caption_align': FieldInfo(                                                     \n",
+       "                                     annotation=str,                                                             \n",
+       "                                     required=False,                                                             \n",
+       "                                     default='center',                                                           \n",
+       "                                     description='Alignment of the caption text'                                 \n",
+       "                                 ),                                                                              \n",
+       "                                 'large_ok': FieldInfo(                                                          \n",
+       "                                     annotation=bool,                                                            \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=False,                                                              \n",
+       "                                     description='If True, allow full rendering of large tables without          \n",
+       "                             truncation'                                                                         \n",
+       "                                 ),                                                                              \n",
+       "                                 'max_str_length': FieldInfo(                                                    \n",
+       "                                     annotation=int,                                                             \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=-1,                                                                 \n",
+       "                                     description='Maximum length for stringified objects (e.g. nested            \n",
+       "                             DataFrames); -1 = unlimited'                                                        \n",
+       "                                 ),                                                                              \n",
+       "                                 'max_table_width': FieldInfo(                                                   \n",
+       "                                     annotation=int,                                                             \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=200,                                                                \n",
+       "                                     description='Maximum table width for markdown/text output mode'             \n",
+       "                                 ),                                                                              \n",
+       "                                 'table_width_mode': FieldInfo(                                                  \n",
+       "                                     annotation=Literal['explicit', 'natural', 'breakable', 'minimum'],          \n",
+       "                                     required=False,                                                             \n",
+       "                                     default='explicit',                                                         \n",
+       "                                     description=\"Mode for determining table width. 'explicit': fixed width      \n",
+       "                             using max_table_width; 'natural': each cell fits its full content; 'breakable':     \n",
+       "                             wrap breakable strings; 'minimum': also wraps dates or float-like cells\"            \n",
+       "                                 ),                                                                              \n",
+       "                                 'table_width_header_adjust': FieldInfo(                                         \n",
+       "                                     annotation=float,                                                           \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=0.1,                                                                \n",
+       "                                     description='Proportion of width allocated to headers to balance content    \n",
+       "                             width'                                                                              \n",
+       "                                 ),                                                                              \n",
+       "                                 'table_width_header_relax': FieldInfo(                                          \n",
+       "                                     annotation=float,                                                           \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=10.0,                                                               \n",
+       "                                     description='Extra characters allowed per column heading to help header     \n",
+       "                             wrapping'                                                                           \n",
+       "                                 ),                                                                              \n",
+       "                                 'debug': FieldInfo(                                                             \n",
+       "                                     annotation=bool,                                                            \n",
+       "                                     required=False,                                                             \n",
+       "                                     default=False,                                                              \n",
+       "                                     description='Run in debug mode with more reporting, include internal ID in  \n",
+       "                             caption and use colored output lines'                                               \n",
+       "                                 )                                                                               \n",
+       "                             }                                                                                   \n",
+       "          model_fields_set = set()                                                                               \n",
+       "              padding_trbl = None                                                                                \n",
+       "                 pef_lower = -3                                                                                  \n",
+       "             pef_precision = 3                                                                                   \n",
+       "                 pef_upper = 6                                                                                   \n",
+       "                   spacing = 'medium'                                                                            \n",
+       "                  sparsify = True                                                                                \n",
+       "          sparsify_columns = True                                                                                \n",
+       "        table_float_format = None                                                                                \n",
+       "         table_hrule_width = 1                                                                                   \n",
+       "         table_vrule_width = 1                                                                                   \n",
+       " table_width_header_adjust = 0.1                                                                                 \n",
+       "  table_width_header_relax = 10.0                                                                                \n",
+       "          table_width_mode = 'explicit'                                                                          \n",
+       "                      tabs = None                                                                                \n",
+       "                tikz_scale = 1.0                                                                                 \n",
+       "              vrule_widths = None                                                                                \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34m╭─\u001b[0m\u001b[34m───────────────────────────────\u001b[0m\u001b[34m \u001b[0m\u001b[1;34m<\u001b[0m\u001b[1;95mclass\u001b[0m\u001b[39m \u001b[0m\u001b[32m'greater_tables.gtconfig.GTConfigModel'\u001b[0m\u001b[1;34m>\u001b[0m\u001b[34m \u001b[0m\u001b[34m───────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[36mConfiguration model for GreaterTables.\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[1;35mGTConfigModel\u001b[0m\u001b[1m(\u001b[0m \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mdefault_integer_str\u001b[0m=\u001b[32m'\u001b[0m\u001b[32m{\u001b[0m\u001b[32mx:,d\u001b[0m\u001b[32m}\u001b[0m\u001b[32m'\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mdefault_float_str\u001b[0m=\u001b[32m'\u001b[0m\u001b[32m{\u001b[0m\u001b[32mx:,.3f\u001b[0m\u001b[32m}\u001b[0m\u001b[32m'\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mdefault_date_str\u001b[0m=\u001b[32m'%Y-%m-%d'\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mdefault_ratio_str\u001b[0m=\u001b[32m'\u001b[0m\u001b[32m{\u001b[0m\u001b[32mx:.1%\u001b[0m\u001b[32m}\u001b[0m\u001b[32m'\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mdefault_formatter\u001b[0m=\u001b[3;35mNone\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mtable_float_format\u001b[0m=\u001b[3;35mNone\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mtable_hrule_width\u001b[0m=\u001b[1;36m1\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mtable_vrule_width\u001b[0m=\u001b[1;36m1\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mhrule_widths\u001b[0m=\u001b[3;35mNone\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mvrule_widths\u001b[0m=\u001b[3;35mNone\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33msparsify\u001b[0m=\u001b[3;92mTrue\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33msparsify_columns\u001b[0m=\u001b[3;92mTrue\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mspacing\u001b[0m=\u001b[32m'medium'\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mpadding_trbl\u001b[0m=\u001b[3;35mNone\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mtikz_scale\u001b[0m=\u001b[1;36m1\u001b[0m\u001b[1;36m.0\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mfont_body\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.9\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mfont_head\u001b[0m=\u001b[1;36m1\u001b[0m\u001b[1;36m.0\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mfont_caption\u001b[0m=\u001b[1;36m1\u001b[0m\u001b[1;36m.1\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mfont_bold_index\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mpef_precision\u001b[0m=\u001b[1;36m3\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mpef_lower\u001b[0m=\u001b[1;36m-3\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mpef_upper\u001b[0m=\u001b[1;36m6\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mcast_to_floats\u001b[0m=\u001b[3;92mTrue\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mheader_row\u001b[0m=\u001b[3;92mTrue\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mtabs\u001b[0m=\u001b[3;35mNone\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mequal\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mcaption_align\u001b[0m=\u001b[32m'center'\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mlarge_ok\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mmax_str_length\u001b[0m=\u001b[1;36m-1\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mmax_table_width\u001b[0m=\u001b[1;36m200\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mtable_width_mode\u001b[0m=\u001b[32m'explicit'\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mtable_width_header_adjust\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.1\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mtable_width_header_relax\u001b[0m=\u001b[1;36m10\u001b[0m\u001b[1;36m.0\u001b[0m, \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[2;32m│ \u001b[0m\u001b[33mdebug\u001b[0m=\u001b[3;91mFalse\u001b[0m \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m│\u001b[0m \u001b[1m)\u001b[0m \u001b[32m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mcaption_align\u001b[0m = \u001b[32m'center'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mcast_to_floats\u001b[0m = \u001b[3;92mTrue\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mdebug\u001b[0m = \u001b[3;91mFalse\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mdefault_date_str\u001b[0m = \u001b[32m'%Y-%m-%d'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mdefault_float_str\u001b[0m = \u001b[32m'\u001b[0m\u001b[32m{\u001b[0m\u001b[32mx:,.3f\u001b[0m\u001b[32m}\u001b[0m\u001b[32m'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mdefault_formatter\u001b[0m = \u001b[3;35mNone\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mdefault_integer_str\u001b[0m = \u001b[32m'\u001b[0m\u001b[32m{\u001b[0m\u001b[32mx:,d\u001b[0m\u001b[32m}\u001b[0m\u001b[32m'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mdefault_ratio_str\u001b[0m = \u001b[32m'\u001b[0m\u001b[32m{\u001b[0m\u001b[32mx:.1%\u001b[0m\u001b[32m}\u001b[0m\u001b[32m'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mequal\u001b[0m = \u001b[3;91mFalse\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mfont_body\u001b[0m = \u001b[1;36m0.9\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mfont_bold_index\u001b[0m = \u001b[3;91mFalse\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mfont_caption\u001b[0m = \u001b[1;36m1.1\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mfont_head\u001b[0m = \u001b[1;36m1.0\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mheader_row\u001b[0m = \u001b[3;92mTrue\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mhrule_widths\u001b[0m = \u001b[3;35mNone\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mlarge_ok\u001b[0m = \u001b[3;91mFalse\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mmax_str_length\u001b[0m = \u001b[1;36m-1\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mmax_table_width\u001b[0m = \u001b[1;36m200\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mmodel_computed_fields\u001b[0m = \u001b[1m{\u001b[0m\u001b[1m}\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mmodel_config\u001b[0m = \u001b[1m{\u001b[0m\u001b[32m'frozen'\u001b[0m: \u001b[3;92mTrue\u001b[0m\u001b[1m}\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mmodel_extra\u001b[0m = \u001b[3;35mNone\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mmodel_fields\u001b[0m = \u001b[1m{\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'default_integer_str'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mstr\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[32m'\u001b[0m\u001b[32m{\u001b[0m\u001b[32mx:,d\u001b[0m\u001b[32m}\u001b[0m\u001b[32m'\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m\"Format\u001b[0m\u001b[32m f-string for integers. Example: '\u001b[0m\u001b[32m{\u001b[0m\u001b[32mx:,d\u001b[0m\u001b[32m}\u001b[0m\u001b[32m'\"\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'default_float_str'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mstr\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[32m'\u001b[0m\u001b[32m{\u001b[0m\u001b[32mx:,.3f\u001b[0m\u001b[32m}\u001b[0m\u001b[32m'\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m\"Format\u001b[0m\u001b[32m f-string for floats. Example: '\u001b[0m\u001b[32m{\u001b[0m\u001b[32mx:,.3f\u001b[0m\u001b[32m}\u001b[0m\u001b[32m'\"\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'default_date_str'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mstr\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[32m'%Y-%m-%d'\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m\"Format\u001b[0m\u001b[32m string for dates \u001b[0m\u001b[32m(\u001b[0m\u001b[32mno braces or 'x'\u001b[0m\u001b[32m)\u001b[0m\u001b[32m. Example: \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'%Y-%m-%d'\"\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'default_ratio_str'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mstr\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[32m'\u001b[0m\u001b[32m{\u001b[0m\u001b[32mx:.1%\u001b[0m\u001b[32m}\u001b[0m\u001b[32m'\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m\"Format\u001b[0m\u001b[32m f-string for ratios. Example: '\u001b[0m\u001b[32m{\u001b[0m\u001b[32mx:.1%\u001b[0m\u001b[32m}\u001b[0m\u001b[32m'\"\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'default_formatter'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mUnion\u001b[0m\u001b[1m[\u001b[0mstr, NoneType\u001b[1m]\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[3;35mNone\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Optional fallback formatter f-string'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'table_float_format'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mUnion\u001b[0m\u001b[1m[\u001b[0mstr, NoneType\u001b[1m]\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[3;35mNone\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Float format string for the entire table; overrides \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32mcolumn-specific formats'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'table_hrule_width'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mint\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[1;36m1\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Width of top, bottom, and header horizontal rules'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'table_vrule_width'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mint\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[1;36m1\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Width of vertical rule separating index from body'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'hrule_widths'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mUnion\u001b[0m\u001b[1m[\u001b[0mtuple\u001b[1m[\u001b[0mint, int, int\u001b[1m]\u001b[0m, NoneType\u001b[1m]\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[3;35mNone\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Tuple of three ints for horizontal rule widths \u001b[0m\u001b[32m(\u001b[0m\u001b[32mfor multiindex\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32muse\u001b[0m\u001b[32m)\u001b[0m\u001b[32m'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'vrule_widths'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mUnion\u001b[0m\u001b[1m[\u001b[0mtuple\u001b[1m[\u001b[0mint, int, int\u001b[1m]\u001b[0m, NoneType\u001b[1m]\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[3;35mNone\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Tuple of three ints for vertical rule widths \u001b[0m\u001b[32m(\u001b[0m\u001b[32mfor multiindex \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32mcolumns\u001b[0m\u001b[32m)\u001b[0m\u001b[32m'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'sparsify'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mbool\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[3;92mTrue\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'If True, sparsify index columns \u001b[0m\u001b[32m(\u001b[0m\u001b[32mrecommended\u001b[0m\u001b[32m)\u001b[0m\u001b[32m'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'sparsify_columns'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mbool\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[3;92mTrue\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'If True, sparsify column headers using colspans'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'spacing'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mstr\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[32m'medium'\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m\"Shorthand\u001b[0m\u001b[32m for cell padding. One of: 'tight', 'medium', 'wide'\"\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'padding_trbl'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mUnion\u001b[0m\u001b[1m[\u001b[0mtuple\u001b[1m[\u001b[0mint, int, int, int\u001b[1m]\u001b[0m, NoneType\u001b[1m]\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[3;35mNone\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Manual padding in the order \u001b[0m\u001b[32m(\u001b[0m\u001b[32mtop, right, bottom, left\u001b[0m\u001b[32m)\u001b[0m\u001b[32m'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'tikz_scale'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mfloat\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[1;36m1\u001b[0m\u001b[1;36m.0\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Scaling factor applied to LaTeX TikZ tables'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'font_body'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mfloat\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.9\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Font size for body text \u001b[0m\u001b[32m(\u001b[0m\u001b[32min em units\u001b[0m\u001b[32m)\u001b[0m\u001b[32m'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'font_head'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mfloat\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[1;36m1\u001b[0m\u001b[1;36m.0\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Font size for header text \u001b[0m\u001b[32m(\u001b[0m\u001b[32min em units\u001b[0m\u001b[32m)\u001b[0m\u001b[32m'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'font_caption'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mfloat\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[1;36m1\u001b[0m\u001b[1;36m.1\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Font size for caption text \u001b[0m\u001b[32m(\u001b[0m\u001b[32min em units\u001b[0m\u001b[32m)\u001b[0m\u001b[32m'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'font_bold_index'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mbool\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'If True, make index columns bold'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'pef_precision'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mint\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[1;36m3\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Precision for engineering format \u001b[0m\u001b[32m(\u001b[0m\u001b[32mdigits after decimal\u001b[0m\u001b[32m)\u001b[0m\u001b[32m'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'pef_lower'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mint\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[1;36m-3\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Lower threshold: apply engineering format if abs\u001b[0m\u001b[32m(\u001b[0m\u001b[32mx\u001b[0m\u001b[32m)\u001b[0m\u001b[32m \u001b[0m\u001b[32m<\u001b[0m\u001b[32m \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m10**pef_lower'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[39m \u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[39m \u001b[0m\u001b[32m'pef_upper'\u001b[0m\u001b[39m: \u001b[0m\u001b[1;35mFieldInfo\u001b[0m\u001b[1;39m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[39m \u001b[0m\u001b[33mannotation\u001b[0m\u001b[39m=\u001b[0m\u001b[35mint\u001b[0m\u001b[39m,\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[39m \u001b[0m\u001b[33mrequired\u001b[0m\u001b[39m=\u001b[0m\u001b[3;91mFalse\u001b[0m\u001b[39m,\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[39m \u001b[0m\u001b[33mdefault\u001b[0m\u001b[39m=\u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m,\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[39m \u001b[0m\u001b[33mdescription\u001b[0m\u001b[39m=\u001b[0m\u001b[32m'Upper threshold: apply engineering format if abs\u001b[0m\u001b[32m(\u001b[0m\u001b[32mx\u001b[0m\u001b[32m)\u001b[0m\u001b[32m \u001b[0m\u001b[32m>\u001b[0m\u001b[32m \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m10**pef_upper'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'cast_to_floats'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mbool\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[3;92mTrue\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'If True, cast non-integer, non-date columns to float where \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32mpossible'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'header_row'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mbool\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[3;92mTrue\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'If True, use the first row as header; False disables header \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32mrow'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'tabs'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mUnion\u001b[0m\u001b[1m[\u001b[0mlist\u001b[1m[\u001b[0mfloat\u001b[1m]\u001b[0m, float, int, NoneType\u001b[1m]\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[3;35mNone\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Column widths in characters or ems; None triggers \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32mauto-calculation'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'equal'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mbool\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'If True, force equal column widths \u001b[0m\u001b[32m(\u001b[0m\u001b[32mmay be ignored if \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32mconflicting\u001b[0m\u001b[32m)\u001b[0m\u001b[32m'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'caption_align'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mstr\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[32m'center'\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Alignment of the caption text'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'large_ok'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mbool\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'If True, allow full rendering of large tables without \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32mtruncation'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'max_str_length'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mint\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[1;36m-1\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Maximum length for stringified objects \u001b[0m\u001b[32m(\u001b[0m\u001b[32me.g. nested \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32mDataFrames\u001b[0m\u001b[32m)\u001b[0m\u001b[32m; -1 = unlimited'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'max_table_width'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mint\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[1;36m200\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Maximum table width for markdown/text output mode'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'table_width_mode'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mLiteral\u001b[0m\u001b[1m[\u001b[0m\u001b[32m'explicit'\u001b[0m, \u001b[32m'natural'\u001b[0m, \u001b[32m'breakable'\u001b[0m, \u001b[32m'minimum'\u001b[0m\u001b[1m]\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[32m'explicit'\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m\"Mode\u001b[0m\u001b[32m for determining table width. 'explicit': fixed width \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32musing max_table_width; 'natural': each cell fits its full content; 'breakable': \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32mwrap breakable strings; 'minimum': also wraps dates or float-like cells\"\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'table_width_header_adjust'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mfloat\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.1\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Proportion of width allocated to headers to balance content \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32mwidth'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'table_width_header_relax'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mfloat\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[1;36m10\u001b[0m\u001b[1;36m.0\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Extra characters allowed per column heading to help header \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32mwrapping'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32m'debug'\u001b[0m: \u001b[1;35mFieldInfo\u001b[0m\u001b[1m(\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mannotation\u001b[0m=\u001b[35mbool\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mrequired\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdefault\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[33mdescription\u001b[0m=\u001b[32m'Run in debug mode with more reporting, include internal ID in \u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[32mcaption and use colored output lines'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m)\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[1m}\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mmodel_fields_set\u001b[0m = \u001b[1;35mset\u001b[0m\u001b[1m(\u001b[0m\u001b[1m)\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mpadding_trbl\u001b[0m = \u001b[3;35mNone\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mpef_lower\u001b[0m = \u001b[1;36m-3\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mpef_precision\u001b[0m = \u001b[1;36m3\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mpef_upper\u001b[0m = \u001b[1;36m6\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mspacing\u001b[0m = \u001b[32m'medium'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33msparsify\u001b[0m = \u001b[3;92mTrue\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33msparsify_columns\u001b[0m = \u001b[3;92mTrue\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mtable_float_format\u001b[0m = \u001b[3;35mNone\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mtable_hrule_width\u001b[0m = \u001b[1;36m1\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mtable_vrule_width\u001b[0m = \u001b[1;36m1\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mtable_width_header_adjust\u001b[0m = \u001b[1;36m0.1\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mtable_width_header_relax\u001b[0m = \u001b[1;36m10.0\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mtable_width_mode\u001b[0m = \u001b[32m'explicit'\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mtabs\u001b[0m = \u001b[3;35mNone\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mtikz_scale\u001b[0m = \u001b[1;36m1.0\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[3;33mvrule_widths\u001b[0m = \u001b[3;35mNone\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 585 ms (started: 2025-06-12 09:17:06 +01:00)\n" + ] + } + ], + "source": [ + "inspect(config)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "5a274385-4791-4142-b1ec-e6637d344243", + "metadata": {}, + "outputs": [ + { + "ename": "ValueError", + "evalue": "dictionary update sequence element #0 has length 1; 2 is required", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[12]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43mconfig\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mlarge_ok\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32mC:\\S\\TELOS\\Python\\greater_tables_project\\greater_tables\\gtconfig.py:171\u001b[39m, in \u001b[36mGTConfig.get\u001b[39m\u001b[34m(self, overrides)\u001b[39m\n\u001b[32m 170\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mget\u001b[39m(\u001b[38;5;28mself\u001b[39m, overrides: \u001b[38;5;28mdict\u001b[39m = {}) -> GTConfigModel:\n\u001b[32m--> \u001b[39m\u001b[32m171\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmodel_copy\u001b[49m\u001b[43m(\u001b[49m\u001b[43mupdate\u001b[49m\u001b[43m=\u001b[49m\u001b[43moverrides\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\miniconda3\\envs\\working313\\Lib\\site-packages\\pydantic\\main.py:417\u001b[39m, in \u001b[36mBaseModel.model_copy\u001b[39m\u001b[34m(self, update, deep)\u001b[39m\n\u001b[32m 415\u001b[39m copied.__pydantic_extra__[k] = v\n\u001b[32m 416\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m417\u001b[39m \u001b[43mcopied\u001b[49m\u001b[43m.\u001b[49m\u001b[34;43m__dict__\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mupdate\u001b[49m\u001b[43m(\u001b[49m\u001b[43mupdate\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 418\u001b[39m copied.__pydantic_fields_set__.update(update.keys())\n\u001b[32m 419\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m copied\n", + "\u001b[31mValueError\u001b[39m: dictionary update sequence element #0 has length 1; 2 is required" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time: 340 ms (started: 2025-06-11 12:26:33 +01:00)\n" + ] + } + ], + "source": [ + "config.get('large_ok')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68b742ef-cd09-40cd-90fc-272cc762f267", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/greater_tables/test_tables.py b/tests/test_tables.py similarity index 100% rename from greater_tables/test_tables.py rename to tests/test_tables.py diff --git a/greater_tables/utilities.py b/tests/utilities.py similarity index 100% rename from greater_tables/utilities.py rename to tests/utilities.py