From e3f2ea8f0a0c0f9ff7862c6e7a0d1f8998d467ef Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Tue, 20 Sep 2022 11:30:34 -0400 Subject: [PATCH 01/22] Add tests --- pandas/tests/io/excel/test_style.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pandas/tests/io/excel/test_style.py b/pandas/tests/io/excel/test_style.py index 00f6ccb96a905..b7e18871496a7 100644 --- a/pandas/tests/io/excel/test_style.py +++ b/pandas/tests/io/excel/test_style.py @@ -115,6 +115,19 @@ def test_styler_to_excel_unstyled(engine): ["border", "left", "color", "rgb"], {"xlsxwriter": "FF111222", "openpyxl": "00111222"}, ), + # Border styles + ( + "border-left-style: hair; border-left-color: black", + ["border", "left", "style"], + "hair" + ), + ("border-left-style: hair;", ["border", "left", "style"], "hair"), + # CSS should default to black if style provided w/o color + ( + "border-left-style: hair;", + ["border", "left", "color", "rgb"], + {"xlsxwriter": "FF000000", "openpyxl": "00000000"}, + ), ] From 16577355216a195c3dc2ba75177cc1b1a63161e4 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Tue, 20 Sep 2022 11:30:48 -0400 Subject: [PATCH 02/22] Add support for "hair" style --- pandas/io/formats/excel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index c4ddac088d901..754a25cb61c2a 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -289,6 +289,8 @@ def _border_style(self, style: str | None, width: str | None, color: str | None) # not handled return width_name + if style == "hair": + return "hair" if style == "double": return "double" if style == "dotted": From 16258a97a6807d107e835a12fa136b0ee9fb3f22 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Tue, 20 Sep 2022 11:39:15 -0400 Subject: [PATCH 03/22] Support excel-specific border styles --- pandas/io/formats/excel.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 754a25cb61c2a..fc5923db588c2 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -289,8 +289,6 @@ def _border_style(self, style: str | None, width: str | None, color: str | None) # not handled return width_name - if style == "hair": - return "hair" if style == "double": return "double" if style == "dotted": @@ -301,6 +299,19 @@ def _border_style(self, style: str | None, width: str | None, color: str | None) if width_name in ("hair", "thin"): return "dashed" return "mediumDashed" + elif style in ( + "hair", "mediumDashDot", "dashDotDot", "mediumDashDotDot", + "dashDot", "slandDashDot", "mediumDashed" + ): + # Excel-specific styles + return style + else: + warnings.warn( + f"Unhandled border style format: {repr(style)}", + CSSWarning, + stacklevel=find_stack_level(inspect.currentframe()), + ) + return "none" def _get_width_name(self, width_input: str | None) -> str | None: width = self._width_to_float(width_input) From 6626a974b915181581807dc6d305e59c123d9b71 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Tue, 20 Sep 2022 11:47:49 -0400 Subject: [PATCH 04/22] Use black border if styled but color unspecified --- pandas/io/formats/excel.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index fc5923db588c2..72cb970dd23ba 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -243,17 +243,21 @@ def _get_is_wrap_text(self, props: Mapping[str, str]) -> bool | None: def build_border( self, props: Mapping[str, str] ) -> dict[str, dict[str, str | None]]: - return { - side: { - "style": self._border_style( - props.get(f"border-{side}-style"), - props.get(f"border-{side}-width"), - self.color_to_excel(props.get(f"border-{side}-color")), - ), - "color": self.color_to_excel(props.get(f"border-{side}-color")), + border_dict = {} + for side in ["top", "right", "bottom", "left"]: + style = self._border_style( + props.get(f"border-{side}-style"), + props.get(f"border-{side}-width"), + self.color_to_excel(props.get(f"border-{side}-color")), + ) + color = self.color_to_excel(props.get(f"border-{side}-color")) + if style and style != "none" and color is None: + color = self.color_to_excel("black") + border_dict[side] = { + "style": style, + "color": color } - for side in ["top", "right", "bottom", "left"] - } + return border_dict def _border_style(self, style: str | None, width: str | None, color: str | None): # convert styles and widths to openxml, one of: From 69d75facf3528572f4a0e6c86ea18386c83c9b5b Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Tue, 20 Sep 2022 16:04:58 +0000 Subject: [PATCH 05/22] Added documentation and whatsnew --- doc/source/user_guide/style.ipynb | 12 +++++++++--- doc/source/whatsnew/v1.5.1.rst | 3 ++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 43021fcbc13fb..956664b87e76c 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -1594,8 +1594,9 @@ "\n", "\n", "- Only CSS2 named colors and hex colors of the form `#rgb` or `#rrggbb` are currently supported.\n", - "- The following pseudo CSS properties are also available to set excel specific style properties:\n", + "- The following pseudo CSS properties are also available to set Excel specific style properties:\n", " - `number-format`\n", + " - `border-style` (for Excel-specific styles: \"hair\", \"mediumDashDot\", \"dashDotDot\", \"mediumDashDotDot\", \"dashDot\", \"slandDashDot\", or \"mediumDashed\")\n", "\n", "Table level styles, and data cell CSS-classes are not included in the export to Excel: individual cells must have their properties mapped by the `Styler.apply` and/or `Styler.applymap` methods." ] @@ -2026,7 +2027,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3.10.4 ('pandas-testing')", "language": "python", "name": "python3" }, @@ -2040,7 +2041,12 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.5" + "version": "3.10.4" + }, + "vscode": { + "interpreter": { + "hash": "99ba7404bdbf380b6668594445346cc49dc5e126b0bc53947f71689e253b4235" + } } }, "nbformat": 4, diff --git a/doc/source/whatsnew/v1.5.1.rst b/doc/source/whatsnew/v1.5.1.rst index f8069b5476d9e..a5c55717cde47 100644 --- a/doc/source/whatsnew/v1.5.1.rst +++ b/doc/source/whatsnew/v1.5.1.rst @@ -23,7 +23,8 @@ Fixed regressions Bug fixes ~~~~~~~~~ -- +- Bug in :class:`CSSToExcelConverter` leading to error when unrecognized ``border-style`` (including ``"hair"``) provided to Excel writers (:issue:`48649`) +- Bug in :class:`CSSToExcelConverter` where border would not fall back on black when border style or width provided without a color (:issue:`48649`) - .. --------------------------------------------------------------------------- From 25ad97049a14394cc56f8736025c61eb8407a945 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Tue, 20 Sep 2022 16:06:51 +0000 Subject: [PATCH 06/22] Black linter --- pandas/io/formats/excel.py | 14 ++++++++------ pandas/tests/io/excel/test_style.py | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 72cb970dd23ba..32219f9efe6c3 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -253,10 +253,7 @@ def build_border( color = self.color_to_excel(props.get(f"border-{side}-color")) if style and style != "none" and color is None: color = self.color_to_excel("black") - border_dict[side] = { - "style": style, - "color": color - } + border_dict[side] = {"style": style, "color": color} return border_dict def _border_style(self, style: str | None, width: str | None, color: str | None): @@ -304,8 +301,13 @@ def _border_style(self, style: str | None, width: str | None, color: str | None) return "dashed" return "mediumDashed" elif style in ( - "hair", "mediumDashDot", "dashDotDot", "mediumDashDotDot", - "dashDot", "slandDashDot", "mediumDashed" + "hair", + "mediumDashDot", + "dashDotDot", + "mediumDashDotDot", + "dashDot", + "slandDashDot", + "mediumDashed", ): # Excel-specific styles return style diff --git a/pandas/tests/io/excel/test_style.py b/pandas/tests/io/excel/test_style.py index b7e18871496a7..6d1cc9825f721 100644 --- a/pandas/tests/io/excel/test_style.py +++ b/pandas/tests/io/excel/test_style.py @@ -119,7 +119,7 @@ def test_styler_to_excel_unstyled(engine): ( "border-left-style: hair; border-left-color: black", ["border", "left", "style"], - "hair" + "hair", ), ("border-left-style: hair;", ["border", "left", "style"], "hair"), # CSS should default to black if style provided w/o color From d2680ef9a4398f676a11526cfe0d3778664e99fb Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Tue, 20 Sep 2022 17:42:10 +0000 Subject: [PATCH 07/22] Fixed tests for Excel border color fallback --- pandas/tests/io/formats/test_to_excel.py | 57 ++++++++++++++---------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/pandas/tests/io/formats/test_to_excel.py b/pandas/tests/io/formats/test_to_excel.py index 7481baaee94f6..b90e202907c2a 100644 --- a/pandas/tests/io/formats/test_to_excel.py +++ b/pandas/tests/io/formats/test_to_excel.py @@ -113,10 +113,10 @@ "border-style: solid", { "border": { - "top": {"style": "medium"}, - "bottom": {"style": "medium"}, - "left": {"style": "medium"}, - "right": {"style": "medium"}, + "top": {"style": "medium", "color": "000000"}, + "bottom": {"style": "medium", "color": "000000"}, + "left": {"style": "medium", "color": "000000"}, + "right": {"style": "medium", "color": "000000"}, } }, ), @@ -124,52 +124,61 @@ "border-style: solid; border-width: thin", { "border": { - "top": {"style": "thin"}, - "bottom": {"style": "thin"}, - "left": {"style": "thin"}, - "right": {"style": "thin"}, + "top": {"style": "thin", "color": "000000"}, + "bottom": {"style": "thin", "color": "000000"}, + "left": {"style": "thin", "color": "000000"}, + "right": {"style": "thin", "color": "000000"}, } }, ), ( "border-top-style: solid; border-top-width: thin", - {"border": {"top": {"style": "thin"}}}, + {"border": {"top": {"style": "thin", "color": "000000"}}}, ), ( "border-top-style: solid; border-top-width: 1pt", - {"border": {"top": {"style": "thin"}}}, + {"border": {"top": {"style": "thin", "color": "000000"}}}, + ), + ( + "border-top-style: solid", + {"border": {"top": {"style": "medium", "color": "000000"}}}, ), - ("border-top-style: solid", {"border": {"top": {"style": "medium"}}}), ( "border-top-style: solid; border-top-width: medium", - {"border": {"top": {"style": "medium"}}}, + {"border": {"top": {"style": "medium", "color": "000000"}}}, ), ( "border-top-style: solid; border-top-width: 2pt", - {"border": {"top": {"style": "medium"}}}, + {"border": {"top": {"style": "medium", "color": "000000"}}}, ), ( "border-top-style: solid; border-top-width: thick", - {"border": {"top": {"style": "thick"}}}, + {"border": {"top": {"style": "thick", "color": "000000"}}}, ), ( "border-top-style: solid; border-top-width: 4pt", - {"border": {"top": {"style": "thick"}}}, + {"border": {"top": {"style": "thick", "color": "000000"}}}, ), ( "border-top-style: dotted", - {"border": {"top": {"style": "mediumDashDotDot"}}}, + {"border": {"top": {"style": "mediumDashDotDot", "color": "000000"}}}, ), ( "border-top-style: dotted; border-top-width: thin", - {"border": {"top": {"style": "dotted"}}}, + {"border": {"top": {"style": "dotted", "color": "000000"}}}, + ), + ( + "border-top-style: dashed", + {"border": {"top": {"style": "mediumDashed", "color": "000000"}}}, ), - ("border-top-style: dashed", {"border": {"top": {"style": "mediumDashed"}}}), ( "border-top-style: dashed; border-top-width: thin", - {"border": {"top": {"style": "dashed"}}}, + {"border": {"top": {"style": "dashed", "color": "000000"}}}, + ), + ( + "border-top-style: double", + {"border": {"top": {"style": "double", "color": "000000"}}}, ), - ("border-top-style: double", {"border": {"top": {"style": "double"}}}), # - color ( "border-style: solid; border-color: #0000ff", @@ -240,10 +249,10 @@ def test_css_to_excel_multiple(): assert { "font": {"bold": True, "underline": "single", "color": "FF0000"}, "border": { - "top": {"style": "thin"}, - "right": {"style": "thin"}, - "bottom": {"style": "thin"}, - "left": {"style": "thin"}, + "top": {"style": "thin", "color": "000000"}, + "right": {"style": "thin", "color": "000000"}, + "bottom": {"style": "thin", "color": "000000"}, + "left": {"style": "thin", "color": "000000"}, }, "alignment": {"horizontal": "center", "vertical": "top"}, } == actual From ec3e1373558bf2dcec3f372225d6b29930c77036 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Thu, 22 Sep 2022 12:32:44 +0000 Subject: [PATCH 08/22] Revert style.ipynb metadata --- doc/source/user_guide/style.ipynb | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 956664b87e76c..07c3299a30fb3 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -2027,7 +2027,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3.10.4 ('pandas-testing')", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -2041,12 +2041,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" - }, - "vscode": { - "interpreter": { - "hash": "99ba7404bdbf380b6668594445346cc49dc5e126b0bc53947f71689e253b4235" - } + "version": "3.9.5" } }, "nbformat": 4, From fb0f81c0367cf8544bbca2a65f131f84e0d9bdec Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Thu, 22 Sep 2022 12:49:49 +0000 Subject: [PATCH 09/22] Fix typo --- 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 32219f9efe6c3..20e1838081ed3 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -306,7 +306,7 @@ def _border_style(self, style: str | None, width: str | None, color: str | None) "dashDotDot", "mediumDashDotDot", "dashDot", - "slandDashDot", + "slantDashDot", "mediumDashed", ): # Excel-specific styles From cb8068a9d0636448a9c303144c7ed741dc9e32dc Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Mon, 26 Sep 2022 12:25:37 +0000 Subject: [PATCH 10/22] Revert border-color default value --- pandas/io/formats/excel.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 20e1838081ed3..ebfa929cbd740 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -243,18 +243,17 @@ def _get_is_wrap_text(self, props: Mapping[str, str]) -> bool | None: def build_border( self, props: Mapping[str, str] ) -> dict[str, dict[str, str | None]]: - border_dict = {} - for side in ["top", "right", "bottom", "left"]: - style = self._border_style( - props.get(f"border-{side}-style"), - props.get(f"border-{side}-width"), - self.color_to_excel(props.get(f"border-{side}-color")), - ) - color = self.color_to_excel(props.get(f"border-{side}-color")) - if style and style != "none" and color is None: - color = self.color_to_excel("black") - border_dict[side] = {"style": style, "color": color} - return border_dict + return { + side: { + "style": self._border_style( + props.get(f"border-{side}-style"), + props.get(f"border-{side}-width"), + self.color_to_excel(props.get(f"border-{side}-color")), + ), + "color": self.color_to_excel(props.get(f"border-{side}-color")), + } + for side in ["top", "right", "bottom", "left"] + } def _border_style(self, style: str | None, width: str | None, color: str | None): # convert styles and widths to openxml, one of: From d169d3a46f6577971bde1deeb3a27496f046e173 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Mon, 26 Sep 2022 08:31:12 -0400 Subject: [PATCH 11/22] Revert "Fixed tests for Excel border color fallback" This reverts commit d2680ef9a4398f676a11526cfe0d3778664e99fb. --- pandas/tests/io/formats/test_to_excel.py | 57 ++++++++++-------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/pandas/tests/io/formats/test_to_excel.py b/pandas/tests/io/formats/test_to_excel.py index b90e202907c2a..7481baaee94f6 100644 --- a/pandas/tests/io/formats/test_to_excel.py +++ b/pandas/tests/io/formats/test_to_excel.py @@ -113,10 +113,10 @@ "border-style: solid", { "border": { - "top": {"style": "medium", "color": "000000"}, - "bottom": {"style": "medium", "color": "000000"}, - "left": {"style": "medium", "color": "000000"}, - "right": {"style": "medium", "color": "000000"}, + "top": {"style": "medium"}, + "bottom": {"style": "medium"}, + "left": {"style": "medium"}, + "right": {"style": "medium"}, } }, ), @@ -124,61 +124,52 @@ "border-style: solid; border-width: thin", { "border": { - "top": {"style": "thin", "color": "000000"}, - "bottom": {"style": "thin", "color": "000000"}, - "left": {"style": "thin", "color": "000000"}, - "right": {"style": "thin", "color": "000000"}, + "top": {"style": "thin"}, + "bottom": {"style": "thin"}, + "left": {"style": "thin"}, + "right": {"style": "thin"}, } }, ), ( "border-top-style: solid; border-top-width: thin", - {"border": {"top": {"style": "thin", "color": "000000"}}}, + {"border": {"top": {"style": "thin"}}}, ), ( "border-top-style: solid; border-top-width: 1pt", - {"border": {"top": {"style": "thin", "color": "000000"}}}, - ), - ( - "border-top-style: solid", - {"border": {"top": {"style": "medium", "color": "000000"}}}, + {"border": {"top": {"style": "thin"}}}, ), + ("border-top-style: solid", {"border": {"top": {"style": "medium"}}}), ( "border-top-style: solid; border-top-width: medium", - {"border": {"top": {"style": "medium", "color": "000000"}}}, + {"border": {"top": {"style": "medium"}}}, ), ( "border-top-style: solid; border-top-width: 2pt", - {"border": {"top": {"style": "medium", "color": "000000"}}}, + {"border": {"top": {"style": "medium"}}}, ), ( "border-top-style: solid; border-top-width: thick", - {"border": {"top": {"style": "thick", "color": "000000"}}}, + {"border": {"top": {"style": "thick"}}}, ), ( "border-top-style: solid; border-top-width: 4pt", - {"border": {"top": {"style": "thick", "color": "000000"}}}, + {"border": {"top": {"style": "thick"}}}, ), ( "border-top-style: dotted", - {"border": {"top": {"style": "mediumDashDotDot", "color": "000000"}}}, + {"border": {"top": {"style": "mediumDashDotDot"}}}, ), ( "border-top-style: dotted; border-top-width: thin", - {"border": {"top": {"style": "dotted", "color": "000000"}}}, - ), - ( - "border-top-style: dashed", - {"border": {"top": {"style": "mediumDashed", "color": "000000"}}}, + {"border": {"top": {"style": "dotted"}}}, ), + ("border-top-style: dashed", {"border": {"top": {"style": "mediumDashed"}}}), ( "border-top-style: dashed; border-top-width: thin", - {"border": {"top": {"style": "dashed", "color": "000000"}}}, - ), - ( - "border-top-style: double", - {"border": {"top": {"style": "double", "color": "000000"}}}, + {"border": {"top": {"style": "dashed"}}}, ), + ("border-top-style: double", {"border": {"top": {"style": "double"}}}), # - color ( "border-style: solid; border-color: #0000ff", @@ -249,10 +240,10 @@ def test_css_to_excel_multiple(): assert { "font": {"bold": True, "underline": "single", "color": "FF0000"}, "border": { - "top": {"style": "thin", "color": "000000"}, - "right": {"style": "thin", "color": "000000"}, - "bottom": {"style": "thin", "color": "000000"}, - "left": {"style": "thin", "color": "000000"}, + "top": {"style": "thin"}, + "right": {"style": "thin"}, + "bottom": {"style": "thin"}, + "left": {"style": "thin"}, }, "alignment": {"horizontal": "center", "vertical": "top"}, } == actual From e6327198c80e8cac40e339283762db2165ae2df7 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Tue, 27 Sep 2022 12:13:10 +0000 Subject: [PATCH 12/22] Revert border color default tests --- pandas/tests/io/excel/test_style.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pandas/tests/io/excel/test_style.py b/pandas/tests/io/excel/test_style.py index 6d1cc9825f721..a2cfd693edc4e 100644 --- a/pandas/tests/io/excel/test_style.py +++ b/pandas/tests/io/excel/test_style.py @@ -121,13 +121,6 @@ def test_styler_to_excel_unstyled(engine): ["border", "left", "style"], "hair", ), - ("border-left-style: hair;", ["border", "left", "style"], "hair"), - # CSS should default to black if style provided w/o color - ( - "border-left-style: hair;", - ["border", "left", "color", "rgb"], - {"xlsxwriter": "FF000000", "openpyxl": "00000000"}, - ), ] From 222cf3af8459bd45c9bb0eb1b41984f80b54a44a Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Tue, 27 Sep 2022 12:15:58 +0000 Subject: [PATCH 13/22] Updated whatsnew entry --- doc/source/whatsnew/v1.5.1.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v1.5.1.rst b/doc/source/whatsnew/v1.5.1.rst index a5c55717cde47..c268e6af05239 100644 --- a/doc/source/whatsnew/v1.5.1.rst +++ b/doc/source/whatsnew/v1.5.1.rst @@ -23,8 +23,7 @@ Fixed regressions Bug fixes ~~~~~~~~~ -- Bug in :class:`CSSToExcelConverter` leading to error when unrecognized ``border-style`` (including ``"hair"``) provided to Excel writers (:issue:`48649`) -- Bug in :class:`CSSToExcelConverter` where border would not fall back on black when border style or width provided without a color (:issue:`48649`) +- Bug when writing to Excel leading to error when unrecognized ``border-style`` (including ``"hair"``) provided to Excel writers (:issue:`48649`) - .. --------------------------------------------------------------------------- From 3e65ce76189550a041174ac1caf3776ab08445f0 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 28 Sep 2022 12:56:10 +0000 Subject: [PATCH 14/22] Add method link to whatsnew --- doc/source/whatsnew/v1.5.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.5.1.rst b/doc/source/whatsnew/v1.5.1.rst index b35f047235e80..9f61a1b9ff6f5 100644 --- a/doc/source/whatsnew/v1.5.1.rst +++ b/doc/source/whatsnew/v1.5.1.rst @@ -89,7 +89,7 @@ Bug fixes - Bug in :func:`assert_index_equal` for extension arrays with non matching ``NA`` raising ``ValueError`` (:issue:`48608`) - Bug in :meth:`DataFrame.pivot_table` raising unexpected ``FutureWarning`` when setting datetime column as index (:issue:`48683`) - Bug in :meth:`DataFrame.sort_values` emitting unnecessary ``FutureWarning`` when called on :class:`DataFrame` with boolean sparse columns (:issue:`48784`) -- Bug when writing to Excel leading to error when unrecognized ``border-style`` (including ``"hair"``) provided to Excel writers (:issue:`48649`) +- Bug in :meth:`.Styler.to_excel` leading to error when unrecognized ``border-style`` (including ``"hair"``) provided to Excel writers (:issue:`48649`) - .. --------------------------------------------------------------------------- From e352a1ca58bf7df7a8a57182038ed857b7be3b3e Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 28 Sep 2022 19:06:52 +0000 Subject: [PATCH 15/22] Add tests and fix case sensitivity --- pandas/io/formats/excel.py | 27 ++++++++----- pandas/tests/io/excel/test_style.py | 60 +++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index ebfa929cbd740..fd0adb6e84b67 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -160,6 +160,21 @@ class CSSToExcelConverter: "fantasy": 5, # decorative } + BORDER_STYLE_MAP = { + style.lower(): style for style in [ + "dashed", + "mediumDashDot", + "dashDotDot", + "hair", + "dotted", + "mediumDashDotDot", + "double", + "dashDot", + "slantDashDot", + "mediumDashed", + ] + } + # NB: Most of the methods here could be classmethods, as only __init__ # and __call__ make use of instance attributes. We leave them as # instancemethods so that users can easily experiment with extensions @@ -299,17 +314,9 @@ def _border_style(self, style: str | None, width: str | None, color: str | None) if width_name in ("hair", "thin"): return "dashed" return "mediumDashed" - elif style in ( - "hair", - "mediumDashDot", - "dashDotDot", - "mediumDashDotDot", - "dashDot", - "slantDashDot", - "mediumDashed", - ): + elif style in self.BORDER_STYLE_MAP: # Excel-specific styles - return style + return self.BORDER_STYLE_MAP[style] else: warnings.warn( f"Unhandled border style format: {repr(style)}", diff --git a/pandas/tests/io/excel/test_style.py b/pandas/tests/io/excel/test_style.py index a2cfd693edc4e..bfd4ba239222d 100644 --- a/pandas/tests/io/excel/test_style.py +++ b/pandas/tests/io/excel/test_style.py @@ -202,6 +202,66 @@ def test_styler_to_excel_basic_indexes(engine, css, attrs, expected): assert sc_cell == expected +# From https://openpyxl.readthedocs.io/en/stable/api/openpyxl.styles.borders.html +# Note: Leaving behavior of "width"-type styles undefined; user should use border-width +# instead +excel_border_styles = [ + # "thin", + "dashed", + "mediumDashDot", + "dashDotDot", + "hair", + "dotted", + "mediumDashDotDot", + # "medium", + "double", + "dashDot", + "slantDashDot", + # "thick", + "mediumDashed", +] + + +@pytest.mark.parametrize( + "engine", + ["xlsxwriter", "openpyxl"], +) +@pytest.mark.parametrize("border_style", excel_border_styles) +def test_styler_to_excel_border_style(engine, border_style): + css = "; ".join(( + f"border-left-style: {border_style}", + "border-left-color: black", + "border-left-width: thin" + )) + attrs = ["border", "left", "style"] + expected = border_style + + pytest.importorskip(engine) + df = DataFrame(np.random.randn(1, 1)) + styler = df.style.applymap(lambda x: css) + + with tm.ensure_clean(".xlsx") as path: + with ExcelWriter(path, engine=engine) as writer: + df.to_excel(writer, sheet_name="dataframe") + styler.to_excel(writer, sheet_name="styled") + + openpyxl = pytest.importorskip("openpyxl") # test loading only with openpyxl + with contextlib.closing(openpyxl.load_workbook(path)) as wb: + + # test unstyled data cell does not have expected styles + # test styled cell has expected styles + u_cell, s_cell = wb["dataframe"].cell(2, 2), wb["styled"].cell(2, 2) + for attr in attrs: + u_cell, s_cell = getattr(u_cell, attr, None), getattr(s_cell, attr) + + if isinstance(expected, dict): + assert u_cell is None or u_cell != expected[engine] + assert s_cell == expected[engine] + else: + assert u_cell is None or u_cell != expected + assert s_cell == expected + + def test_styler_custom_converter(): openpyxl = pytest.importorskip("openpyxl") From d9fad6d29f425984169fd93663501166cbc45808 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 28 Sep 2022 19:14:05 +0000 Subject: [PATCH 16/22] Apply black linter --- pandas/io/formats/excel.py | 3 ++- pandas/tests/io/excel/test_style.py | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index fd0adb6e84b67..9537102e1d140 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -161,7 +161,8 @@ class CSSToExcelConverter: } BORDER_STYLE_MAP = { - style.lower(): style for style in [ + style.lower(): style + for style in [ "dashed", "mediumDashDot", "dashDotDot", diff --git a/pandas/tests/io/excel/test_style.py b/pandas/tests/io/excel/test_style.py index bfd4ba239222d..686c822a5f326 100644 --- a/pandas/tests/io/excel/test_style.py +++ b/pandas/tests/io/excel/test_style.py @@ -203,7 +203,7 @@ def test_styler_to_excel_basic_indexes(engine, css, attrs, expected): # From https://openpyxl.readthedocs.io/en/stable/api/openpyxl.styles.borders.html -# Note: Leaving behavior of "width"-type styles undefined; user should use border-width +# Note: Leaving behavior of "width"-type styles undefined; user should use border-width # instead excel_border_styles = [ # "thin", @@ -228,11 +228,13 @@ def test_styler_to_excel_basic_indexes(engine, css, attrs, expected): ) @pytest.mark.parametrize("border_style", excel_border_styles) def test_styler_to_excel_border_style(engine, border_style): - css = "; ".join(( - f"border-left-style: {border_style}", - "border-left-color: black", - "border-left-width: thin" - )) + css = "; ".join( + ( + f"border-left-style: {border_style}", + "border-left-color: black", + "border-left-width: thin", + ) + ) attrs = ["border", "left", "style"] expected = border_style From a6abe184aa4dc3ea2aa767962e34bbed8e26cb82 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 2 Nov 2022 11:56:13 -0400 Subject: [PATCH 17/22] Update v1.5.2.rst --- doc/source/whatsnew/v1.5.2.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.5.2.rst b/doc/source/whatsnew/v1.5.2.rst index e65be3bcecd76..11b82b10f390b 100644 --- a/doc/source/whatsnew/v1.5.2.rst +++ b/doc/source/whatsnew/v1.5.2.rst @@ -24,7 +24,8 @@ Fixed regressions Bug fixes ~~~~~~~~~ - Bug in the Copy-on-Write implementation losing track of views in certain chained indexing cases (:issue:`48996`) -- +- Bug in :meth:`.Styler.to_excel` leading to error when unrecognized ``border-style`` (e.g. ``"hair"``) provided to Excel writers (:issue:`48649`) + .. --------------------------------------------------------------------------- .. _whatsnew_152.other: From 0ae7634e3265768e0ff51e86ad024300ef1c1f54 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Thu, 10 Nov 2022 14:00:42 -0500 Subject: [PATCH 18/22] Fix documentation typo --- doc/source/user_guide/style.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 07c3299a30fb3..620e3806a33b5 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -1596,7 +1596,7 @@ "- Only CSS2 named colors and hex colors of the form `#rgb` or `#rrggbb` are currently supported.\n", "- The following pseudo CSS properties are also available to set Excel specific style properties:\n", " - `number-format`\n", - " - `border-style` (for Excel-specific styles: \"hair\", \"mediumDashDot\", \"dashDotDot\", \"mediumDashDotDot\", \"dashDot\", \"slandDashDot\", or \"mediumDashed\")\n", + " - `border-style` (for Excel-specific styles: \"hair\", \"mediumDashDot\", \"dashDotDot\", \"mediumDashDotDot\", \"dashDot\", \"slantDashDot\", or \"mediumDashed\")\n", "\n", "Table level styles, and data cell CSS-classes are not included in the export to Excel: individual cells must have their properties mapped by the `Styler.apply` and/or `Styler.applymap` methods." ] From 69366ae6733de88a12da3f2f07daeadf6ded6e81 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Thu, 10 Nov 2022 14:14:51 -0500 Subject: [PATCH 19/22] Test that border shorthand works --- pandas/tests/io/excel/test_style.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pandas/tests/io/excel/test_style.py b/pandas/tests/io/excel/test_style.py index 686c822a5f326..f26df440d263b 100644 --- a/pandas/tests/io/excel/test_style.py +++ b/pandas/tests/io/excel/test_style.py @@ -228,13 +228,7 @@ def test_styler_to_excel_basic_indexes(engine, css, attrs, expected): ) @pytest.mark.parametrize("border_style", excel_border_styles) def test_styler_to_excel_border_style(engine, border_style): - css = "; ".join( - ( - f"border-left-style: {border_style}", - "border-left-color: black", - "border-left-width: thin", - ) - ) + css = f"border-left: {border_style} black thin" attrs = ["border", "left", "style"] expected = border_style From ba774f774aba6651f1901b8b83aa3710bc827193 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Thu, 10 Nov 2022 14:16:12 -0500 Subject: [PATCH 20/22] Append Excel styles to shorthand parsing --- pandas/io/formats/css.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/css.py b/pandas/io/formats/css.py index 4b5b178445c38..f2f808a6e2081 100644 --- a/pandas/io/formats/css.py +++ b/pandas/io/formats/css.py @@ -105,9 +105,9 @@ def expand(self, prop, value: str) -> Generator[tuple[str, str], None, None]: f"border{side}-width": "medium", } for token in tokens: - if token in self.BORDER_STYLES: + if token.lower() in self.BORDER_STYLES: border_declarations[f"border{side}-style"] = token - elif any(ratio in token for ratio in self.BORDER_WIDTH_RATIOS): + elif any(ratio in token.lower() for ratio in self.BORDER_WIDTH_RATIOS): border_declarations[f"border{side}-width"] = token else: border_declarations[f"border{side}-color"] = token @@ -181,6 +181,13 @@ class CSSResolver: "ridge", "inset", "outset", + "mediumdashdot", + "dashdotdot", + "hair", + "mediumdashdotdot", + "dashdot", + "slantdashdot", + "mediumdashed", ] SIDE_SHORTHANDS = { From 97fd1c17f575abf6422a250d6772299d05cc749a Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Thu, 10 Nov 2022 14:22:40 -0500 Subject: [PATCH 21/22] Update to find_stack_level call --- 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 acabbf9c0a4bf..2a01f694486c6 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -321,7 +321,7 @@ def _border_style(self, style: str | None, width: str | None, color: str | None) warnings.warn( f"Unhandled border style format: {repr(style)}", CSSWarning, - stacklevel=find_stack_level(inspect.currentframe()), + stacklevel=find_stack_level(), ) return "none" From b6f62120deabbd69ad1300156377d7c1d6a89ecc Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Wed, 23 Nov 2022 09:58:56 -0500 Subject: [PATCH 22/22] Update v1.5.3.rst --- doc/source/whatsnew/v1.5.3.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.5.3.rst b/doc/source/whatsnew/v1.5.3.rst index d35d3bf8b89ca..406462d1b5a50 100644 --- a/doc/source/whatsnew/v1.5.3.rst +++ b/doc/source/whatsnew/v1.5.3.rst @@ -21,7 +21,7 @@ Fixed regressions Bug fixes ~~~~~~~~~ -- +- Bug in :meth:`.Styler.to_excel` leading to error when unrecognized ``border-style`` (e.g. ``"hair"``) provided to Excel writers (:issue:`48649`) - .. ---------------------------------------------------------------------------