pl2html 0.2.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pl2html-0.2.0 → pl2html-0.4.0}/PKG-INFO +13 -6
- {pl2html-0.2.0 → pl2html-0.4.0}/README.md +12 -5
- {pl2html-0.2.0 → pl2html-0.4.0}/pl2html/__init__.py +11 -5
- {pl2html-0.2.0 → pl2html-0.4.0}/pl2html/formats.py +95 -38
- {pl2html-0.2.0 → pl2html-0.4.0}/pyproject.toml +1 -1
- {pl2html-0.2.0 → pl2html-0.4.0}/pl2html/styles.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pl2html
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Dist: polars>=1.42.1
|
|
6
6
|
Requires-Python: >=3.14
|
|
@@ -51,10 +51,8 @@ df = pl.DataFrame(
|
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
# Compiles safely to a Polars LazyFrame containing the HTML output
|
|
54
|
-
|
|
54
|
+
html_table_string = to_html(df)
|
|
55
55
|
|
|
56
|
-
# Collect and extract the compiled string
|
|
57
|
-
html_table_string = html_lazy.collect().item()
|
|
58
56
|
print(html_table_string)
|
|
59
57
|
```
|
|
60
58
|
|
|
@@ -101,7 +99,7 @@ df_f = df.with_columns(
|
|
|
101
99
|
fmt.sub_zero("status_code", zero_text="OK")
|
|
102
100
|
)
|
|
103
101
|
|
|
104
|
-
html = to_html(df_f)
|
|
102
|
+
html = to_html(df_f)
|
|
105
103
|
```
|
|
106
104
|
|
|
107
105
|
### 3. Advanced Styling via Native Attributes (`attrs`)
|
|
@@ -141,7 +139,7 @@ styles.update(
|
|
|
141
139
|
)
|
|
142
140
|
)
|
|
143
141
|
|
|
144
|
-
html_table = to_html(df, attrs=styles)
|
|
142
|
+
html_table = to_html(df, attrs=styles)
|
|
145
143
|
```
|
|
146
144
|
|
|
147
145
|
Output (github may override table colors, but they are there):
|
|
@@ -205,3 +203,12 @@ Generates structural Polars expression payloads mapped to specific HTML style at
|
|
|
205
203
|
.str.replace_all('"', '"')
|
|
206
204
|
.str.replace_all("'", ''')
|
|
207
205
|
```
|
|
206
|
+
|
|
207
|
+
### Design Inspiration & Why This Library Exists
|
|
208
|
+
|
|
209
|
+
Most of the API design and formatting parameters in this library are heavily inspired by [great-tables](https://posit-dev.github.io/great-tables/reference/). If you are looking for detailed descriptions or conceptual overviews of specific formatting arguments, the `great-tables` reference documentation serves as an excellent companion guide.
|
|
210
|
+
|
|
211
|
+
However, this library was born out of distinct architectural and environment constraints:
|
|
212
|
+
|
|
213
|
+
* **Zero Overhead Styling:** While `great-tables` is incredibly feature-rich, it often produces dense HTML styles and boilerplate structures that were unnecessary for the target use cases. This library optimizes for minimal, highly clean HTML output.
|
|
214
|
+
* **Modern Python Compatibility (Py 3.14+):** `great-tables` pulls in heavy external dependencies, including binary wheels like `multimark`. At the time of development, these dependencies did not support Python 3.14 (see [multimark #3](https://github.com/posit-dev/multimark/issues/3)). To unblock workflows and maintain an ultra-lightweight, future-proof stack with zero complex binary dependencies, this native, Rust-backed Polars formatter was built from the ground up.
|
|
@@ -42,10 +42,8 @@ df = pl.DataFrame(
|
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
# Compiles safely to a Polars LazyFrame containing the HTML output
|
|
45
|
-
|
|
45
|
+
html_table_string = to_html(df)
|
|
46
46
|
|
|
47
|
-
# Collect and extract the compiled string
|
|
48
|
-
html_table_string = html_lazy.collect().item()
|
|
49
47
|
print(html_table_string)
|
|
50
48
|
```
|
|
51
49
|
|
|
@@ -92,7 +90,7 @@ df_f = df.with_columns(
|
|
|
92
90
|
fmt.sub_zero("status_code", zero_text="OK")
|
|
93
91
|
)
|
|
94
92
|
|
|
95
|
-
html = to_html(df_f)
|
|
93
|
+
html = to_html(df_f)
|
|
96
94
|
```
|
|
97
95
|
|
|
98
96
|
### 3. Advanced Styling via Native Attributes (`attrs`)
|
|
@@ -132,7 +130,7 @@ styles.update(
|
|
|
132
130
|
)
|
|
133
131
|
)
|
|
134
132
|
|
|
135
|
-
html_table = to_html(df, attrs=styles)
|
|
133
|
+
html_table = to_html(df, attrs=styles)
|
|
136
134
|
```
|
|
137
135
|
|
|
138
136
|
Output (github may override table colors, but they are there):
|
|
@@ -196,3 +194,12 @@ Generates structural Polars expression payloads mapped to specific HTML style at
|
|
|
196
194
|
.str.replace_all('"', '"')
|
|
197
195
|
.str.replace_all("'", ''')
|
|
198
196
|
```
|
|
197
|
+
|
|
198
|
+
### Design Inspiration & Why This Library Exists
|
|
199
|
+
|
|
200
|
+
Most of the API design and formatting parameters in this library are heavily inspired by [great-tables](https://posit-dev.github.io/great-tables/reference/). If you are looking for detailed descriptions or conceptual overviews of specific formatting arguments, the `great-tables` reference documentation serves as an excellent companion guide.
|
|
201
|
+
|
|
202
|
+
However, this library was born out of distinct architectural and environment constraints:
|
|
203
|
+
|
|
204
|
+
* **Zero Overhead Styling:** While `great-tables` is incredibly feature-rich, it often produces dense HTML styles and boilerplate structures that were unnecessary for the target use cases. This library optimizes for minimal, highly clean HTML output.
|
|
205
|
+
* **Modern Python Compatibility (Py 3.14+):** `great-tables` pulls in heavy external dependencies, including binary wheels like `multimark`. At the time of development, these dependencies did not support Python 3.14 (see [multimark #3](https://github.com/posit-dev/multimark/issues/3)). To unblock workflows and maintain an ultra-lightweight, future-proof stack with zero complex binary dependencies, this native, Rust-backed Polars formatter was built from the ground up.
|
|
@@ -10,7 +10,7 @@ from polars import (
|
|
|
10
10
|
when as _when,
|
|
11
11
|
)
|
|
12
12
|
|
|
13
|
-
__version__ = '0.
|
|
13
|
+
__version__ = '0.4.0'
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def _escape_polars_string(col_name: str) -> _Expr:
|
|
@@ -126,7 +126,7 @@ def to_html(
|
|
|
126
126
|
*,
|
|
127
127
|
attrs: dict[str, dict[str, _Expr]] | None = None,
|
|
128
128
|
exclude_columns: list[str] | None = None,
|
|
129
|
-
) ->
|
|
129
|
+
) -> str:
|
|
130
130
|
"""
|
|
131
131
|
Compiles a Polars DataFrame safely into an HTML string layout.
|
|
132
132
|
Accepts structural custom attributes mappings to handle layout modifications natively.
|
|
@@ -151,8 +151,14 @@ def to_html(
|
|
|
151
151
|
# 4. Generate wrappers and frame the query graph
|
|
152
152
|
html_header, html_footer = _build_html_skeleton(visible_columns)
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
# 5. Execute the query graph and pull out the raw python string directly
|
|
155
|
+
return (
|
|
156
|
+
lf.select(row_expr.alias('html_row'))
|
|
157
|
+
.select(
|
|
158
|
+
(
|
|
159
|
+
html_header + _col('html_row').str.join('\n') + html_footer
|
|
160
|
+
).alias('html_table')
|
|
157
161
|
)
|
|
162
|
+
.collect()
|
|
163
|
+
.item()
|
|
158
164
|
)
|
|
@@ -17,12 +17,10 @@ from polars import (
|
|
|
17
17
|
# --- 0. Decorator to cleanly support single or multiple columns ---
|
|
18
18
|
def _multicolumn(func):
|
|
19
19
|
@_wraps(func)
|
|
20
|
-
def wrapper(columns: str | _Iterable[str],
|
|
20
|
+
def wrapper(*, columns: str | _Iterable[str], **kwargs):
|
|
21
21
|
if isinstance(columns, str):
|
|
22
|
-
return func(columns,
|
|
23
|
-
|
|
24
|
-
# so Polars knows exactly which column to overwrite/create.
|
|
25
|
-
return [func(col, *args, **kwargs).alias(col) for col in columns]
|
|
22
|
+
return func(columns=columns, **kwargs)
|
|
23
|
+
return [func(columns=col, **kwargs).alias(col) for col in columns]
|
|
26
24
|
|
|
27
25
|
return wrapper
|
|
28
26
|
|
|
@@ -34,6 +32,7 @@ _Columns = str | list[str]
|
|
|
34
32
|
|
|
35
33
|
@_multicolumn
|
|
36
34
|
def fmt_number(
|
|
35
|
+
*,
|
|
37
36
|
columns: _Columns,
|
|
38
37
|
decimals: int = 2,
|
|
39
38
|
scale_by: float = 1.0,
|
|
@@ -42,6 +41,8 @@ def fmt_number(
|
|
|
42
41
|
use_seps: bool = True,
|
|
43
42
|
accounting: bool = False,
|
|
44
43
|
pattern: str = '{x}',
|
|
44
|
+
n_sigfig: int | None = None,
|
|
45
|
+
force_sign: bool = False,
|
|
45
46
|
) -> _Expr:
|
|
46
47
|
"""
|
|
47
48
|
Highly optimized, native Polars numeric formatter matching great_tables features.
|
|
@@ -90,38 +91,71 @@ def fmt_number(
|
|
|
90
91
|
val_scaled = val
|
|
91
92
|
suffix_chain = _lit('')
|
|
92
93
|
|
|
93
|
-
# 2.
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
# 2. Dynamic Precision Handling (Significant Figures vs Fixed Decimals)
|
|
95
|
+
if n_sigfig is not None:
|
|
96
|
+
if n_sigfig < 1:
|
|
97
|
+
raise ValueError('n_sigfig must be a positive integer >= 1')
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
frac_part = (
|
|
101
|
-
_when(full_str.str.contains(r'\.'))
|
|
102
|
-
.then(full_str.str.split('.').list.get(1).str.slice(0, decimals))
|
|
103
|
-
.otherwise(_lit(''))
|
|
104
|
-
).str.pad_end(decimals, fill_char='0')
|
|
105
|
-
|
|
106
|
-
if use_seps:
|
|
107
|
-
int_part = (
|
|
108
|
-
int_part.str.reverse()
|
|
109
|
-
.str.replace_all(r'(\d{3})', r'${1},')
|
|
110
|
-
.str.strip_suffix(',')
|
|
111
|
-
.str.reverse()
|
|
112
|
-
)
|
|
99
|
+
# Round natively using Polars' built-in significant figures feature
|
|
100
|
+
rounded = val_scaled.round_sig_figs(n_sigfig)
|
|
101
|
+
abs_rounded = rounded.abs()
|
|
113
102
|
|
|
114
|
-
|
|
103
|
+
# Determine the number of dynamic decimal places needed for padding per row
|
|
104
|
+
log10_expr = (
|
|
105
|
+
_when(abs_rounded > 0)
|
|
106
|
+
.then(abs_rounded.log10())
|
|
107
|
+
.otherwise(_lit(0.0))
|
|
108
|
+
)
|
|
109
|
+
# decimals required = n_sigfig - 1 - floor(log10(x))
|
|
110
|
+
dynamic_decimals = (_lit(n_sigfig - 1) - log10_expr.floor()).cast(
|
|
111
|
+
_Int32
|
|
112
|
+
)
|
|
113
|
+
# Ensure we don't try to pad negative decimal places for large numbers
|
|
114
|
+
dynamic_decimals = (
|
|
115
|
+
_when(dynamic_decimals < 0)
|
|
116
|
+
.then(_lit(0))
|
|
117
|
+
.otherwise(dynamic_decimals)
|
|
118
|
+
)
|
|
115
119
|
else:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
# Fallback to standard fixed decimal logic
|
|
121
|
+
epsilon = (
|
|
122
|
+
_when(val_scaled >= 0).then(_lit(1e-9)).otherwise(_lit(-1e-9))
|
|
123
|
+
)
|
|
124
|
+
rounded = (val_scaled + epsilon).round(decimals)
|
|
125
|
+
dynamic_decimals = _lit(decimals)
|
|
126
|
+
|
|
127
|
+
# 3. Component Extraction & Alignment
|
|
128
|
+
int_part = rounded.cast(_Int64).abs().cast(_String)
|
|
129
|
+
full_str = rounded.abs().cast(_String)
|
|
130
|
+
|
|
131
|
+
raw_frac = (
|
|
132
|
+
_when(full_str.str.contains(r'\.'))
|
|
133
|
+
.then(full_str.str.split('.').list.get(1))
|
|
134
|
+
.otherwise(_lit(''))
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Use native Python string multiplication inside _lit()
|
|
138
|
+
pad_len = n_sigfig if n_sigfig is not None else decimals
|
|
139
|
+
frac_part = (
|
|
140
|
+
_when(dynamic_decimals > 0)
|
|
141
|
+
.then((raw_frac + _lit('0' * pad_len)).str.slice(0, dynamic_decimals))
|
|
142
|
+
.otherwise(_lit(''))
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# 4. Constructing Base Number String
|
|
146
|
+
if use_seps:
|
|
147
|
+
int_part = (
|
|
148
|
+
int_part.str.reverse()
|
|
149
|
+
.str.replace_all(r'(\d{3})', r'${1},')
|
|
150
|
+
.str.strip_suffix(',')
|
|
151
|
+
.str.reverse()
|
|
152
|
+
)
|
|
124
153
|
|
|
154
|
+
base_num_str = (
|
|
155
|
+
_when(dynamic_decimals > 0)
|
|
156
|
+
.then(int_part + _lit('.') + frac_part)
|
|
157
|
+
.otherwise(int_part)
|
|
158
|
+
)
|
|
125
159
|
base_num_str = base_num_str + suffix_chain
|
|
126
160
|
|
|
127
161
|
prefix, suffix = '', ''
|
|
@@ -140,13 +174,22 @@ def fmt_number(
|
|
|
140
174
|
+ _lit(suffix)
|
|
141
175
|
+ _lit(')')
|
|
142
176
|
)
|
|
143
|
-
.otherwise(
|
|
177
|
+
.otherwise(
|
|
178
|
+
# If force_sign is True, explicitly prepend '+' to positive values
|
|
179
|
+
_lit('+' if force_sign else '')
|
|
180
|
+
+ _lit(prefix)
|
|
181
|
+
+ base_num_str
|
|
182
|
+
+ _lit(suffix)
|
|
183
|
+
)
|
|
144
184
|
)
|
|
145
185
|
else:
|
|
146
186
|
formatted_expr = (
|
|
147
187
|
_when(val < 0)
|
|
148
188
|
.then(_lit('-') + _lit(prefix) + base_num_str + _lit(suffix))
|
|
149
|
-
|
|
189
|
+
# Handle zero explicitly if you don't want a sign on exactly 0.0
|
|
190
|
+
.when((val == 0) | (not force_sign))
|
|
191
|
+
.then(_lit(prefix) + base_num_str + _lit(suffix))
|
|
192
|
+
.otherwise(_lit('+') + _lit(prefix) + base_num_str + _lit(suffix))
|
|
150
193
|
)
|
|
151
194
|
|
|
152
195
|
return formatted_expr
|
|
@@ -154,7 +197,11 @@ def fmt_number(
|
|
|
154
197
|
|
|
155
198
|
@_multicolumn
|
|
156
199
|
def fmt_percent(
|
|
157
|
-
|
|
200
|
+
*,
|
|
201
|
+
columns: _Columns,
|
|
202
|
+
decimals: int = 2,
|
|
203
|
+
use_seps: bool = True,
|
|
204
|
+
force_sign: bool = False,
|
|
158
205
|
) -> _Expr:
|
|
159
206
|
"""Formats columns as percentages, multiplying by 100 automatically."""
|
|
160
207
|
return fmt_number(
|
|
@@ -163,16 +210,19 @@ def fmt_percent(
|
|
|
163
210
|
scale_by=100.0,
|
|
164
211
|
use_seps=use_seps,
|
|
165
212
|
pattern='{x}%',
|
|
213
|
+
force_sign=force_sign,
|
|
166
214
|
)
|
|
167
215
|
|
|
168
216
|
|
|
169
217
|
@_multicolumn
|
|
170
218
|
def fmt_currency(
|
|
219
|
+
*,
|
|
171
220
|
columns: _Columns,
|
|
172
221
|
symbol: str = '$',
|
|
173
222
|
decimals: int = 2,
|
|
174
223
|
use_seps: bool = True,
|
|
175
224
|
accounting: bool = True,
|
|
225
|
+
force_sign: bool = False,
|
|
176
226
|
) -> _Expr:
|
|
177
227
|
"""Formats columns as localized currency values."""
|
|
178
228
|
return fmt_number(
|
|
@@ -181,11 +231,13 @@ def fmt_currency(
|
|
|
181
231
|
use_seps=use_seps,
|
|
182
232
|
accounting=accounting,
|
|
183
233
|
pattern=f'{symbol}{{x}}',
|
|
234
|
+
force_sign=force_sign,
|
|
184
235
|
)
|
|
185
236
|
|
|
186
237
|
|
|
187
238
|
@_multicolumn
|
|
188
239
|
def fmt_scientific(
|
|
240
|
+
*,
|
|
189
241
|
columns: _Columns,
|
|
190
242
|
decimals: int = 2,
|
|
191
243
|
scale_by: float = 1.0,
|
|
@@ -259,6 +311,7 @@ def fmt_scientific(
|
|
|
259
311
|
|
|
260
312
|
@_multicolumn
|
|
261
313
|
def fmt_bytes(
|
|
314
|
+
*,
|
|
262
315
|
columns: _Columns,
|
|
263
316
|
standard: _Literal['decimal', 'binary'] = 'decimal',
|
|
264
317
|
decimals: int = 1,
|
|
@@ -375,6 +428,7 @@ def fmt_bytes(
|
|
|
375
428
|
|
|
376
429
|
@_multicolumn
|
|
377
430
|
def fmt_tf(
|
|
431
|
+
*,
|
|
378
432
|
columns: _Columns,
|
|
379
433
|
tf_style: _Literal[
|
|
380
434
|
'true-false',
|
|
@@ -439,12 +493,12 @@ def fmt_tf(
|
|
|
439
493
|
|
|
440
494
|
|
|
441
495
|
@_multicolumn
|
|
442
|
-
def sub_missing(columns: _Columns, missing_text: str = '—') -> _Expr:
|
|
496
|
+
def sub_missing(*, columns: _Columns, missing_text: str = '—') -> _Expr:
|
|
443
497
|
return _col(columns).fill_null(_lit(missing_text))
|
|
444
498
|
|
|
445
499
|
|
|
446
500
|
@_multicolumn
|
|
447
|
-
def sub_zero(columns: _Columns, zero_text: str = '—') -> _Expr:
|
|
501
|
+
def sub_zero(*, columns: _Columns, zero_text: str = '—') -> _Expr:
|
|
448
502
|
return (
|
|
449
503
|
_when(_col(columns) == 0)
|
|
450
504
|
.then(_lit(zero_text))
|
|
@@ -454,6 +508,7 @@ def sub_zero(columns: _Columns, zero_text: str = '—') -> _Expr:
|
|
|
454
508
|
|
|
455
509
|
@_multicolumn
|
|
456
510
|
def fmt_integer(
|
|
511
|
+
*,
|
|
457
512
|
columns: _Columns,
|
|
458
513
|
scale_by: float = 1.0,
|
|
459
514
|
compact: bool = False,
|
|
@@ -461,6 +516,7 @@ def fmt_integer(
|
|
|
461
516
|
use_seps: bool = True,
|
|
462
517
|
accounting: bool = False,
|
|
463
518
|
pattern: str = '{x}',
|
|
519
|
+
force_sign: bool = False,
|
|
464
520
|
) -> _Expr:
|
|
465
521
|
"""Highly optimized native Polars integer formatter wrapper."""
|
|
466
522
|
return fmt_number(
|
|
@@ -472,4 +528,5 @@ def fmt_integer(
|
|
|
472
528
|
use_seps=use_seps,
|
|
473
529
|
accounting=accounting,
|
|
474
530
|
pattern=pattern,
|
|
531
|
+
force_sign=force_sign,
|
|
475
532
|
)
|
|
File without changes
|