From 05ea015c15da81be985a558daedc6a88a554cd44 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 10 Apr 2025 17:17:13 -0400 Subject: [PATCH 01/25] Add the fmt_tf() formatting method --- great_tables/_formats.py | 202 +++++++++++++++++++++++++++++++++++++++ great_tables/gt.py | 2 + 2 files changed, 204 insertions(+) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index 616d4c343..d33c50e72 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2250,6 +2250,208 @@ def fmt_datetime_context( return x_formatted +def fmt_tf( + self: GTSelf, + columns: SelectExpr = None, + rows: int | list[int] | None = None, + tf_style: str = "true-false", + pattern: str = "{x}", + true_val: str | None = None, + false_val: str | None = None, + na_val: str | None = None, + colors: list[str] | None = None, +) -> GTSelf: + """ + Format True and False values + + There can be times where boolean values are useful in a display table. You might want to express + a 'yes' or 'no', a 'true' or 'false', or, perhaps use pairings of complementary symbols that + make sense in a table. The `fmt_tf()` method has a set of `tf_style=` presets that can be used + to quickly map `True`/`False` values to strings, or, symbols like up/down or left/right arrows + and open/closed shapes. + + While the presets are nice, you can provide your own mappings through the `true_val=` and + `false_val=` arguments. For extra customization, you can also apply color to the individual + `True`, `False`, and NA mappings. Just supply a list of colors (up to a length of 3) to the + `colors=` argument. + + Parameters + ---------- + columns + The columns to target. Can either be a single column name or a series of column names + provided in a list. + rows + In conjunction with `columns=`, we can specify which of their rows should undergo + formatting. The default is all rows, resulting in all rows in targeted columns being + formatted. Alternatively, we can supply a list of row indices. + tf_style + The `True`/`False` mapping style to use. By default this is the short name `"true-false"` + which corresponds to the words `"true"` and `"false"`. Two other `tf_style=` values produce + words: `"yes-no"` and `"up-down"`. Options `4` through to `10` involve pairs of symbols + (e.g., `"check-mark"` displays a check mark for `True` and an X symbol for `False`). + pattern + A formatting pattern that allows for decoration of the formatted value. The formatted value + is represented by the `{x}` (which can be used multiple times, if needed) and all other + characters will be interpreted as string literals. + true_val + While the choice of a `tf_style=` will typically supply the `true_val=` and `false_val=` + text, we could override this and supply text for any `True` values. This doesn't need to be + used in conjunction with `false_val=`. + false_val + While the choice of a `tf_style=` will typically supply the `true_val=` and `false_val=` + text, we could override this and supply text for any `False` values. This doesn't need to be + used in conjunction with `true_val=`. + na_val + None of the `tf_style` presets will replace any missing values encountered in the targeted + cells. While we always have the option to use `sub_missing()` for NA replacement, we have + the opportunity handle missing values here with the `na_val=` option. This is useful because + we also have the means to add color to the `na_val=` text or symbol and doing that requires + that a replacement value for NAs is specified here. + colors + Providing a list of color values to colors will progressively add color to the formatted + result depending on the number of colors provided. With a single color, all formatted values + will be in that color. Giving two colors results in `True` values being the first color, and + `False` values receiving the second. With the three-color option, the final color will be + given to any missing values replaced through `na_val=`. + + Returns + ------- + GT + The GT object is returned. This is the same object that the method is called on so that we + can facilitate method chaining. + """ + # If colors is a string, convert it to a list + if isinstance(colors, str): + colors = [colors] + + pf_format = partial( + fmt_tf_context, + data=self, + tf_style=tf_style, + pattern=pattern, + true_val=true_val, + false_val=false_val, + na_val=na_val, + colors=colors, + ) + + return fmt_by_context(self, pf_format=pf_format, columns=columns, rows=rows) + + +def fmt_tf_context( + x: float, + data: GTData, + tf_style: str, + pattern: str, + true_val: str | None, + false_val: str | None, + na_val: str | None, + colors: list[str] | None, + context: str, +) -> str: + # If `x` is not a boolean value, return it as is + if not isinstance(x, bool) and not is_na(data._tbl_data, x): + return x + + # Obtain the list of `True`/`False` text values + tf_vals_list = _get_tf_vals(tf_style=tf_style) + + if x is True: + x_formatted = tf_vals_list[0] + if true_val is not None: + x_formatted = true_val + elif x is False: + x_formatted = tf_vals_list[1] + if false_val is not None: + x_formatted = false_val + elif is_na(data._tbl_data, x) and na_val is not None: + x_formatted = na_val + else: + return x + + if context == "html" and colors is not None: + _check_colors(colors=colors) + + # Apply colors to the formatted value + if len(colors) >= 1 and x is True: + x_formatted = f'{x_formatted}' + elif len(colors) == 1 and x is False: + x_formatted = f'{x_formatted}' + elif len(colors) >= 2 and x is False: + x_formatted = f'{x_formatted}' + elif len(colors) == 3 and is_na(data._tbl_data, x): + x_formatted = f'{x_formatted}' + + # Use a supplied pattern specification to decorate the formatted value + if pattern != "{x}": + # Escape LaTeX special characters from literals in the pattern + if context == "latex": + pattern = escape_pattern_str_latex(pattern_str=pattern) + + x_formatted = pattern.replace("{x}", x_formatted) + + return x_formatted + + +TF_FORMATS = { + "true-false": ["true", "false"], + "yes-no": ["yes", "no"], + "up-down": ["up", "down"], + "check-mark": ["\u2714", "\u2718"], + "circles": ["\u25cf", "\u2b58"], + "squares": ["\u25a0", "\u25a1"], + "diamonds": ["\u25c6", "\u25c7"], + "arrows": ["\u2191", "\u2193"], + "triangles": ["\u25b2", "\u25bc"], + "triangles-lr": ["\u25b6", "\u25c0"], +} + + +def _check_colors(colors: list[str]): + """ + Check if the provided colors are valid. + + Parameters + ---------- + colors + A list of colors to check. + Raises + ------ + ValueError + If the colors are not valid. + """ + if not isinstance(colors, list): + raise ValueError("The `colors` argument must be a list.") + if len(colors) > 3 or len(colors) < 1: + raise ValueError("The `colors` argument must be a list of 1 to 3 colors.") + for color in colors: + if not isinstance(color, str): + raise ValueError("Each color in the `colors` list must be a string.") + + +def _get_tf_vals(tf_style: str) -> list[str]: + """ + Get the `True`/`False` text values based on the `tf_style`. + + Parameters + ---------- + tf_style + The `True`/`False` mapping style to use. + + Returns + ------- + list[str] + A list of two strings representing the `True` and `False` values as strings. + """ + if tf_style not in TF_FORMATS: + raise ValueError( + f"Invalid `tf_style`: {tf_style}. Must be one of {list(TF_FORMATS.keys())}." + ) + + # Return the corresponding `True` and `False` values as a two-element list + return TF_FORMATS[tf_style] + + def fmt_markdown( self: GTSelf, columns: SelectExpr = None, diff --git a/great_tables/gt.py b/great_tables/gt.py index 82b43fd99..93e48e951 100644 --- a/great_tables/gt.py +++ b/great_tables/gt.py @@ -25,6 +25,7 @@ fmt_percent, fmt_roman, fmt_scientific, + fmt_tf, fmt_time, fmt_units, ) @@ -237,6 +238,7 @@ def __init__( fmt_flag = fmt_flag fmt_units = fmt_units fmt_nanoplot = fmt_nanoplot + fmt_tf = fmt_tf data_color = data_color sub_missing = sub_missing From 012b317d8c6616279ea907053cff39c6539ce1fe Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 14 Apr 2025 12:44:27 -0400 Subject: [PATCH 02/25] Remove unneeded check --- great_tables/_formats.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index d33c50e72..58bb6489a 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2420,8 +2420,6 @@ def _check_colors(colors: list[str]): ValueError If the colors are not valid. """ - if not isinstance(colors, list): - raise ValueError("The `colors` argument must be a list.") if len(colors) > 3 or len(colors) < 1: raise ValueError("The `colors` argument must be a list of 1 to 3 colors.") for color in colors: From 66adae745e5782d0098763c2750f99c9c16f233b Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 14 Apr 2025 12:44:33 -0400 Subject: [PATCH 03/25] Remove comment --- great_tables/_formats.py | 1 - 1 file changed, 1 deletion(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index 58bb6489a..b6b247538 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2446,7 +2446,6 @@ def _get_tf_vals(tf_style: str) -> list[str]: f"Invalid `tf_style`: {tf_style}. Must be one of {list(TF_FORMATS.keys())}." ) - # Return the corresponding `True` and `False` values as a two-element list return TF_FORMATS[tf_style] From 724d4ec0b2113deeffad40781cc28231a0b6a713 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 14 Apr 2025 12:44:55 -0400 Subject: [PATCH 04/25] Add example to `fmt_tf()` docs --- great_tables/_formats.py | 56 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index b6b247538..b24490d82 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2287,8 +2287,8 @@ def fmt_tf( tf_style The `True`/`False` mapping style to use. By default this is the short name `"true-false"` which corresponds to the words `"true"` and `"false"`. Two other `tf_style=` values produce - words: `"yes-no"` and `"up-down"`. Options `4` through to `10` involve pairs of symbols - (e.g., `"check-mark"` displays a check mark for `True` and an X symbol for `False`). + words: `"yes-no"` and `"up-down"`. The remaining options involve pairs of symbols (e.g., + `"check-mark"` displays a check mark for `True` and an ✗ symbol for `False`). pattern A formatting pattern that allows for decoration of the formatted value. The formatted value is represented by the `{x}` (which can be used multiple times, if needed) and all other @@ -2310,7 +2310,7 @@ def fmt_tf( colors Providing a list of color values to colors will progressively add color to the formatted result depending on the number of colors provided. With a single color, all formatted values - will be in that color. Giving two colors results in `True` values being the first color, and + will be in that color. Using two colors results in `True` values being the first color, and `False` values receiving the second. With the three-color option, the final color will be given to any missing values replaced through `na_val=`. @@ -2319,6 +2319,56 @@ def fmt_tf( GT The GT object is returned. This is the same object that the method is called on so that we can facilitate method chaining. + + Formatting with the `tf_style=` argument + ---------------------------------------- + We need to supply a preset `tf_style=` value. The following table provides a listing of all + `tf_style=` values and their output `True` and `False` values. + + | | TF Style | Output | + |----|-----------------|-------------------------| + | 1 | `"true-false"` | `"true" / `"false"` | + | 2 | `"yes-no"` | `"yes" / `"no"` | + | 3 | `"up-down"` | `"up" / `"down"` | + | 4 | `"check-mark"` | `"✓" / `"✗"` | + | 5 | `"circles"` | `"●" / `"○"` | + | 6 | `"squares"` | `"■" / `"□"` | + | 7 | `"diamonds"` | `"◆" / `"◇"` | + | 8 | `"arrows"` | `"↑" / `"↓"` | + | 9 | `"triangles"` | `"▲" / `"▼"` | + | 10 | `"triangles-lr"`| `"▶" / `"◀"` | + + Examples + -------- + Let's use a subset of the `sp500` dataset to create a small table containing opening and closing + price data for the last few days in 2015. We added a boolean column (`dir`) where `True` + indicates a price increase from opening to closing and `False` is the opposite. Using `fmt_tf()` + generates up and down arrows in the `dir` column. We elect to use green upward arrows and red + downward arrows (through the `colors=` option). + + ```{python} + from great_tables import GT + from great_tables.data import sp500 + import polars as pl + + sp500_mini = ( + pl.from_pandas(sp500) + .slice(0, 5) + .drop(["volume", "adj_close", "high", "low"]) + .with_columns(dir = pl.col("close") > pl.col("open")) + ) + + ( + GT(sp500_mini, rowname_col="date") + .fmt_tf(columns="dir", tf_style="arrows", colors=["green", "red"]) + .fmt_currency(columns=["open", "close"]) + .cols_label( + open="Opening", + close="Closing", + dir="" + ) + ) + ``` """ # If colors is a string, convert it to a list if isinstance(colors, str): From f2e4148e8bb4ac1f61eac85f959f5e06265dbf5c Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 6 May 2025 11:32:05 -0400 Subject: [PATCH 05/25] Refactor fmt_tf_context() --- great_tables/_formats.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index 3559146ba..fab3e9545 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2404,17 +2404,13 @@ def fmt_tf_context( if not isinstance(x, bool) and not is_na(data._tbl_data, x): return x - # Obtain the list of `True`/`False` text values - tf_vals_list = _get_tf_vals(tf_style=tf_style) + # Obtain the list of `True`/`False` text values with overrides + tf_vals_list = _get_tf_vals(tf_style=tf_style, true_val=true_val, false_val=false_val) if x is True: x_formatted = tf_vals_list[0] - if true_val is not None: - x_formatted = true_val elif x is False: x_formatted = tf_vals_list[1] - if false_val is not None: - x_formatted = false_val elif is_na(data._tbl_data, x) and na_val is not None: x_formatted = na_val else: @@ -2478,26 +2474,41 @@ def _check_colors(colors: list[str]): raise ValueError("Each color in the `colors` list must be a string.") -def _get_tf_vals(tf_style: str) -> list[str]: +def _get_tf_vals( + tf_style: str, true_val: str | None = None, false_val: str | None = None +) -> list[str]: """ - Get the `True`/`False` text values based on the `tf_style`. + Get the `True`/`False` text values based on the `tf_style`, with optional overrides. Parameters ---------- tf_style The `True`/`False` mapping style to use. + true_val + Optional override for the True value. + false_val + Optional override for the False value. Returns ------- list[str] - A list of two strings representing the `True` and `False` values as strings. + A list of two strings representing the `True` and `False` values. """ if tf_style not in TF_FORMATS: raise ValueError( f"Invalid `tf_style`: {tf_style}. Must be one of {list(TF_FORMATS.keys())}." ) - return TF_FORMATS[tf_style] + # Get the base values from the TF_FORMATS dictionary + tf_vals = TF_FORMATS[tf_style].copy() + + # Override with provided values if any + if true_val is not None: + tf_vals[0] = true_val + if false_val is not None: + tf_vals[1] = false_val + + return tf_vals def fmt_markdown( From 38e1069dde0c8da8f1605d70e840af7b4e2aed45 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 6 May 2025 11:34:35 -0400 Subject: [PATCH 06/25] Modify comments in fmt_tf_context() --- great_tables/_formats.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index fab3e9545..3e3042cd0 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2407,6 +2407,7 @@ def fmt_tf_context( # Obtain the list of `True`/`False` text values with overrides tf_vals_list = _get_tf_vals(tf_style=tf_style, true_val=true_val, false_val=false_val) + # Depending on the value of `x`, assign the appropriate formatted value if x is True: x_formatted = tf_vals_list[0] elif x is False: @@ -2416,7 +2417,9 @@ def fmt_tf_context( else: return x + # Apply colors to the formatted value if context == "html" and colors is not None: + # Ensure that the `colors=` value satisfies the requirements _check_colors(colors=colors) # Apply colors to the formatted value From 7e8f1650ff0d2b33adcaef6f1fe866b34f70be6e Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 6 May 2025 11:52:44 -0400 Subject: [PATCH 07/25] Add tests for fmt_tf() --- tests/test_formats.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/test_formats.py b/tests/test_formats.py index 12e5fcf7b..4f08c6fd3 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -1339,6 +1339,47 @@ def test_fmt_datetime_bad_date_style_raises(): assert "date_style must be one of:" in exc_info.value.args[0] +# ------------------------------------------------------------------------------ +# Test `fmt_tf()` +# ------------------------------------------------------------------------------ + +FMT_TF_CASES: list[tuple[dict[str, Any], list[str]]] = [ + (dict(), ["false", "false", "true", "false", "false"]), + (dict(tf_style="arrows"), ["↓", "↓", "↑", "↓", "↓"]), + (dict(tf_style="yes-no"), ["no", "no", "yes", "no", "no"]), + ( + dict(colors=["green"]), + [ + 'false', + 'false', + 'true', + 'false', + 'false', + ], + ), + ( + dict(colors=["green", "red"]), + [ + 'false', + 'false', + 'true', + 'false', + 'false', + ], + ), + (dict(tf_style="yes-no", true_val="YES"), ["no", "no", "YES", "no", "no"]), + (dict(tf_style="yes-no", false_val="NO"), ["NO", "NO", "yes", "NO", "NO"]), +] + + +@pytest.mark.parametrize("fmt_tf_kwargs,x_out", FMT_TF_CASES) +def test_fmt_tf_case(fmt_tf_kwargs: dict[str, Any], x_out: list[str]): + df = pl.DataFrame({"x": [False, False, True, False, False]}) + gt = GT(df).fmt_tf(columns="x", **fmt_tf_kwargs) + x = _get_column_of_values(gt, column_name="x", context="html") + assert x == x_out + + # ------------------------------------------------------------------------------ # Test `fmt_bytes()` # ------------------------------------------------------------------------------ From 4b8296fe8f7778b7639f44a2bbced60ecd6d8d6a Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 6 May 2025 12:42:41 -0400 Subject: [PATCH 08/25] Move validation of tf_style= param --- great_tables/_formats.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index 3e3042cd0..d382019bc 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2404,6 +2404,12 @@ def fmt_tf_context( if not isinstance(x, bool) and not is_na(data._tbl_data, x): return x + # Validate `tf_style=` value + if tf_style not in TF_FORMATS: + raise ValueError( + f"Invalid `tf_style`: {tf_style}. Must be one of {list(TF_FORMATS.keys())}." + ) + # Obtain the list of `True`/`False` text values with overrides tf_vals_list = _get_tf_vals(tf_style=tf_style, true_val=true_val, false_val=false_val) @@ -2497,11 +2503,6 @@ def _get_tf_vals( list[str] A list of two strings representing the `True` and `False` values. """ - if tf_style not in TF_FORMATS: - raise ValueError( - f"Invalid `tf_style`: {tf_style}. Must be one of {list(TF_FORMATS.keys())}." - ) - # Get the base values from the TF_FORMATS dictionary tf_vals = TF_FORMATS[tf_style].copy() From 7c3827a40ebe48440f18327836184d24f24074b6 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 6 May 2025 12:42:57 -0400 Subject: [PATCH 09/25] Add several tests of fmt_tf() --- tests/test_formats.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_formats.py b/tests/test_formats.py index 4f08c6fd3..8b94aa60f 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -1344,9 +1344,9 @@ def test_fmt_datetime_bad_date_style_raises(): # ------------------------------------------------------------------------------ FMT_TF_CASES: list[tuple[dict[str, Any], list[str]]] = [ - (dict(), ["false", "false", "true", "false", "false"]), - (dict(tf_style="arrows"), ["↓", "↓", "↑", "↓", "↓"]), - (dict(tf_style="yes-no"), ["no", "no", "yes", "no", "no"]), + (dict(), ["false", "false", "true", "false", "false", "None"]), + (dict(tf_style="arrows"), ["↓", "↓", "↑", "↓", "↓", "None"]), + (dict(tf_style="yes-no"), ["no", "no", "yes", "no", "no", "None"]), ( dict(colors=["green"]), [ @@ -1355,6 +1355,7 @@ def test_fmt_datetime_bad_date_style_raises(): 'true', 'false', 'false', + "None", ], ), ( @@ -1365,16 +1366,18 @@ def test_fmt_datetime_bad_date_style_raises(): 'true', 'false', 'false', + "None", ], ), - (dict(tf_style="yes-no", true_val="YES"), ["no", "no", "YES", "no", "no"]), - (dict(tf_style="yes-no", false_val="NO"), ["NO", "NO", "yes", "NO", "NO"]), + (dict(tf_style="yes-no", true_val="YES"), ["no", "no", "YES", "no", "no", "None"]), + (dict(tf_style="yes-no", false_val="NO"), ["NO", "NO", "yes", "NO", "NO", "None"]), + (dict(tf_style="yes-no", na_val="NA"), ["no", "no", "yes", "no", "no", "NA"]), ] @pytest.mark.parametrize("fmt_tf_kwargs,x_out", FMT_TF_CASES) def test_fmt_tf_case(fmt_tf_kwargs: dict[str, Any], x_out: list[str]): - df = pl.DataFrame({"x": [False, False, True, False, False]}) + df = pl.DataFrame({"x": [False, False, True, False, False, None]}) gt = GT(df).fmt_tf(columns="x", **fmt_tf_kwargs) x = _get_column_of_values(gt, column_name="x", context="html") assert x == x_out From b03371a364ad4b02d24362ff8d6242d1a0a89ba0 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 6 May 2025 12:44:03 -0400 Subject: [PATCH 10/25] Add another test for fmt_tf() --- tests/test_formats.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_formats.py b/tests/test_formats.py index 8b94aa60f..4e2541dc9 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -1369,6 +1369,17 @@ def test_fmt_datetime_bad_date_style_raises(): "None", ], ), + ( + dict(na_val="NA", colors=["green", "red", "blue"]), + [ + 'false', + 'false', + 'true', + 'false', + 'false', + 'NA', + ], + ), (dict(tf_style="yes-no", true_val="YES"), ["no", "no", "YES", "no", "no", "None"]), (dict(tf_style="yes-no", false_val="NO"), ["NO", "NO", "yes", "NO", "NO", "None"]), (dict(tf_style="yes-no", na_val="NA"), ["no", "no", "yes", "no", "no", "NA"]), From c53ac7cf18c808e29b36e8aabc9539e4d75bfbcb Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 6 May 2025 13:30:06 -0400 Subject: [PATCH 11/25] Add tests of _check_colors() --- tests/test_formats.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_formats.py b/tests/test_formats.py index 4e2541dc9..d0990e2c2 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -9,6 +9,7 @@ from great_tables._data_color.base import _html_color from great_tables._formats import ( FmtImage, + _check_colors, _expand_exponential_to_full_string, _format_number_n_sigfig, _format_number_fixed_decimals, @@ -1863,6 +1864,15 @@ def test_fmt_image_http(url: str): assert strip_windows_drive(res) == dst +def test_check_colors(): + # Error on more than 3 colors provided + with pytest.raises(ValueError): + _check_colors(colors=["red", "blue", "green", "gray"]) + # Error on not passing a list of strings + with pytest.raises(ValueError): + _check_colors(colors=[1, 2, 3]) # type: ignore + + @pytest.mark.parametrize( "src,dst", [ From 4596bead169755642b9aaa1d56880c91ab07633d Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 6 May 2025 13:39:49 -0400 Subject: [PATCH 12/25] Add test of pattern= use with fmt_tf() --- tests/test_formats.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_formats.py b/tests/test_formats.py index d0990e2c2..93538f0d3 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -1384,6 +1384,7 @@ def test_fmt_datetime_bad_date_style_raises(): (dict(tf_style="yes-no", true_val="YES"), ["no", "no", "YES", "no", "no", "None"]), (dict(tf_style="yes-no", false_val="NO"), ["NO", "NO", "yes", "NO", "NO", "None"]), (dict(tf_style="yes-no", na_val="NA"), ["no", "no", "yes", "no", "no", "NA"]), + (dict(pattern="{x}!"), ["false!", "false!", "true!", "false!", "false!", "None"]), ] From 73d7822945bc8eafdcfba78c899eb7702f25538b Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 6 May 2025 13:41:41 -0400 Subject: [PATCH 13/25] Add test where scalar colors= param used --- tests/test_formats.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_formats.py b/tests/test_formats.py index 93538f0d3..32790a25d 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -1359,6 +1359,17 @@ def test_fmt_datetime_bad_date_style_raises(): "None", ], ), + ( + dict(colors="blue"), + [ + 'false', + 'false', + 'true', + 'false', + 'false', + "None", + ], + ), ( dict(colors=["green", "red"]), [ From c873d757bc2a2ee5212b5b5f5b0857a28eba5518 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 6 May 2025 13:49:12 -0400 Subject: [PATCH 14/25] Refactor color application logic in fmt_tf_context() --- great_tables/_formats.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index d382019bc..3e3e98208 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2428,14 +2428,14 @@ def fmt_tf_context( # Ensure that the `colors=` value satisfies the requirements _check_colors(colors=colors) - # Apply colors to the formatted value - if len(colors) >= 1 and x is True: + # Apply colors to the formatted value based on condition + if x is True and len(colors) >= 1: x_formatted = f'{x_formatted}' - elif len(colors) == 1 and x is False: - x_formatted = f'{x_formatted}' - elif len(colors) >= 2 and x is False: - x_formatted = f'{x_formatted}' - elif len(colors) == 3 and is_na(data._tbl_data, x): + elif x is False: + # Use first color if only one color provided, otherwise use second color + color_idx = 0 if len(colors) == 1 else 1 + x_formatted = f'{x_formatted}' + elif is_na(data._tbl_data, x) and len(colors) == 3: x_formatted = f'{x_formatted}' # Use a supplied pattern specification to decorate the formatted value From c638e2e44365e98e05ee7625637b5a173106289b Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 9 Jun 2025 08:36:29 -0400 Subject: [PATCH 15/25] Add type annot to TF_FORMATS dict --- great_tables/_formats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index 455281615..f4e7b4a6b 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2461,7 +2461,7 @@ def fmt_tf_context( return x_formatted -TF_FORMATS = { +TF_FORMATS: dict[str, list[str]] = { "true-false": ["true", "false"], "yes-no": ["yes", "no"], "up-down": ["up", "down"], From 91aff26b8d4618be7c8d1a89539731f2e9c597b2 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 9 Jun 2025 08:40:32 -0400 Subject: [PATCH 16/25] Reduce size of list in test --- tests/test_formats.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/tests/test_formats.py b/tests/test_formats.py index 67ccc01e4..d542cd63f 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -1346,63 +1346,51 @@ def test_fmt_datetime_bad_date_style_raises(): # ------------------------------------------------------------------------------ FMT_TF_CASES: list[tuple[dict[str, Any], list[str]]] = [ - (dict(), ["false", "false", "true", "false", "false", "None"]), - (dict(tf_style="arrows"), ["↓", "↓", "↑", "↓", "↓", "None"]), - (dict(tf_style="yes-no"), ["no", "no", "yes", "no", "no", "None"]), + (dict(), ["true", "false", "None"]), + (dict(tf_style="arrows"), ["↑", "↓", "None"]), + (dict(tf_style="yes-no"), ["yes", "no", "None"]), ( dict(colors=["green"]), [ - 'false', - 'false', 'true', 'false', - 'false', "None", ], ), ( dict(colors="blue"), [ - 'false', - 'false', 'true', 'false', - 'false', "None", ], ), ( dict(colors=["green", "red"]), [ - 'false', - 'false', 'true', 'false', - 'false', "None", ], ), ( dict(na_val="NA", colors=["green", "red", "blue"]), [ - 'false', - 'false', 'true', 'false', - 'false', 'NA', ], ), - (dict(tf_style="yes-no", true_val="YES"), ["no", "no", "YES", "no", "no", "None"]), - (dict(tf_style="yes-no", false_val="NO"), ["NO", "NO", "yes", "NO", "NO", "None"]), - (dict(tf_style="yes-no", na_val="NA"), ["no", "no", "yes", "no", "no", "NA"]), - (dict(pattern="{x}!"), ["false!", "false!", "true!", "false!", "false!", "None"]), + (dict(tf_style="yes-no", true_val="YES"), ["YES", "no", "None"]), + (dict(tf_style="yes-no", false_val="NO"), ["yes", "NO", "None"]), + (dict(tf_style="yes-no", na_val="NA"), ["yes", "no", "NA"]), + (dict(pattern="{x}!"), ["true!", "false!", "None"]), ] @pytest.mark.parametrize("fmt_tf_kwargs,x_out", FMT_TF_CASES) def test_fmt_tf_case(fmt_tf_kwargs: dict[str, Any], x_out: list[str]): - df = pl.DataFrame({"x": [False, False, True, False, False, None]}) + df = pl.DataFrame({"x": [True, False, None]}) gt = GT(df).fmt_tf(columns="x", **fmt_tf_kwargs) x = _get_column_of_values(gt, column_name="x", context="html") assert x == x_out From 65be80fed7b53cb711b8ef9948a7bd3c3e8295dc Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 9 Jun 2025 08:51:09 -0400 Subject: [PATCH 17/25] Raise ValueError is input not bool/NA --- great_tables/_formats.py | 4 ++-- tests/test_formats.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index f4e7b4a6b..f5d11dca4 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2412,9 +2412,9 @@ def fmt_tf_context( colors: list[str] | None, context: str, ) -> str: - # If `x` is not a boolean value, return it as is + # If `x` is not a boolean value, raise an error if not isinstance(x, bool) and not is_na(data._tbl_data, x): - return x + raise ValueError(f"Expected boolean value or NA, but got {str(type(x))}.") # Validate `tf_style=` value if tf_style not in TF_FORMATS: diff --git a/tests/test_formats.py b/tests/test_formats.py index d542cd63f..77b6c445e 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -1396,6 +1396,17 @@ def test_fmt_tf_case(fmt_tf_kwargs: dict[str, Any], x_out: list[str]): assert x == x_out +def test_fmt_tf_column_invalid_type(): + df = pl.DataFrame({"x": [0, 1, 2]}) + gt = GT(df).fmt_tf(columns="x") + + with pytest.raises(ValueError) as exc_info: + # This triggers the actual formatting by accessing the formatted values + _get_column_of_values(gt, column_name="x", context="html") + + assert "Expected boolean value or NA, but got" in exc_info.value.args[0] + + # ------------------------------------------------------------------------------ # Test `fmt_bytes()` # ------------------------------------------------------------------------------ From 2d6347b7e7276ca6bf7ef1f7da7defce8852e5f9 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 9 Jun 2025 08:54:39 -0400 Subject: [PATCH 18/25] Add ValueError in else block for unexpected cases --- great_tables/_formats.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index f5d11dca4..58b9ecc3b 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2449,6 +2449,8 @@ def fmt_tf_context( x_formatted = f'{x_formatted}' elif is_na(data._tbl_data, x) and len(colors) == 3: x_formatted = f'{x_formatted}' + else: + raise ValueError("Unexpected condition in color application.") # Use a supplied pattern specification to decorate the formatted value if pattern != "{x}": From 4537199bbecb5d86a6b63bc3c92ff2d9e20a24de Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 9 Jun 2025 09:10:28 -0400 Subject: [PATCH 19/25] Put color-mapping logic in TfColorMap cls --- great_tables/_formats.py | 47 ++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index 58b9ecc3b..a1f1fa3c3 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2440,17 +2440,14 @@ def fmt_tf_context( # Ensure that the `colors=` value satisfies the requirements _check_colors(colors=colors) - # Apply colors to the formatted value based on condition - if x is True and len(colors) >= 1: - x_formatted = f'{x_formatted}' - elif x is False: - # Use first color if only one color provided, otherwise use second color - color_idx = 0 if len(colors) == 1 else 1 - x_formatted = f'{x_formatted}' - elif is_na(data._tbl_data, x) and len(colors) == 3: - x_formatted = f'{x_formatted}' - else: - raise ValueError("Unexpected condition in color application.") + # Create color mapping + color_map = from_colors_list(colors) + + # Get the appropriate color for this value + color = color_map.get_color(x, data) + + if color is not None: + x_formatted = f'{x_formatted}' # Use a supplied pattern specification to decorate the formatted value if pattern != "{x}": @@ -2529,6 +2526,34 @@ def _get_tf_vals( return tf_vals +@dataclass +class TfColorMap: + true_color: str | None = None + false_color: str | None = None + na_color: str | None = None + + def get_color(self, x: bool | None, data: GTData) -> str | None: + if x is True: + return self.true_color + elif x is False: + return self.false_color + elif is_na(data._tbl_data, x): + return self.na_color + else: + raise ValueError(f"Unexpected value type: {type(x)}") + + +def from_colors_list(colors: list[str]) -> TfColorMap: + if len(colors) == 1: + return TfColorMap(true_color=colors[0], false_color=colors[0]) + elif len(colors) == 2: + return TfColorMap(true_color=colors[0], false_color=colors[1]) + elif len(colors) == 3: + return TfColorMap(true_color=colors[0], false_color=colors[1], na_color=colors[2]) + else: + raise ValueError("Colors list must have 1-3 elements.") + + def fmt_markdown( self: GTSelf, columns: SelectExpr = None, From e348dd49681be1acae810d5801658e2b9d6af4e9 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 9 Jun 2025 11:18:06 -0400 Subject: [PATCH 20/25] Better handle NA values (skip formatting if needed) --- great_tables/_formats.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index a1f1fa3c3..9081f9e1a 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -14,7 +14,7 @@ from babel.dates import format_date, format_datetime, format_time from typing_extensions import TypeAlias -from ._gt_data import FormatFn, FormatFns, FormatInfo, GTData +from ._gt_data import FormatFn, FormatFns, FormatInfo, FormatterSkipElement, GTData from ._helpers import px from ._locale import ( _get_currencies_data, @@ -2411,7 +2411,7 @@ def fmt_tf_context( na_val: str | None, colors: list[str] | None, context: str, -) -> str: +) -> str | FormatterSkipElement: # If `x` is not a boolean value, raise an error if not isinstance(x, bool) and not is_na(data._tbl_data, x): raise ValueError(f"Expected boolean value or NA, but got {str(type(x))}.") @@ -2432,8 +2432,12 @@ def fmt_tf_context( x_formatted = tf_vals_list[1] elif is_na(data._tbl_data, x) and na_val is not None: x_formatted = na_val - else: - return x + elif is_na(data._tbl_data, x): + # Handle NA values when no `na_val` is provided by skipping formatting entirely + return FormatterSkipElement() + elif is_na(data._tbl_data, x): + # Note that this should never happen (we have validation at top) but guard against anyway + raise ValueError(f"Unexpected value in `fmt_tf_context()`: {x} (type: {type(x)}).") # Apply colors to the formatted value if context == "html" and colors is not None: From aa26e830c136833b5bb6e1a293a92a790e85b423 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 9 Jun 2025 11:20:57 -0400 Subject: [PATCH 21/25] Fix mistake in conditional logic (end with else) --- great_tables/_formats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index 9081f9e1a..d39ae06ec 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2435,7 +2435,7 @@ def fmt_tf_context( elif is_na(data._tbl_data, x): # Handle NA values when no `na_val` is provided by skipping formatting entirely return FormatterSkipElement() - elif is_na(data._tbl_data, x): + else: # Note that this should never happen (we have validation at top) but guard against anyway raise ValueError(f"Unexpected value in `fmt_tf_context()`: {x} (type: {type(x)}).") From 65365a369c82924e181cc0a83f621536e537b066 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 9 Jun 2025 14:19:38 -0400 Subject: [PATCH 22/25] Refactor based on code review --- great_tables/_formats.py | 90 ++++++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index d39ae06ec..af3bfa69e 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -7,7 +7,18 @@ from decimal import Decimal from functools import partial from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal, TypedDict, TypeVar, cast +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Literal, + TypedDict, + TypeVar, + Union, + cast, + overload, +) import babel import faicons @@ -2402,7 +2413,7 @@ def fmt_tf( def fmt_tf_context( - x: float, + x: Any, data: GTData, tf_style: str, pattern: str, @@ -2412,9 +2423,12 @@ def fmt_tf_context( colors: list[str] | None, context: str, ) -> str | FormatterSkipElement: - # If `x` is not a boolean value, raise an error - if not isinstance(x, bool) and not is_na(data._tbl_data, x): - raise ValueError(f"Expected boolean value or NA, but got {str(type(x))}.") + if is_na(data._tbl_data, x): + x = None + elif not isinstance(x, bool): + raise ValueError(f"Expected boolean value or NA, but got {type(x)}.") + + x = cast(Union[bool, None], x) # Validate `tf_style=` value if tf_style not in TF_FORMATS: @@ -2422,22 +2436,17 @@ def fmt_tf_context( f"Invalid `tf_style`: {tf_style}. Must be one of {list(TF_FORMATS.keys())}." ) + if x is None and na_val is None: + return FormatterSkipElement() + + # TODO: Check type of `na_val=` and raise error if not a string or None + # Obtain the list of `True`/`False` text values with overrides tf_vals_list = _get_tf_vals(tf_style=tf_style, true_val=true_val, false_val=false_val) - # Depending on the value of `x`, assign the appropriate formatted value - if x is True: - x_formatted = tf_vals_list[0] - elif x is False: - x_formatted = tf_vals_list[1] - elif is_na(data._tbl_data, x) and na_val is not None: - x_formatted = na_val - elif is_na(data._tbl_data, x): - # Handle NA values when no `na_val` is provided by skipping formatting entirely - return FormatterSkipElement() - else: - # Note that this should never happen (we have validation at top) but guard against anyway - raise ValueError(f"Unexpected value in `fmt_tf_context()`: {x} (type: {type(x)}).") + tf_vals = TfMap(*tf_vals_list, na_color=na_val) + + x_formatted = tf_vals.get_color(x, data, strict=True) # Apply colors to the formatted value if context == "html" and colors is not None: @@ -2445,10 +2454,10 @@ def fmt_tf_context( _check_colors(colors=colors) # Create color mapping - color_map = from_colors_list(colors) + color_map = TfMap.from_list(colors) # Get the appropriate color for this value - color = color_map.get_color(x, data) + color = color_map.get_color(x, data, strict=False) if color is not None: x_formatted = f'{x_formatted}' @@ -2531,31 +2540,42 @@ def _get_tf_vals( @dataclass -class TfColorMap: +class TfMap: true_color: str | None = None false_color: str | None = None na_color: str | None = None - def get_color(self, x: bool | None, data: GTData) -> str | None: + @classmethod + def from_list(cls, colors: list[str]) -> TfMap: + if len(colors) == 1: + return cls(true_color=colors[0], false_color=colors[0]) + elif len(colors) == 2: + return cls(true_color=colors[0], false_color=colors[1]) + elif len(colors) == 3: + return cls(true_color=colors[0], false_color=colors[1], na_color=colors[2]) + else: + raise ValueError("Colors list must have 1-3 elements.") + + @overload + def get_color(self, x: bool | None, data: GTData, strict: Literal[False]) -> str | None: ... + + @overload + def get_color(self, x: bool | None, data: GTData, strict: Literal[True]) -> str: ... + + def get_color(self, x: bool | None, data: GTData, strict: bool = False) -> str | None: if x is True: - return self.true_color + res = self.true_color elif x is False: - return self.false_color + res = self.false_color elif is_na(data._tbl_data, x): - return self.na_color + res = self.na_color else: - raise ValueError(f"Unexpected value type: {type(x)}") + raise TypeError(f"Unexpected value type: {type(x)}") + if strict and res is None: + raise ValueError("No style defined for this value in TfMap.") -def from_colors_list(colors: list[str]) -> TfColorMap: - if len(colors) == 1: - return TfColorMap(true_color=colors[0], false_color=colors[0]) - elif len(colors) == 2: - return TfColorMap(true_color=colors[0], false_color=colors[1]) - elif len(colors) == 3: - return TfColorMap(true_color=colors[0], false_color=colors[1], na_color=colors[2]) - else: - raise ValueError("Colors list must have 1-3 elements.") + return res def fmt_markdown( From 6c1667439bc9da66f3005deea28cfa8e00a0a579 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 9 Jun 2025 14:24:15 -0400 Subject: [PATCH 23/25] Remove reassignments; add check for na_val type --- great_tables/_formats.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index af3bfa69e..fff90506d 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2436,11 +2436,13 @@ def fmt_tf_context( f"Invalid `tf_style`: {tf_style}. Must be one of {list(TF_FORMATS.keys())}." ) + # Check type of `na_val=` and raise error if not a string or None + if na_val is not None and not isinstance(na_val, str): + raise ValueError("The `na_val` argument must be a string or None.") + if x is None and na_val is None: return FormatterSkipElement() - # TODO: Check type of `na_val=` and raise error if not a string or None - # Obtain the list of `True`/`False` text values with overrides tf_vals_list = _get_tf_vals(tf_style=tf_style, true_val=true_val, false_val=false_val) @@ -2459,8 +2461,10 @@ def fmt_tf_context( # Get the appropriate color for this value color = color_map.get_color(x, data, strict=False) - if color is not None: - x_formatted = f'{x_formatted}' + x_styled = f'{x_formatted}' + + else: + x_styled = x_formatted # Use a supplied pattern specification to decorate the formatted value if pattern != "{x}": @@ -2468,9 +2472,11 @@ def fmt_tf_context( if context == "latex": pattern = escape_pattern_str_latex(pattern_str=pattern) - x_formatted = pattern.replace("{x}", x_formatted) + x_out = pattern.replace("{x}", x_styled) + else: + x_out = x_styled - return x_formatted + return x_out TF_FORMATS: dict[str, list[str]] = { From 331fcce9af741230be8de882043f4ccae16e33a9 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 9 Jun 2025 14:36:58 -0400 Subject: [PATCH 24/25] Add warning about colors= unsupported in LaTeX --- great_tables/_formats.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/great_tables/_formats.py b/great_tables/_formats.py index fff90506d..63077a820 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -2440,9 +2440,14 @@ def fmt_tf_context( if na_val is not None and not isinstance(na_val, str): raise ValueError("The `na_val` argument must be a string or None.") + # If `x` is None and `na_val` is None, skip formatting entirely if x is None and na_val is None: return FormatterSkipElement() + # Add warning in LaTeX context about `colors=` not being supported + if context == "latex" and colors is not None: + raise ValueError("The `colors=` argument is not currently supported for LaTeX tables.") + # Obtain the list of `True`/`False` text values with overrides tf_vals_list = _get_tf_vals(tf_style=tf_style, true_val=true_val, false_val=false_val) From 6012a75a978ce2db0160a30a6f19ea163473b775 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 9 Jun 2025 16:20:11 -0400 Subject: [PATCH 25/25] Add a few more tests --- tests/test_formats.py | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_formats.py b/tests/test_formats.py index 77b6c445e..69c1094f1 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -1407,6 +1407,55 @@ def test_fmt_tf_column_invalid_type(): assert "Expected boolean value or NA, but got" in exc_info.value.args[0] +def test_fmt_tf_invalid_na_val_type(): + df = pl.DataFrame({"x": [True, False, None]}) + gt = GT(df).fmt_tf(columns="x", na_val=123) # Invalid: numeric na_val + + with pytest.raises(ValueError, match="The `na_val` argument must be a string or None"): + _get_column_of_values(gt, column_name="x", context="html") + + +def test_fmt_tf_skip_formatting_na_without_na_val(): + df = pl.DataFrame({"x": [True, False, None]}) + gt = GT(df).fmt_tf(columns="x") # No na_val provided + + # NA vals should be skipped (where the original value is preserved) + result = _get_column_of_values(gt, column_name="x", context="html") + + # The NA value should remain as the string representation "None" + assert result[2] == "None" + + +def test_fmt_tf_latex_context_with_colors(): + df = pl.DataFrame({"x": [True, False]}) + gt = GT(df).fmt_tf(columns="x", colors=["red", "blue"]) + + with pytest.raises( + ValueError, match="The `colors=` argument is not currently supported for LaTeX tables" + ): + _get_column_of_values(gt, column_name="x", context="latex") + + +@pytest.mark.parametrize( + "invalid_na_val", + [ + ["invalid"], # list + {"invalid": "value"}, # dict + True, # boolean + 123, # integer + 12.34, # float + ("tuple", "value"), # tuple + set(["set_value"]), # set + ], +) +def test_fmt_tf_na_val_type_validation(invalid_na_val): + df = pl.DataFrame({"x": [True, False, None]}) + gt = GT(df).fmt_tf(columns="x", na_val=invalid_na_val) + + with pytest.raises(ValueError, match="The `na_val` argument must be a string or None"): + _get_column_of_values(gt, column_name="x", context="html") + + # ------------------------------------------------------------------------------ # Test `fmt_bytes()` # ------------------------------------------------------------------------------