diff --git a/great_tables/_gt_data.py b/great_tables/_gt_data.py index bfe6d2d4d..c0bce03c1 100644 --- a/great_tables/_gt_data.py +++ b/great_tables/_gt_data.py @@ -5,7 +5,7 @@ from collections.abc import Sequence from dataclasses import dataclass, field, replace from enum import Enum, auto -from typing import Any, Callable, TypeVar, overload, TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, overload from typing_extensions import Self, TypeAlias, Union @@ -15,17 +15,17 @@ DataFrameLike, TblData, _get_cell, + _get_column_dtype, _set_cell, copy_data, create_empty_frame, get_column_names, - _get_column_dtype, n_rows, to_list, validate_frame, ) from ._text import BaseText -from ._utils import _str_detect, OrderedSet +from ._utils import OrderedSet, _str_detect if TYPE_CHECKING: from ._helpers import UnitStr @@ -1219,6 +1219,9 @@ class Options: quarto_disable_processing: OptionsInfo = OptionsInfo(False, "quarto", "logical", False) quarto_use_bootstrap: OptionsInfo = OptionsInfo(False, "quarto", "logical", False) + def __getitem__(self, k: str) -> Any: + return getattr(self, k).value + def _get_all_options_keys(self) -> list[str | None]: return [x.parameter for x in self._options.values()] diff --git a/great_tables/_options.py b/great_tables/_options.py index 793cbb0e4..0284ca811 100644 --- a/great_tables/_options.py +++ b/great_tables/_options.py @@ -1,12 +1,11 @@ from __future__ import annotations from dataclasses import dataclass, fields, replace -from typing import TYPE_CHECKING, ClassVar, cast, Iterable +from typing import TYPE_CHECKING, ClassVar, Iterable, cast from . import _utils from ._helpers import FontStackName, GoogleFont, _intify_scaled_px, px - if TYPE_CHECKING: from ._types import GTSelf @@ -164,6 +163,7 @@ def tab_options( row_striping_background_color: str | None = None, row_striping_include_stub: bool | None = None, row_striping_include_table_body: bool | None = None, + quarto_disable_processing: bool | None = None, ) -> GTSelf: """ Modify the table output options. @@ -473,6 +473,8 @@ def tab_options( An option for whether to include the stub when striping rows. row_striping_include_table_body An option for whether to include the table body when striping rows. + quarto_disable_processing + Whether to disable Quarto table processing. Returns diff --git a/great_tables/_render_checks.py b/great_tables/_render_checks.py new file mode 100644 index 000000000..1c1a319bb --- /dev/null +++ b/great_tables/_render_checks.py @@ -0,0 +1,43 @@ +import warnings + +from ._gt_data import GTData +from ._render import infer_render_env + + +class RenderWarning(Warning): + """Base warning for render checks.""" + + +def _render_check(data: GTData): + if infer_render_env() == "quarto": + _render_check_quarto(data) + + +def _render_check_quarto(data: GTData): + """Check for rendering issues in Quarto. + + * Quarto uses Pandoc internally to handle tables, and Pandoc tables do not support pixel widths. + As a result, when cols_width is used in a Quarto environment, widths need to be converted to + percentages. + * Alternatively, users may set the option quarto_disable_processing to True. + * Disabling table processing also helps with pieces like table striping, but means Quarto will + not process cross-references, etc.. + """ + + # quarto_disable_processing is set, no need to warn ---- + if data._options["quarto_disable_processing"]: + return + + # cols_widths set ---- + col_widths = [col.column_width for col in data._boxhead] + + is_any_set = any([width is not None for width in col_widths]) + is_all_pct = all([width is None or width.rstrip().endswith("%") for width in col_widths]) + if is_any_set and not is_all_pct: + warnings.warn( + "Rendering table with .col_widths() in Quarto may result in unexpected behavior." + " This is because Quarto performs custom table processing." + " Either use all percentage widths, or set .tab_options(quarto_disable_processing=True)" + " to disable Quarto table processing.", + RenderWarning, + ) diff --git a/great_tables/_spanners.py b/great_tables/_spanners.py index b41bd1fdd..5c9f837c6 100644 --- a/great_tables/_spanners.py +++ b/great_tables/_spanners.py @@ -2,6 +2,7 @@ import itertools from typing import TYPE_CHECKING +import warnings from ._gt_data import SpannerInfo, Spanners from ._locations import resolve_cols_c @@ -706,6 +707,14 @@ def cols_width(self: GTSelf, cases: dict[str, str] | None = None, **kwargs: str) _assert_list_is_subset(mod_columns, set_list=column_names) for col, width in new_cases.items(): + if not isinstance(width, str): + warnings.warn( + "Column widths must be a string." + f" Column `{col}` specified width using a {type(width)}." + " Coercing width to a string, but in the future this will raise an error.", + DeprecationWarning, + ) + width = str(width) curr_boxhead = curr_boxhead._set_column_width(col, width) return self._replace(_boxhead=curr_boxhead) diff --git a/great_tables/gt.py b/great_tables/gt.py index ed1b84a19..08acd7060 100644 --- a/great_tables/gt.py +++ b/great_tables/gt.py @@ -46,6 +46,7 @@ ) from ._pipe import pipe from ._render import infer_render_env_defaults +from ._render_checks import _render_check from ._source_notes import tab_source_note from ._spanners import ( cols_hide, @@ -350,6 +351,9 @@ def _render_as_html( make_page: bool = False, all_important: bool = False, ) -> str: + # TODO: better to put these checks in a pre render hook? + _render_check(self) + heading_component = create_heading_component_h(data=self) column_labels_component = create_columns_component_h(data=self) body_component = create_body_component_h(data=self) diff --git a/tests/test__render_checks.py b/tests/test__render_checks.py new file mode 100644 index 000000000..f7d45d96c --- /dev/null +++ b/tests/test__render_checks.py @@ -0,0 +1,48 @@ +import warnings + +import pytest + +from contextlib import contextmanager +from great_tables import GT, exibble +from great_tables._render import infer_render_env +from great_tables._render_checks import RenderWarning, _render_check_quarto + + +@contextmanager +def set_quarto_env(): + import os + + orig = os.environ.get("QUARTO_BIN_PATH", None) + + try: + os.environ["QUARTO_BIN_PATH"] = "1" + yield + finally: + if orig is not None: + os.environ["QUARTO_BIN_PATH"] = orig + else: + del os.environ["QUARTO_BIN_PATH"] + + +def test_check_quarto_runs(): + gt = GT(exibble).cols_width({"num": "100px"}) + + with set_quarto_env(), pytest.warns(RenderWarning): + assert infer_render_env() == "quarto" + gt.render("html") + + +def test_check_quarto_disable_processing(): + gt = GT(exibble).cols_width({"num": "100px"}).tab_options(quarto_disable_processing=True) + + # assert no warning issued + with warnings.catch_warnings(): + warnings.simplefilter("error") + _render_check_quarto(gt) + + +def test_check_quarto_cols_width(): + gt = GT(exibble).cols_width({"num": "100px"}) + + with pytest.warns(RenderWarning): + _render_check_quarto(gt) diff --git a/tests/test_spanners.py b/tests/test_spanners.py index 4e5e6dd66..e115fc4f3 100644 --- a/tests/test_spanners.py +++ b/tests/test_spanners.py @@ -244,6 +244,13 @@ def test_cols_width_fully_set_pct_2(): assert gt_tbl._boxhead[2].column_width == "40%" +def test_cols_width_non_str_deprecated(): + df = pd.DataFrame({"a": [1, 2], "b": [3, 4], "c": [5, 6]}) + + with pytest.warns(DeprecationWarning): + gt_tbl = GT(df).cols_width({"a": 10}) + + def test_cols_width_html_colgroup(): df = pd.DataFrame({"a": [1, 2], "b": [3, 4], "c": [5, 6]}) gt_tbl = GT(df).cols_width({"a": "10px", "b": "20px", "c": "30px"})