Skip to content

Commit 9b03dd4

Browse files
3w36zj6mroeschke
andauthored
ENH: Add Styler.to_typst() (#60733)
* ENH: Add `to_typst` method to `Styler` * TST: Add `Styler.to_typst()` test cases * STY: Apply Ruff suggestions * DOC: Update What's new * DOC: Update reference * CI: Add `Styler.template_typst` to validation ignore list * DOC: Update docstring format for `Styler.to_typst()` example * DOC: Update versionadded for `Styler.to_typst()` to 3.0.0 in documentation --------- Co-authored-by: Matthew Roeschke <[email protected]>
1 parent ea7ff0e commit 9b03dd4

File tree

7 files changed

+233
-0
lines changed

7 files changed

+233
-0
lines changed

Diff for: doc/source/reference/style.rst

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Styler properties
2727
Styler.template_html_style
2828
Styler.template_html_table
2929
Styler.template_latex
30+
Styler.template_typst
3031
Styler.template_string
3132
Styler.loader
3233

@@ -77,6 +78,7 @@ Style export and import
7778

7879
Styler.to_html
7980
Styler.to_latex
81+
Styler.to_typst
8082
Styler.to_excel
8183
Styler.to_string
8284
Styler.export

Diff for: doc/source/whatsnew/v3.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Other enhancements
3131
- :class:`pandas.api.typing.FrozenList` is available for typing the outputs of :attr:`MultiIndex.names`, :attr:`MultiIndex.codes` and :attr:`MultiIndex.levels` (:issue:`58237`)
3232
- :class:`pandas.api.typing.SASReader` is available for typing the output of :func:`read_sas` (:issue:`55689`)
3333
- :meth:`pandas.api.interchange.from_dataframe` now uses the `PyCapsule Interface <https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html>`_ if available, only falling back to the Dataframe Interchange Protocol if that fails (:issue:`60739`)
34+
- Added :meth:`.Styler.to_typst` to write Styler objects to file, buffer or string in Typst format (:issue:`57617`)
3435
- :class:`pandas.api.typing.NoDefault` is available for typing ``no_default``
3536
- :func:`DataFrame.to_excel` now raises an ``UserWarning`` when the character count in a cell exceeds Excel's limitation of 32767 characters (:issue:`56954`)
3637
- :func:`pandas.merge` now validates the ``how`` parameter input (merge type) (:issue:`59435`)

Diff for: pandas/io/formats/style.py

+105
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,111 @@ def to_latex(
12281228
)
12291229
return save_to_buffer(latex, buf=buf, encoding=encoding)
12301230

1231+
@overload
1232+
def to_typst(
1233+
self,
1234+
buf: FilePath | WriteBuffer[str],
1235+
*,
1236+
encoding: str | None = ...,
1237+
sparse_index: bool | None = ...,
1238+
sparse_columns: bool | None = ...,
1239+
max_rows: int | None = ...,
1240+
max_columns: int | None = ...,
1241+
) -> None: ...
1242+
1243+
@overload
1244+
def to_typst(
1245+
self,
1246+
buf: None = ...,
1247+
*,
1248+
encoding: str | None = ...,
1249+
sparse_index: bool | None = ...,
1250+
sparse_columns: bool | None = ...,
1251+
max_rows: int | None = ...,
1252+
max_columns: int | None = ...,
1253+
) -> str: ...
1254+
1255+
@Substitution(buf=buffering_args, encoding=encoding_args)
1256+
def to_typst(
1257+
self,
1258+
buf: FilePath | WriteBuffer[str] | None = None,
1259+
*,
1260+
encoding: str | None = None,
1261+
sparse_index: bool | None = None,
1262+
sparse_columns: bool | None = None,
1263+
max_rows: int | None = None,
1264+
max_columns: int | None = None,
1265+
) -> str | None:
1266+
"""
1267+
Write Styler to a file, buffer or string in Typst format.
1268+
1269+
.. versionadded:: 3.0.0
1270+
1271+
Parameters
1272+
----------
1273+
%(buf)s
1274+
%(encoding)s
1275+
sparse_index : bool, optional
1276+
Whether to sparsify the display of a hierarchical index. Setting to False
1277+
will display each explicit level element in a hierarchical key for each row.
1278+
Defaults to ``pandas.options.styler.sparse.index`` value.
1279+
sparse_columns : bool, optional
1280+
Whether to sparsify the display of a hierarchical index. Setting to False
1281+
will display each explicit level element in a hierarchical key for each
1282+
column. Defaults to ``pandas.options.styler.sparse.columns`` value.
1283+
max_rows : int, optional
1284+
The maximum number of rows that will be rendered. Defaults to
1285+
``pandas.options.styler.render.max_rows``, which is None.
1286+
max_columns : int, optional
1287+
The maximum number of columns that will be rendered. Defaults to
1288+
``pandas.options.styler.render.max_columns``, which is None.
1289+
1290+
Rows and columns may be reduced if the number of total elements is
1291+
large. This value is set to ``pandas.options.styler.render.max_elements``,
1292+
which is 262144 (18 bit browser rendering).
1293+
1294+
Returns
1295+
-------
1296+
str or None
1297+
If `buf` is None, returns the result as a string. Otherwise returns `None`.
1298+
1299+
See Also
1300+
--------
1301+
DataFrame.to_typst : Write a DataFrame to a file,
1302+
buffer or string in Typst format.
1303+
1304+
Examples
1305+
--------
1306+
>>> df = pd.DataFrame({"A": [1, 2], "B": [3, 4]})
1307+
>>> df.style.to_typst() # doctest: +SKIP
1308+
1309+
.. code-block:: typst
1310+
1311+
#table(
1312+
columns: 3,
1313+
[], [A], [B],
1314+
1315+
[0], [1], [3],
1316+
[1], [2], [4],
1317+
)
1318+
"""
1319+
obj = self._copy(deepcopy=True)
1320+
1321+
if sparse_index is None:
1322+
sparse_index = get_option("styler.sparse.index")
1323+
if sparse_columns is None:
1324+
sparse_columns = get_option("styler.sparse.columns")
1325+
1326+
text = obj._render_typst(
1327+
sparse_columns=sparse_columns,
1328+
sparse_index=sparse_index,
1329+
max_rows=max_rows,
1330+
max_cols=max_columns,
1331+
)
1332+
return save_to_buffer(
1333+
text, buf=buf, encoding=(encoding if buf is not None else None)
1334+
)
1335+
12311336
@overload
12321337
def to_html(
12331338
self,

Diff for: pandas/io/formats/style_render.py

+16
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class StylerRenderer:
7777
template_html_table = env.get_template("html_table.tpl")
7878
template_html_style = env.get_template("html_style.tpl")
7979
template_latex = env.get_template("latex.tpl")
80+
template_typst = env.get_template("typst.tpl")
8081
template_string = env.get_template("string.tpl")
8182

8283
def __init__(
@@ -232,6 +233,21 @@ def _render_latex(
232233
d.update(kwargs)
233234
return self.template_latex.render(**d)
234235

236+
def _render_typst(
237+
self,
238+
sparse_index: bool,
239+
sparse_columns: bool,
240+
max_rows: int | None = None,
241+
max_cols: int | None = None,
242+
**kwargs,
243+
) -> str:
244+
"""
245+
Render a Styler in typst format
246+
"""
247+
d = self._render(sparse_index, sparse_columns, max_rows, max_cols)
248+
d.update(kwargs)
249+
return self.template_typst.render(**d)
250+
235251
def _render_string(
236252
self,
237253
sparse_index: bool,

Diff for: pandas/io/formats/templates/typst.tpl

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#table(
2+
columns: {{ head[0] | length }},
3+
{% for r in head %}
4+
{% for c in r %}[{% if c["is_visible"] %}{{ c["display_value"] }}{% endif %}],{% if not loop.last %} {% endif%}{% endfor %}
5+
6+
{% endfor %}
7+
8+
{% for r in body %}
9+
{% for c in r %}[{% if c["is_visible"] %}{{ c["display_value"] }}{% endif %}],{% if not loop.last %} {% endif%}{% endfor %}
10+
11+
{% endfor %}
12+
)

Diff for: pandas/tests/io/formats/style/test_to_typst.py

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from textwrap import dedent
2+
3+
import pytest
4+
5+
from pandas import (
6+
DataFrame,
7+
Series,
8+
)
9+
10+
pytest.importorskip("jinja2")
11+
from pandas.io.formats.style import Styler
12+
13+
14+
@pytest.fixture
15+
def df():
16+
return DataFrame(
17+
{"A": [0, 1], "B": [-0.61, -1.22], "C": Series(["ab", "cd"], dtype=object)}
18+
)
19+
20+
21+
@pytest.fixture
22+
def styler(df):
23+
return Styler(df, uuid_len=0, precision=2)
24+
25+
26+
def test_basic_table(styler):
27+
result = styler.to_typst()
28+
expected = dedent(
29+
"""\
30+
#table(
31+
columns: 4,
32+
[], [A], [B], [C],
33+
34+
[0], [0], [-0.61], [ab],
35+
[1], [1], [-1.22], [cd],
36+
)"""
37+
)
38+
assert result == expected
39+
40+
41+
def test_concat(styler):
42+
result = styler.concat(styler.data.agg(["sum"]).style).to_typst()
43+
expected = dedent(
44+
"""\
45+
#table(
46+
columns: 4,
47+
[], [A], [B], [C],
48+
49+
[0], [0], [-0.61], [ab],
50+
[1], [1], [-1.22], [cd],
51+
[sum], [1], [-1.830000], [abcd],
52+
)"""
53+
)
54+
assert result == expected
55+
56+
57+
def test_concat_recursion(styler):
58+
df = styler.data
59+
styler1 = styler
60+
styler2 = Styler(df.agg(["sum"]), uuid_len=0, precision=3)
61+
styler3 = Styler(df.agg(["sum"]), uuid_len=0, precision=4)
62+
result = styler1.concat(styler2.concat(styler3)).to_typst()
63+
expected = dedent(
64+
"""\
65+
#table(
66+
columns: 4,
67+
[], [A], [B], [C],
68+
69+
[0], [0], [-0.61], [ab],
70+
[1], [1], [-1.22], [cd],
71+
[sum], [1], [-1.830], [abcd],
72+
[sum], [1], [-1.8300], [abcd],
73+
)"""
74+
)
75+
assert result == expected
76+
77+
78+
def test_concat_chain(styler):
79+
df = styler.data
80+
styler1 = styler
81+
styler2 = Styler(df.agg(["sum"]), uuid_len=0, precision=3)
82+
styler3 = Styler(df.agg(["sum"]), uuid_len=0, precision=4)
83+
result = styler1.concat(styler2).concat(styler3).to_typst()
84+
expected = dedent(
85+
"""\
86+
#table(
87+
columns: 4,
88+
[], [A], [B], [C],
89+
90+
[0], [0], [-0.61], [ab],
91+
[1], [1], [-1.22], [cd],
92+
[sum], [1], [-1.830], [abcd],
93+
[sum], [1], [-1.8300], [abcd],
94+
)"""
95+
)
96+
assert result == expected

Diff for: scripts/validate_docstrings.py

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"Styler.template_html_style",
4646
"Styler.template_html_table",
4747
"Styler.template_latex",
48+
"Styler.template_typst",
4849
"Styler.template_string",
4950
"Styler.loader",
5051
"errors.InvalidComparison",

0 commit comments

Comments
 (0)