5.4: allow namedtuple as argument.

This commit is contained in:
Stephen Mildenhall
2025-10-09 19:57:42 +01:00
parent 4f49d8adc5
commit 1e4396d60e
4 changed files with 68 additions and 116 deletions
+8
View File
@@ -11,6 +11,14 @@ Versions and Change Log
.. TODO
* self.padl and r / 12 in make html width adj s/b elsewhere
5.4
----
* Can now pass a namedtuple as df; it is converted to a dataframe.
5.3
----
* Passing a callable to formatters applies that function to **all** columns.
5.2.1
-----
* Added ``SmartTitle`` class in utilites - eventually add capitalize option for index?
+1 -1
View File
@@ -1,6 +1,6 @@
__project__ = 'greater_tables'
__author__ = 'Stephen J Mildenhall'
__version__ = '5.2.1'
__version__ = '5.4'
from . core import GT
from . fabrications import *
+42 -14
View File
@@ -7,6 +7,7 @@ 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
@@ -62,6 +63,7 @@ class GT(object):
**Input transformation**
* ``namedtuple`` converted to ``DataFrame``
* ``pd.Series`` converted to ``DataFrame``
* ``list`` converted to ``DataFrame``, optionally using row 0 as
``config.header_row``
@@ -124,7 +126,7 @@ class GT(object):
**overrides,
):
"""
Available keyword ``**overrides`:
Parameters:
:param df: target DataFrame or list of lists or markdown table string
:param caption: table caption, optional (GT will look for gt_caption
@@ -133,8 +135,13 @@ class GT(object):
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 formatters: None or dict (type or colname) or callable ->
format function for the column; formatters trump ratio_cols; if
callable passed, it is applied to **all columns**.
:param 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 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.
@@ -146,6 +153,9 @@ class GT(object):
: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
Available keyword *overrides:
: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
@@ -188,10 +198,6 @@ class GT(object):
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
@@ -224,6 +230,7 @@ class GT(object):
if config:
base_config = config
elif config_path:
config_path = Path(config_path)
try:
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
base_config = Configurator.model_validate(raw)
@@ -266,6 +273,8 @@ class GT(object):
else:
df, aligners, caption, label = MD2DF.md_to_df(df)
show_index = False
elif GT._is_namedtuple_instance(df):
df = GT._ntdf(df)
else:
raise ValueError(
'df must be a DataFrame, a list of lists, or a markdown table string')
@@ -492,16 +501,20 @@ class GT(object):
self.df_idx_aligners = self.df_aligners[:self.nindex]
self.default_formatters = {}
if formatters is None:
self.default_formatters = {}
pass
elif callable(formatters):
# apply to all columns
for k in self.df.columns:
self.default_formatters[k] = formatters
else:
self.default_formatters = {}
for k, v in formatters.items():
if callable(v):
self.default_formatters[k] = v
elif type(v) == str:
elif isinstance(v, str):
self.default_formatters[k] = lambda x: v.format(x=x)
elif type(v) == int:
elif isinstance(v, int):
fmt = f'{{x:.{v}f}}'
self.default_formatters[k] = lambda x: fmt.format(x=x)
else:
@@ -526,7 +539,7 @@ class GT(object):
self.tabs = None
if self.config.padding_trbl is not None:
padding_trbl = self.config_padding_trbl
padding_trbl = self.config.padding_trbl
elif self.config.padding_trbl is None:
if self.config.spacing == 'tight':
padding_trbl = (0, 5, 0, 5)
@@ -1020,12 +1033,16 @@ class GT(object):
if mode == 'text':
df = self.df
len_function = len
# no bold in text mode
bold_adjustment = 1.0
elif mode == 'html':
df = self.df_html
len_function = TextLength.text_display_len
bold_adjustment = 1.1
else: # mode == 'tex':
df = self.df_tex
len_function = TextLength.text_display_len
bold_adjustment = 1.1
n_row, n_col = df.shape
@@ -1055,8 +1072,8 @@ class GT(object):
)
# ensure is a tuple
ctuple = col_name if isinstance(col_name, tuple) else (col_name, )
header_natural[col_name] = max(map(len_function, ctuple))
header_minimum[col_name] = min(len_function(part) for i in ctuple for part in re.split(pat, str(i)))
header_natural[col_name] = bold_adjustment * max(map(len_function, ctuple))
header_minimum[col_name] = bold_adjustment * min(len_function(part) for i in ctuple for part in re.split(pat, str(i)))
# begin to assemble the parts
# ans will be the col_width_df; break_penalties needed by all methods
@@ -2028,3 +2045,14 @@ class GT(object):
display(f.width_report())
print(f.make_tikz())
return f
@staticmethod
def _is_namedtuple_instance(x) -> bool:
"""Heuristic: namedtuple instances are tuples whose class defines _fields."""
return isinstance(x, tuple) and isinstance(getattr(type(x), "_fields", None), tuple)
@staticmethod
def _ntdf(t):
"""Convert named tuple to pandas dataframe to display."""
return pd.Series(t, index=pd.Index(t._fields, name="Item")).to_frame('Value')
+17 -101
View File
@@ -192,7 +192,7 @@ class Escaping:
class TextLength:
"""Estimate length of displayed text."""
"""Estimate length in em of displayed text."""
# TeX control sequence display widths (heuristic)
TEX_SIMPLE_GLYPHS = {
'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta',
@@ -203,109 +203,25 @@ class TextLength:
}
TEX_WIDE = {'frac', 'sqrt', 'sum', 'int', 'prod'}
TEX_SPACING = {'quad', 'qquad', ',', ';', ' ', '!'}
_WIDTH_TABLE = {"a": 0.444, "b": 0.5, "c": 0.444, "d": 0.5, "e": 0.444, "f": 0.333,
"g": 0.5, "h": 0.5, "i": 0.278, "j": 0.278, "k": 0.5, "l": 0.278, "m": 0.778,
"n": 0.5, "o": 0.5, "p": 0.5, "q": 0.5, "r": 0.333, "s": 0.389, "t": 0.278,
"u": 0.5, "v": 0.5, "w": 0.722, "x": 0.5, "y": 0.5, "z": 0.444, "A": 0.722,
"B": 0.667, "C": 0.667, "D": 0.722, "E": 0.611, "F": 0.556, "G": 0.722, "H": 0.722,
"I": 0.333, "J": 0.389, "K": 0.722, "L": 0.611, "M": 0.889, "N": 0.722, "O": 0.722,
"P": 0.556, "Q": 0.722, "R": 0.667, "S": 0.556, "T": 0.611, "U": 0.722, "V": 0.722,
"W": 0.944, "X": 0.722, "Y": 0.722, "Z": 0.611, "0": 0.5, "1": 0.5, "2": 0.5,
"3": 0.5, "4": 0.5, "5": 0.5, "6": 0.5, "7": 0.5, "8": 0.5, "9": 0.5,
".": 0.25, ",": 0.25, ":": 0.278, ";": 0.278, "(": 0.333, ")": 0.333, "[": 0.333,
"]": 0.333, "": 0.333, '"': 0.444, "!": 0.333, "?": 0.444, " ": 0.25, "|": 0.2,
"": 0.333, "{": 0.48, "}": 0.48, "-": 0.5, # 0.333,
}
_DEFAULT_WIDTH = 0.6
@staticmethod
def approximate_char_width_em(c: str) -> float:
width_table = {
"il.':|!`": 0.3,
"frtJ(){}[]*": 0.5,
"abcdeghknopqsuvxyz": 0.6,
"LCDEFHISTUZ": 0.7,
"ABGKNOPQRXYV": 0.8,
"mwMW": 0.9,
"0123456789": 0.6,
"-_=+<>": 0.5,
"#$%^&@~": 0.6,
",;": 0.25,
'"': 0.4,
"/\\": 0.5,
"?": 0.6,
" ": 0.4,
}
width_table = {
"a": 0.444,
"b": 0.5,
"c": 0.444,
"d": 0.5,
"e": 0.444,
"f": 0.333,
"g": 0.5,
"h": 0.5,
"i": 0.278,
"j": 0.278,
"k": 0.5,
"l": 0.278,
"m": 0.778,
"n": 0.5,
"o": 0.5,
"p": 0.5,
"q": 0.5,
"r": 0.333,
"s": 0.389,
"t": 0.278,
"u": 0.5,
"v": 0.5,
"w": 0.722,
"x": 0.5,
"y": 0.5,
"z": 0.444,
"A": 0.722,
"B": 0.667,
"C": 0.667,
"D": 0.722,
"E": 0.611,
"F": 0.556,
"G": 0.722,
"H": 0.722,
"I": 0.333,
"J": 0.389,
"K": 0.722,
"L": 0.611,
"M": 0.889,
"N": 0.722,
"O": 0.722,
"P": 0.556,
"Q": 0.722,
"R": 0.667,
"S": 0.556,
"T": 0.611,
"U": 0.722,
"V": 0.722,
"W": 0.944,
"X": 0.722,
"Y": 0.722,
"Z": 0.611,
"0": 0.5,
"1": 0.5,
"2": 0.5,
"3": 0.5,
"4": 0.5,
"5": 0.5,
"6": 0.5,
"7": 0.5,
"8": 0.5,
"9": 0.5,
".": 0.25,
",": 0.25,
":": 0.278,
";": 0.278,
"(": 0.333,
")": 0.333,
"[": 0.333,
"]": 0.333,
"": 0.333,
'"': 0.444,
"!": 0.333,
"?": 0.444,
" ": 0.25,
"|": 0.2,
"": 0.333,
"{": 0.48,
"}": 0.48,
"-": 0.5, # 0.333,
}
char_width = {c: w for chars, w in width_table.items() for c in chars}
return char_width.get(c, 0.6)
"""Estimate character width in em."""
return TextLength._WIDTH_TABLE.get(c, TextLength._DEFAULT_WIDTH)
@staticmethod
def text_display_len(s: str) -> float: