From e105a4cf2f1c463cf22aa16ab93e2871bb6f9135 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 15 Jun 2022 08:44:16 -0400 Subject: [PATCH 01/14] Move CSS expansion lookup to dictionary --- pandas/io/formats/css.py | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/pandas/io/formats/css.py b/pandas/io/formats/css.py index 5335887785881..f9d3cb09447ba 100644 --- a/pandas/io/formats/css.py +++ b/pandas/io/formats/css.py @@ -187,6 +187,12 @@ class CSSResolver: SIDES = ("top", "right", "bottom", "left") + CSS_EXPANSIONS = { + **{"-".join(["border", prop]): _border_expander(prop) for prop in ["", "top", "right", "bottom", "left"]}, + **{"-".join(["border", prop]): _side_expander("border-{:s}-"+prop) for prop in ["color", "style", "width"]}, + **{"margin": _side_expander("margin-{:s}"), "padding": _side_expander("padding-{:s}")} + } + def __call__( self, declarations_str: str, @@ -348,26 +354,12 @@ def _error(): def atomize(self, declarations) -> Generator[tuple[str, str], None, None]: for prop, value in declarations: - attr = "expand_" + prop.replace("-", "_") - try: - expand = getattr(self, attr) - except AttributeError: - yield prop, value - else: - for prop, value in expand(prop, value): + if prop in self.CSS_EXPANSIONS: + expand = self.CSS_EXPANSIONS[prop] + for prop, value in expand(self, prop, value): yield prop, value - - expand_border = _border_expander() - expand_border_top = _border_expander("top") - expand_border_right = _border_expander("right") - expand_border_bottom = _border_expander("bottom") - expand_border_left = _border_expander("left") - - expand_border_color = _side_expander("border-{:s}-color") - expand_border_style = _side_expander("border-{:s}-style") - expand_border_width = _side_expander("border-{:s}-width") - expand_margin = _side_expander("margin-{:s}") - expand_padding = _side_expander("padding-{:s}") + else: + yield prop, value def parse(self, declarations_str: str): """ From 510266bdebff43ccf1efedeb552c0c3fd876d588 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 15 Jun 2022 08:44:43 -0400 Subject: [PATCH 02/14] Implement simple CSSToExcelConverter cache --- pandas/io/formats/excel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index d0fea32cafe26..9b2d3b0867520 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -3,7 +3,7 @@ """ from __future__ import annotations -from functools import reduce +from functools import lru_cache, reduce import itertools import re from typing import ( @@ -166,6 +166,7 @@ def __init__(self, inherited: str | None = None) -> None: compute_css = CSSResolver() + @lru_cache def __call__(self, declarations_str: str) -> dict[str, dict[str, str]]: """ Convert CSS declarations to ExcelWriter style. @@ -182,7 +183,6 @@ def __call__(self, declarations_str: str) -> dict[str, dict[str, str]]: A style as interpreted by ExcelWriter when found in ExcelCell.style. """ - # TODO: memoize? properties = self.compute_css(declarations_str, self.inherited) return self.build_xlstyle(properties) From 80a7f6f5d43e91de02c896a334f3c45942c461d4 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 15 Jun 2022 10:32:16 -0400 Subject: [PATCH 03/14] Eliminate list -> str -> list in CSSResolver --- pandas/io/formats/css.py | 13 +++++++------ pandas/io/formats/excel.py | 10 ++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/pandas/io/formats/css.py b/pandas/io/formats/css.py index f9d3cb09447ba..5af25d9c7859e 100644 --- a/pandas/io/formats/css.py +++ b/pandas/io/formats/css.py @@ -7,6 +7,7 @@ from typing import ( Callable, Generator, + List, ) import warnings @@ -195,7 +196,7 @@ class CSSResolver: def __call__( self, - declarations_str: str, + declarations, inherited: dict[str, str] | None = None, ) -> dict[str, str]: """ @@ -235,7 +236,7 @@ def __call__( ('font-size', '24pt'), ('font-weight', 'bold')] """ - props = dict(self.atomize(self.parse(declarations_str))) + props = dict(self.atomize(declarations)) if inherited is None: inherited = {} @@ -354,10 +355,10 @@ def _error(): def atomize(self, declarations) -> Generator[tuple[str, str], None, None]: for prop, value in declarations: - if prop in self.CSS_EXPANSIONS: - expand = self.CSS_EXPANSIONS[prop] - for prop, value in expand(self, prop, value): - yield prop, value + if prop.lower() in self.CSS_EXPANSIONS: + expand = self.CSS_EXPANSIONS[prop.lower()] + for expanded_prop, expanded_value in expand(self, prop.lower(), value.lower()): + yield expanded_prop, expanded_value else: yield prop, value diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 9b2d3b0867520..2400271bef864 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -11,6 +11,7 @@ Callable, Hashable, Iterable, + List, Mapping, Sequence, cast, @@ -85,10 +86,7 @@ def __init__( **kwargs, ) -> None: if css_styles and css_converter: - css = ";".join( - [a + ":" + str(v) for (a, v) in css_styles[css_row, css_col]] - ) - style = css_converter(css) + style = css_converter(frozenset(css_styles[css_row, css_col])) return super().__init__(row=row, col=col, val=val, style=style, **kwargs) @@ -167,7 +165,7 @@ def __init__(self, inherited: str | None = None) -> None: compute_css = CSSResolver() @lru_cache - def __call__(self, declarations_str: str) -> dict[str, dict[str, str]]: + def __call__(self, declarations) -> dict[str, dict[str, str]]: """ Convert CSS declarations to ExcelWriter style. @@ -183,7 +181,7 @@ def __call__(self, declarations_str: str) -> dict[str, dict[str, str]]: A style as interpreted by ExcelWriter when found in ExcelCell.style. """ - properties = self.compute_css(declarations_str, self.inherited) + properties = self.compute_css(declarations, self.inherited) return self.build_xlstyle(properties) def build_xlstyle(self, props: Mapping[str, str]) -> dict[str, dict[str, str]]: From c1eee92a7b1aeb260c64b8faf2a60de4c3611081 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 15 Jun 2022 11:47:07 -0400 Subject: [PATCH 04/14] Allow for resolution of duplicate properties --- pandas/io/formats/css.py | 18 +++++++++++------- pandas/io/formats/excel.py | 7 +++++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/pandas/io/formats/css.py b/pandas/io/formats/css.py index 5af25d9c7859e..b2723e1e456b5 100644 --- a/pandas/io/formats/css.py +++ b/pandas/io/formats/css.py @@ -7,7 +7,7 @@ from typing import ( Callable, Generator, - List, + Iterable, ) import warnings @@ -189,14 +189,14 @@ class CSSResolver: SIDES = ("top", "right", "bottom", "left") CSS_EXPANSIONS = { - **{"-".join(["border", prop]): _border_expander(prop) for prop in ["", "top", "right", "bottom", "left"]}, + **{"-".join(filter(None, ["border", prop])): _border_expander(prop) for prop in ["", "top", "right", "bottom", "left"]}, **{"-".join(["border", prop]): _side_expander("border-{:s}-"+prop) for prop in ["color", "style", "width"]}, **{"margin": _side_expander("margin-{:s}"), "padding": _side_expander("padding-{:s}")} } def __call__( self, - declarations, + declarations: str | frozenset[tuple[str, str]], inherited: dict[str, str] | None = None, ) -> dict[str, str]: """ @@ -236,6 +236,8 @@ def __call__( ('font-size', '24pt'), ('font-weight', 'bold')] """ + if isinstance(declarations, str): + declarations = self.parse(declarations) props = dict(self.atomize(declarations)) if inherited is None: inherited = {} @@ -353,11 +355,13 @@ def _error(): size_fmt = f"{val:f}pt" return size_fmt - def atomize(self, declarations) -> Generator[tuple[str, str], None, None]: + def atomize(self, declarations: Iterable) -> Generator[tuple[str, str], None, None]: for prop, value in declarations: - if prop.lower() in self.CSS_EXPANSIONS: - expand = self.CSS_EXPANSIONS[prop.lower()] - for expanded_prop, expanded_value in expand(self, prop.lower(), value.lower()): + prop = prop.lower() + value = value.lower() + if prop in self.CSS_EXPANSIONS: + expand = self.CSS_EXPANSIONS[prop] + for expanded_prop, expanded_value in expand(self, prop, value): yield expanded_prop, expanded_value else: yield prop, value diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 2400271bef864..61e9d939b02fb 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -86,7 +86,10 @@ def __init__( **kwargs, ) -> None: if css_styles and css_converter: - style = css_converter(frozenset(css_styles[css_row, css_col])) + # Use dict to get only one declaration per property, then convert + # to frozenset for caching + unique_declarations = frozenset(dict(css_styles[css_row, css_col]).items()) + style = css_converter(unique_declarations) return super().__init__(row=row, col=col, val=val, style=style, **kwargs) @@ -165,7 +168,7 @@ def __init__(self, inherited: str | None = None) -> None: compute_css = CSSResolver() @lru_cache - def __call__(self, declarations) -> dict[str, dict[str, str]]: + def __call__(self, declarations: str | set[tuple[str, str]]) -> dict[str, dict[str, str]]: """ Convert CSS declarations to ExcelWriter style. From 618aa4c44b307cac8f803fece77e7d6f16d99430 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 15 Jun 2022 14:28:09 -0400 Subject: [PATCH 05/14] Add performance benchmark for styled Excel --- asv_bench/benchmarks/io/excel.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/asv_bench/benchmarks/io/excel.py b/asv_bench/benchmarks/io/excel.py index a2d989e787e0f..2e7f7e51080ec 100644 --- a/asv_bench/benchmarks/io/excel.py +++ b/asv_bench/benchmarks/io/excel.py @@ -46,6 +46,24 @@ def time_write_excel(self, engine): self.df.to_excel(writer, sheet_name="Sheet1") writer.save() +class WriteExcelStyled: + params = ["openpyxl", "xlsxwriter"] + param_names = ["engine"] + + def setup(self, engine): + self.df = _generate_dataframe() + + def time_write_excel_style(self, engine): + bio = BytesIO() + bio.seek(0) + writer = ExcelWriter(bio, engine=engine) + df_style = self.df.style + df_style.applymap(lambda x: "border: red 1px solid;") + df_style.applymap(lambda x: "color: blue") + df_style.applymap(lambda x: "border-color: green black", subset=["float1"]) + df_style.to_excel(writer, sheet_name="Sheet1") + writer.save() + class ReadExcel: From b8af31723593dc6ec91e2cc046ef19e67cbe5b3b Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 15 Jun 2022 14:41:01 -0400 Subject: [PATCH 06/14] CLN: Clean up PEP8 issues --- asv_bench/benchmarks/io/excel.py | 1 + pandas/io/formats/css.py | 15 ++++++++++++--- pandas/io/formats/excel.py | 4 +++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/asv_bench/benchmarks/io/excel.py b/asv_bench/benchmarks/io/excel.py index 2e7f7e51080ec..a88c4374b7030 100644 --- a/asv_bench/benchmarks/io/excel.py +++ b/asv_bench/benchmarks/io/excel.py @@ -46,6 +46,7 @@ def time_write_excel(self, engine): self.df.to_excel(writer, sheet_name="Sheet1") writer.save() + class WriteExcelStyled: params = ["openpyxl", "xlsxwriter"] param_names = ["engine"] diff --git a/pandas/io/formats/css.py b/pandas/io/formats/css.py index b2723e1e456b5..9e7e6c760d80c 100644 --- a/pandas/io/formats/css.py +++ b/pandas/io/formats/css.py @@ -189,9 +189,18 @@ class CSSResolver: SIDES = ("top", "right", "bottom", "left") CSS_EXPANSIONS = { - **{"-".join(filter(None, ["border", prop])): _border_expander(prop) for prop in ["", "top", "right", "bottom", "left"]}, - **{"-".join(["border", prop]): _side_expander("border-{:s}-"+prop) for prop in ["color", "style", "width"]}, - **{"margin": _side_expander("margin-{:s}"), "padding": _side_expander("padding-{:s}")} + **{ + "-".join(filter(None, ["border", prop])): _border_expander(prop) + for prop in ["", "top", "right", "bottom", "left"] + }, + **{ + "-".join(["border", prop]): _side_expander("border-{:s}-" + prop) + for prop in ["color", "style", "width"] + }, + **{ + "margin": _side_expander("margin-{:s}"), + "padding": _side_expander("padding-{:s}") + } } def __call__( diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 61e9d939b02fb..4a2f6bdeb8096 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -168,7 +168,9 @@ def __init__(self, inherited: str | None = None) -> None: compute_css = CSSResolver() @lru_cache - def __call__(self, declarations: str | set[tuple[str, str]]) -> dict[str, dict[str, str]]: + def __call__( + self, declarations: str | set[tuple[str, str]] + ) -> dict[str, dict[str, str]]: """ Convert CSS declarations to ExcelWriter style. From edf0f24598789945fd2067e22444d29ad46ae291 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 15 Jun 2022 14:50:22 -0400 Subject: [PATCH 07/14] DOC: Update PR documentation --- doc/source/whatsnew/v1.5.0.rst | 1 + pandas/io/formats/css.py | 8 +++++--- pandas/io/formats/excel.py | 9 +++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 681139fb51272..dcf7a36d5dc78 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -738,6 +738,7 @@ Performance improvements - Performance improvement in :func:`factorize` (:issue:`46109`) - Performance improvement in :class:`DataFrame` and :class:`Series` constructors for extension dtype scalars (:issue:`45854`) - Performance improvement in :func:`read_excel` when ``nrows`` argument provided (:issue:`32727`) +- Performance improvement in :meth:`.Styler.to_excel` when applying repeated CSS formats (:issue:`47371`) .. --------------------------------------------------------------------------- .. _whatsnew_150.bug_fixes: diff --git a/pandas/io/formats/css.py b/pandas/io/formats/css.py index 9e7e6c760d80c..b1419f2b28020 100644 --- a/pandas/io/formats/css.py +++ b/pandas/io/formats/css.py @@ -205,7 +205,7 @@ class CSSResolver: def __call__( self, - declarations: str | frozenset[tuple[str, str]], + declarations: str | Iterable[tuple[str, str]], inherited: dict[str, str] | None = None, ) -> dict[str, str]: """ @@ -213,8 +213,10 @@ def __call__( Parameters ---------- - declarations_str : str - A list of CSS declarations + declarations_str : str | Iterable[tuple[str, str]] + A CSS string or set of CSS declaration tuples + e.g. "font-weight: bold; background: blue" or + {("font-weight", "bold"), ("background", "blue")} inherited : dict, optional Atomic properties indicating the inherited style context in which declarations_str is to be resolved. ``inherited`` should already diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 4a2f6bdeb8096..3dc1c52837276 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -169,16 +169,17 @@ def __init__(self, inherited: str | None = None) -> None: @lru_cache def __call__( - self, declarations: str | set[tuple[str, str]] + self, declarations: str | frozenset[tuple[str, str]] ) -> dict[str, dict[str, str]]: """ Convert CSS declarations to ExcelWriter style. Parameters ---------- - declarations_str : str - List of CSS declarations. - e.g. "font-weight: bold; background: blue" + declarations : str | frozenset[tuple[str, str]] + CSS string or set of CSS declaration tuples. + e.g. "font-weight: bold; background: blue" or + {("font-weight", "bold"), ("background", "blue")} Returns ------- From 80ff2b5047d3f63791b4def35a3a7fb5838f976f Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 15 Jun 2022 14:51:17 -0400 Subject: [PATCH 08/14] CLN: Clean up PEP8 issues --- pandas/io/formats/css.py | 2 +- pandas/io/formats/excel.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/css.py b/pandas/io/formats/css.py index b1419f2b28020..c92e9882688ea 100644 --- a/pandas/io/formats/css.py +++ b/pandas/io/formats/css.py @@ -215,7 +215,7 @@ def __call__( ---------- declarations_str : str | Iterable[tuple[str, str]] A CSS string or set of CSS declaration tuples - e.g. "font-weight: bold; background: blue" or + e.g. "font-weight: bold; background: blue" or {("font-weight", "bold"), ("background", "blue")} inherited : dict, optional Atomic properties indicating the inherited style context in which diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 3dc1c52837276..8a54e5959f2a7 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -178,7 +178,7 @@ def __call__( ---------- declarations : str | frozenset[tuple[str, str]] CSS string or set of CSS declaration tuples. - e.g. "font-weight: bold; background: blue" or + e.g. "font-weight: bold; background: blue" or {("font-weight", "bold"), ("background", "blue")} Returns From ac48a7b87521843f6643fe4c5f2fbe4cdf484a55 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 15 Jun 2022 20:49:56 +0000 Subject: [PATCH 09/14] Fixes from pre-commit [automated commit] --- pandas/io/formats/css.py | 7 +++---- pandas/io/formats/excel.py | 5 ++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pandas/io/formats/css.py b/pandas/io/formats/css.py index c92e9882688ea..8262654d0a3b5 100644 --- a/pandas/io/formats/css.py +++ b/pandas/io/formats/css.py @@ -199,8 +199,8 @@ class CSSResolver: }, **{ "margin": _side_expander("margin-{:s}"), - "padding": _side_expander("padding-{:s}") - } + "padding": _side_expander("padding-{:s}"), + }, } def __call__( @@ -372,8 +372,7 @@ def atomize(self, declarations: Iterable) -> Generator[tuple[str, str], None, No value = value.lower() if prop in self.CSS_EXPANSIONS: expand = self.CSS_EXPANSIONS[prop] - for expanded_prop, expanded_value in expand(self, prop, value): - yield expanded_prop, expanded_value + yield from expand(self, prop, value) else: yield prop, value diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 8a54e5959f2a7..d4e28cd6fe250 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -3,7 +3,10 @@ """ from __future__ import annotations -from functools import lru_cache, reduce +from functools import ( + lru_cache, + reduce, +) import itertools import re from typing import ( From d9bbd3aa3b9eabf9149afc0b8871b868596d6da3 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Tue, 28 Jun 2022 10:27:11 -0400 Subject: [PATCH 10/14] Make Excel CSS case-insensitive --- pandas/io/formats/excel.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index d4e28cd6fe250..953362f0a4d4b 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -89,9 +89,13 @@ def __init__( **kwargs, ) -> None: if css_styles and css_converter: - # Use dict to get only one declaration per property, then convert - # to frozenset for caching - unique_declarations = frozenset(dict(css_styles[css_row, css_col]).items()) + # Use dict to get only one (case-insensitive) declaration per property + declaration_dict = { + prop.lower(): val + for prop,val in css_styles[css_row, css_col] + } + # Convert to frozenset for order-invariant caching + unique_declarations = frozenset(declaration_dict.items()) style = css_converter(unique_declarations) return super().__init__(row=row, col=col, val=val, style=style, **kwargs) From 8600ba641956eab195cd305ffab7cb4ceb208b09 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Tue, 28 Jun 2022 10:27:33 -0400 Subject: [PATCH 11/14] Test for ordering and caching --- pandas/tests/io/formats/test_to_excel.py | 95 +++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/pandas/tests/io/formats/test_to_excel.py b/pandas/tests/io/formats/test_to_excel.py index b95a5b4365f43..a61a67414a026 100644 --- a/pandas/tests/io/formats/test_to_excel.py +++ b/pandas/tests/io/formats/test_to_excel.py @@ -2,6 +2,7 @@ ExcelFormatter is tested implicitly in pandas/tests/io/excel """ +import enum import string import pytest @@ -11,7 +12,10 @@ import pandas._testing as tm from pandas.io.formats.css import CSSWarning -from pandas.io.formats.excel import CSSToExcelConverter +from pandas.io.formats.excel import ( + CssExcelCell, + CSSToExcelConverter, +) @pytest.mark.parametrize( @@ -340,3 +344,92 @@ def test_css_named_colors_from_mpl_present(): pd_colors = CSSToExcelConverter.NAMED_COLORS for name, color in mpl_colors.items(): assert name in pd_colors and pd_colors[name] == color[1:] + +@pytest.mark.parametrize( + "styles,expected", + [ + ([("color", "green"), ("color", "red")], "color: red;"), + ([("font-weight", "bold"), ("font-weight", "normal")], "font-weight: normal;"), + ([("text-align", "center"), ("TEXT-ALIGN", "right")], "text-align: right;"), + ] +) +def test_css_excel_cell_precedence(styles,expected): + """It applies favors latter declarations over former declarations""" + # See GH 47371 + converter = CSSToExcelConverter() + converter.__call__.cache_clear() + css_styles = { + (0,0): styles + } + cell = CssExcelCell( + row = 0, + col = 0, + val = "", + style = None, + css_styles = css_styles, + css_row = 0, + css_col = 0, + css_converter = converter, + ) + converter.__call__.cache_clear() + + assert cell.style == converter(expected) + +@pytest.mark.parametrize( + "styles,cache_hits,cache_misses", + [ + ( + [ + [("color", "green"), ("color", "red"), ("color", "green")] + ], 0, 1 + ), + ( + [ + [("font-weight", "bold")], + [("font-weight", "normal"), ("font-weight", "bold")] + ], 1, 1 + ), + ( + [ + [("text-align", "center")], + [("TEXT-ALIGN", "center")] + ], 1, 1 + ), + ( + [ + [("font-weight", "bold"), ("text-align", "center")], + [("font-weight", "bold"), ("text-align", "left")], + ], 0, 2 + ), + ( + [ + [("font-weight", "bold"), ("text-align", "center")], + [("font-weight", "bold"), ("text-align", "left")], + [("font-weight", "bold"), ("text-align", "center")], + ], 1, 2 + ), + ] +) +def test_css_excel_cell_cache(styles,cache_hits,cache_misses): + """It caches unique cell styles""" + # See GH 47371 + converter = CSSToExcelConverter() + converter.__call__.cache_clear() + + css_styles = {(0,i): _style for i,_style in enumerate(styles)} + for css_row,css_col in css_styles: + cell = CssExcelCell( + row = 0, + col = 0, + val = "", + style = None, + css_styles = css_styles, + css_row = css_row, + css_col = css_col, + css_converter = converter, + ) + cache_info = converter.__call__.cache_info() + converter.__call__.cache_clear() + + assert cache_info.hits == cache_hits + assert cache_info.misses == cache_misses From 37ab4ca190bedf726bb354ee112d227af9f68671 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Tue, 28 Jun 2022 10:51:49 -0400 Subject: [PATCH 12/14] Pre-commit fixes --- pandas/io/formats/excel.py | 4 +- pandas/tests/io/formats/test_to_excel.py | 80 +++++++++++------------- 2 files changed, 39 insertions(+), 45 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 953362f0a4d4b..6fc2840b34466 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -14,7 +14,6 @@ Callable, Hashable, Iterable, - List, Mapping, Sequence, cast, @@ -91,8 +90,7 @@ def __init__( if css_styles and css_converter: # Use dict to get only one (case-insensitive) declaration per property declaration_dict = { - prop.lower(): val - for prop,val in css_styles[css_row, css_col] + prop.lower(): val for prop, val in css_styles[css_row, css_col] } # Convert to frozenset for order-invariant caching unique_declarations = frozenset(declaration_dict.items()) diff --git a/pandas/tests/io/formats/test_to_excel.py b/pandas/tests/io/formats/test_to_excel.py index a61a67414a026..b98fd74643207 100644 --- a/pandas/tests/io/formats/test_to_excel.py +++ b/pandas/tests/io/formats/test_to_excel.py @@ -2,7 +2,6 @@ ExcelFormatter is tested implicitly in pandas/tests/io/excel """ -import enum import string import pytest @@ -345,88 +344,85 @@ def test_css_named_colors_from_mpl_present(): for name, color in mpl_colors.items(): assert name in pd_colors and pd_colors[name] == color[1:] + @pytest.mark.parametrize( "styles,expected", [ ([("color", "green"), ("color", "red")], "color: red;"), ([("font-weight", "bold"), ("font-weight", "normal")], "font-weight: normal;"), ([("text-align", "center"), ("TEXT-ALIGN", "right")], "text-align: right;"), - ] + ], ) -def test_css_excel_cell_precedence(styles,expected): +def test_css_excel_cell_precedence(styles, expected): """It applies favors latter declarations over former declarations""" # See GH 47371 converter = CSSToExcelConverter() converter.__call__.cache_clear() - css_styles = { - (0,0): styles - } + css_styles = {(0, 0): styles} cell = CssExcelCell( - row = 0, - col = 0, - val = "", - style = None, - css_styles = css_styles, - css_row = 0, - css_col = 0, - css_converter = converter, + row=0, + col=0, + val="", + style=None, + css_styles=css_styles, + css_row=0, + css_col=0, + css_converter=converter, ) converter.__call__.cache_clear() assert cell.style == converter(expected) + @pytest.mark.parametrize( "styles,cache_hits,cache_misses", [ - ( - [ - [("color", "green"), ("color", "red"), ("color", "green")] - ], 0, 1 - ), + ([[("color", "green"), ("color", "red"), ("color", "green")]], 0, 1), ( [ [("font-weight", "bold")], - [("font-weight", "normal"), ("font-weight", "bold")] - ], 1, 1 - ), - ( - [ - [("text-align", "center")], - [("TEXT-ALIGN", "center")] - ], 1, 1 + [("font-weight", "normal"), ("font-weight", "bold")], + ], + 1, + 1, ), + ([[("text-align", "center")], [("TEXT-ALIGN", "center")]], 1, 1), ( [ [("font-weight", "bold"), ("text-align", "center")], [("font-weight", "bold"), ("text-align", "left")], - ], 0, 2 + ], + 0, + 2, ), ( [ [("font-weight", "bold"), ("text-align", "center")], [("font-weight", "bold"), ("text-align", "left")], [("font-weight", "bold"), ("text-align", "center")], - ], 1, 2 + ], + 1, + 2, ), - ] + ], ) -def test_css_excel_cell_cache(styles,cache_hits,cache_misses): +def test_css_excel_cell_cache(styles, cache_hits, cache_misses): """It caches unique cell styles""" # See GH 47371 converter = CSSToExcelConverter() converter.__call__.cache_clear() - css_styles = {(0,i): _style for i,_style in enumerate(styles)} - for css_row,css_col in css_styles: - cell = CssExcelCell( - row = 0, - col = 0, - val = "", - style = None, - css_styles = css_styles, - css_row = css_row, - css_col = css_col, - css_converter = converter, + css_styles = {(0, i): _style for i, _style in enumerate(styles)} + for css_row, css_col in css_styles: + CssExcelCell( + row=0, + col=0, + val="", + style=None, + css_styles=css_styles, + css_row=css_row, + css_col=css_col, + css_converter=converter, ) cache_info = converter.__call__.cache_info() converter.__call__.cache_clear() From 8e56402ed0639c231c98d06c25c62ee3da6f4524 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Tue, 28 Jun 2022 11:50:11 -0400 Subject: [PATCH 13/14] Remove built-in filter --- pandas/io/formats/css.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/css.py b/pandas/io/formats/css.py index 214d25c9a2a8f..92dafffc9c3de 100644 --- a/pandas/io/formats/css.py +++ b/pandas/io/formats/css.py @@ -191,7 +191,7 @@ class CSSResolver: CSS_EXPANSIONS = { **{ - "-".join(filter(None, ["border", prop])): _border_expander(prop) + "-".join(["border", prop] if prop else ["border"]): _border_expander(prop) for prop in ["", "top", "right", "bottom", "left"] }, **{ From 06489882fa827c2e989b65c668c7eb8e40cb5a9d Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 29 Jun 2022 10:17:38 -0400 Subject: [PATCH 14/14] Increase maxsize of Excel cache --- pandas/io/formats/excel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index d2d60f86777a5..811b079c3c693 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -172,7 +172,7 @@ def __init__(self, inherited: str | None = None) -> None: compute_css = CSSResolver() - @lru_cache + @lru_cache(maxsize=None) def __call__( self, declarations: str | frozenset[tuple[str, str]] ) -> dict[str, dict[str, str]]: