From a1beeeabeb5f166e28ad13ccd707bebc0fef7f2d Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 18:13:27 +0700 Subject: [PATCH 01/17] REF: dict mapping instead of is-elif-else --- pandas/io/formats/excel.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 0140804e8c7b5..ade597a4f2478 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -198,6 +198,7 @@ def _border_style(self, style: Optional[str], width): return "dashed" return "mediumDashed" + def build_fill(self, props: Dict[str, str]): # TODO: perhaps allow for special properties # -excel-pattern-bgcolor and -excel-pattern-type @@ -251,19 +252,17 @@ def build_font(self, props) -> Dict[str, Optional[Union[bool, int, str]]]: if name: font_names.append(name) + family_mapping = { + "serif": 1, # roman + "sans-serif": 2, # swiss + "cursive": 4, # script + "fantasy": 5, # decorative + } + family = None for name in font_names: - if name == "serif": - family = 1 # roman - break - elif name == "sans-serif": - family = 2 # swiss - break - elif name == "cursive": - family = 4 # script - break - elif name == "fantasy": - family = 5 # decorative + family = family_mapping.get(name) + if family: break decoration = props.get("text-decoration") From b15ae56e6349c010546fe3354c221738bfbdb74c Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 18:16:05 +0700 Subject: [PATCH 02/17] REF: extract method _select_font_family --- pandas/io/formats/excel.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index ade597a4f2478..2dc592c6cad31 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -198,7 +198,6 @@ def _border_style(self, style: Optional[str], width): return "dashed" return "mediumDashed" - def build_fill(self, props: Dict[str, str]): # TODO: perhaps allow for special properties # -excel-pattern-bgcolor and -excel-pattern-type @@ -252,19 +251,6 @@ def build_font(self, props) -> Dict[str, Optional[Union[bool, int, str]]]: if name: font_names.append(name) - family_mapping = { - "serif": 1, # roman - "sans-serif": 2, # swiss - "cursive": 4, # script - "fantasy": 5, # decorative - } - - family = None - for name in font_names: - family = family_mapping.get(name) - if family: - break - decoration = props.get("text-decoration") if decoration is not None: decoration = decoration.split() @@ -273,7 +259,7 @@ def build_font(self, props) -> Dict[str, Optional[Union[bool, int, str]]]: return { "name": font_names[0] if font_names else None, - "family": family, + "family": self._select_font_family(font_names), "size": size, "bold": self.BOLD_MAP.get(props.get("font-weight")), "italic": self.ITALIC_MAP.get(props.get("font-style")), @@ -317,6 +303,22 @@ def build_font(self, props) -> Dict[str, Optional[Union[bool, int, str]]]: "white": "FFFFFF", } + def _select_font_family(self, font_names): + family_mapping = { + "serif": 1, # roman + "sans-serif": 2, # swiss + "cursive": 4, # script + "fantasy": 5, # decorative + } + + family = None + for name in font_names: + family = family_mapping.get(name) + if family: + break + + return family + def color_to_excel(self, val: Optional[str]): if val is None: return None From ec73fd82103a31420d0a0e260a68ef2f71eb5aec Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 18:21:13 +0700 Subject: [PATCH 03/17] CLN: clean-up size definition --- 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 2dc592c6cad31..bda4ff2cba8bf 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -226,7 +226,7 @@ def build_font(self, props) -> Dict[str, Optional[Union[bool, int, str]]]: size = props.get("font-size") if size is not None: assert size.endswith("pt") - size = float(size[:-2]) + size = float(size.rstrip("pt")) font_names_tmp = re.findall( r"""(?x) From 79620ace56cd04791f64df585d933c08fe00c92b Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 19:12:00 +0700 Subject: [PATCH 04/17] REF: extract method _get_font_names --- pandas/io/formats/excel.py | 71 +++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index bda4ff2cba8bf..2b43d40ecc789 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -5,7 +5,7 @@ from functools import reduce import itertools import re -from typing import Callable, Dict, Optional, Sequence, Union +from typing import Callable, Dict, Mapping, Optional, Sequence, Union import warnings import numpy as np @@ -222,45 +222,19 @@ def build_fill(self, props: Dict[str, str]): } ITALIC_MAP = {"normal": False, "italic": True, "oblique": True} - def build_font(self, props) -> Dict[str, Optional[Union[bool, int, str]]]: - size = props.get("font-size") - if size is not None: - assert size.endswith("pt") - size = float(size.rstrip("pt")) - - font_names_tmp = re.findall( - r"""(?x) - ( - "(?:[^"]|\\")+" - | - '(?:[^']|\\')+' - | - [^'",]+ - )(?=,|\s*$) - """, - props.get("font-family", ""), - ) - font_names = [] - for name in font_names_tmp: - if name[:1] == '"': - name = name[1:-1].replace('\\"', '"') - elif name[:1] == "'": - name = name[1:-1].replace("\\'", "'") - else: - name = name.strip() - if name: - font_names.append(name) - + def build_font(self, props) -> Dict[str, Optional[Union[bool, int, float, str]]]: decoration = props.get("text-decoration") if decoration is not None: decoration = decoration.split() else: decoration = () + font_names = self._get_font_names(props) + return { "name": font_names[0] if font_names else None, "family": self._select_font_family(font_names), - "size": size, + "size": self._get_font_size(props), "bold": self.BOLD_MAP.get(props.get("font-weight")), "italic": self.ITALIC_MAP.get(props.get("font-style")), "underline": ("single" if "underline" in decoration else None), @@ -303,7 +277,40 @@ def build_font(self, props) -> Dict[str, Optional[Union[bool, int, str]]]: "white": "FFFFFF", } - def _select_font_family(self, font_names): + def _get_font_names(self, props: Mapping[str, str]) -> Sequence[str]: + font_names_tmp = re.findall( + r"""(?x) + ( + "(?:[^"]|\\")+" + | + '(?:[^']|\\')+' + | + [^'",]+ + )(?=,|\s*$) + """, + props.get("font-family", ""), + ) + + font_names = [] + for name in font_names_tmp: + if name[:1] == '"': + name = name[1:-1].replace('\\"', '"') + elif name[:1] == "'": + name = name[1:-1].replace("\\'", "'") + else: + name = name.strip() + if name: + font_names.append(name) + return font_names + + def _get_font_size(self, props: Mapping[str, str]) -> Optional[float]: + size = props.get("font-size") + if size is None: + return size + assert size.endswith("pt") + return float(size.rstrip("pt")) + + def _select_font_family(self, font_names) -> Optional[int]: family_mapping = { "serif": 1, # roman "sans-serif": 2, # swiss From 14bddca23f80e619ccea502fc7f9f5259fecec52 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 19:14:53 +0700 Subject: [PATCH 05/17] CLN: move class attributes on top --- pandas/io/formats/excel.py | 121 +++++++++++++++++++------------------ 1 file changed, 63 insertions(+), 58 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 2b43d40ecc789..4a2ff62d82ab7 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -58,6 +58,68 @@ class CSSToExcelConverter: CSS processed by :meth:`__call__`. """ + NAMED_COLORS = { + "maroon": "800000", + "brown": "A52A2A", + "red": "FF0000", + "pink": "FFC0CB", + "orange": "FFA500", + "yellow": "FFFF00", + "olive": "808000", + "green": "008000", + "purple": "800080", + "fuchsia": "FF00FF", + "lime": "00FF00", + "teal": "008080", + "aqua": "00FFFF", + "blue": "0000FF", + "navy": "000080", + "black": "000000", + "gray": "808080", + "grey": "808080", + "silver": "C0C0C0", + "white": "FFFFFF", + } + + VERTICAL_MAP = { + "top": "top", + "text-top": "top", + "middle": "center", + "baseline": "bottom", + "bottom": "bottom", + "text-bottom": "bottom", + # OpenXML also has 'justify', 'distributed' + } + + BOLD_MAP = { + "bold": True, + "bolder": True, + "600": True, + "700": True, + "800": True, + "900": True, + "normal": False, + "lighter": False, + "100": False, + "200": False, + "300": False, + "400": False, + "500": False, + } + + ITALIC_MAP = { + "normal": False, + "italic": True, + "oblique": True, + } + + FAMILY_MAP = { + "serif": 1, # roman + "sans-serif": 2, # swiss + "cursive": 4, # script + "fantasy": 5, # decorative + } + # 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 @@ -115,16 +177,6 @@ def remove_none(d: Dict[str, str]) -> None: remove_none(out) return out - VERTICAL_MAP = { - "top": "top", - "text-top": "top", - "middle": "center", - "baseline": "bottom", - "bottom": "bottom", - "text-bottom": "bottom", - # OpenXML also has 'justify', 'distributed' - } - def build_alignment(self, props) -> Dict[str, Optional[Union[bool, str]]]: # TODO: text-indent, padding-left -> alignment.indent return { @@ -205,23 +257,6 @@ def build_fill(self, props: Dict[str, str]): if fill_color not in (None, "transparent", "none"): return {"fgColor": self.color_to_excel(fill_color), "patternType": "solid"} - BOLD_MAP = { - "bold": True, - "bolder": True, - "600": True, - "700": True, - "800": True, - "900": True, - "normal": False, - "lighter": False, - "100": False, - "200": False, - "300": False, - "400": False, - "500": False, - } - ITALIC_MAP = {"normal": False, "italic": True, "oblique": True} - def build_font(self, props) -> Dict[str, Optional[Union[bool, int, float, str]]]: decoration = props.get("text-decoration") if decoration is not None: @@ -254,29 +289,6 @@ def build_font(self, props) -> Dict[str, Optional[Union[bool, int, float, str]]] # 'condense': , } - NAMED_COLORS = { - "maroon": "800000", - "brown": "A52A2A", - "red": "FF0000", - "pink": "FFC0CB", - "orange": "FFA500", - "yellow": "FFFF00", - "olive": "808000", - "green": "008000", - "purple": "800080", - "fuchsia": "FF00FF", - "lime": "00FF00", - "teal": "008080", - "aqua": "00FFFF", - "blue": "0000FF", - "navy": "000080", - "black": "000000", - "gray": "808080", - "grey": "808080", - "silver": "C0C0C0", - "white": "FFFFFF", - } - def _get_font_names(self, props: Mapping[str, str]) -> Sequence[str]: font_names_tmp = re.findall( r"""(?x) @@ -311,16 +323,9 @@ def _get_font_size(self, props: Mapping[str, str]) -> Optional[float]: return float(size.rstrip("pt")) def _select_font_family(self, font_names) -> Optional[int]: - family_mapping = { - "serif": 1, # roman - "sans-serif": 2, # swiss - "cursive": 4, # script - "fantasy": 5, # decorative - } - family = None for name in font_names: - family = family_mapping.get(name) + family = self.FAMILY_MAP.get(name) if family: break From d12f8b7bb2feced7ec5b70b9125b118b99aba499 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 19:20:37 +0700 Subject: [PATCH 06/17] REF: move build_number_format upper --- pandas/io/formats/excel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 4a2ff62d82ab7..948074c23be90 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -257,6 +257,9 @@ def build_fill(self, props: Dict[str, str]): if fill_color not in (None, "transparent", "none"): return {"fgColor": self.color_to_excel(fill_color), "patternType": "solid"} + def build_number_format(self, props: Dict) -> Dict[str, Optional[str]]: + return {"format_code": props.get("number-format")} + def build_font(self, props) -> Dict[str, Optional[Union[bool, int, float, str]]]: decoration = props.get("text-decoration") if decoration is not None: @@ -343,9 +346,6 @@ def color_to_excel(self, val: Optional[str]): except KeyError: warnings.warn(f"Unhandled color format: {repr(val)}", CSSWarning) - def build_number_format(self, props: Dict) -> Dict[str, Optional[str]]: - return {"format_code": props.get("number-format")} - class ExcelFormatter: """ From 6541c58942d3fc7e9a3d4df1a9b6d9996de5e53e Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 19:51:20 +0700 Subject: [PATCH 07/17] REF: extract method _get_width_name --- pandas/io/formats/excel.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 948074c23be90..f32da3a74eee3 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -201,7 +201,7 @@ def build_border(self, props: Dict) -> Dict[str, Dict[str, str]]: for side in ["top", "right", "bottom", "left"] } - def _border_style(self, style: Optional[str], width): + def _border_style(self, style: Optional[str], width: Optional[str]): # convert styles and widths to openxml, one of: # 'dashDot' # 'dashDotDot' @@ -221,17 +221,9 @@ def _border_style(self, style: Optional[str], width): if style == "none" or style == "hidden": return None - if width is None: - width = "2pt" - width = float(width[:-2]) - if width < 1e-5: + width_name = self._get_width_name(width) + if width_name is None: return None - elif width < 1.3: - width_name = "thin" - elif width < 2.8: - width_name = "medium" - else: - width_name = "thick" if style in (None, "groove", "ridge", "inset", "outset"): # not handled @@ -250,6 +242,21 @@ def _border_style(self, style: Optional[str], width): return "dashed" return "mediumDashed" + def _get_width_name(self, width_input: Optional[str]) -> Optional[str]: + width = self._width_to_float(width_input) + if width < 1e-5: + return None + elif width < 1.3: + return "thin" + elif width < 2.8: + return "medium" + return "thick" + + def _width_to_float(self, width: Optional[str]) -> float: + if width is None: + width = "2pt" + return float(width[:-2]) + def build_fill(self, props: Dict[str, str]): # TODO: perhaps allow for special properties # -excel-pattern-bgcolor and -excel-pattern-type From 0b0f8cec9f61c32e7200aa0883ec981efb8a7204 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 20:03:51 +0700 Subject: [PATCH 08/17] REF: simplify logic in _border_style --- pandas/io/formats/excel.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index f32da3a74eee3..c665dbeb7614c 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -225,14 +225,12 @@ def _border_style(self, style: Optional[str], width: Optional[str]): if width_name is None: return None - if style in (None, "groove", "ridge", "inset", "outset"): + if style in (None, "groove", "ridge", "inset", "outset", "solid"): # not handled - style = "solid" + return width_name if style == "double": return "double" - if style == "solid": - return width_name if style == "dotted": if width_name in ("hair", "thin"): return "dotted" From 17ab78cf5399002cab58254d79f3cb6462fe3785 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 20:10:24 +0700 Subject: [PATCH 09/17] TYP: color_to_excel --- pandas/io/formats/excel.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index c665dbeb7614c..c1c5a992a3001 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -189,7 +189,7 @@ def build_alignment(self, props) -> Dict[str, Optional[Union[bool, str]]]: ), } - def build_border(self, props: Dict) -> Dict[str, Dict[str, str]]: + def build_border(self, props: Dict) -> Dict[str, Dict[str, Optional[str]]]: return { side: { "style": self._border_style( @@ -339,7 +339,7 @@ def _select_font_family(self, font_names) -> Optional[int]: return family - def color_to_excel(self, val: Optional[str]): + def color_to_excel(self, val: Optional[str]) -> Optional[str]: if val is None: return None if val.startswith("#") and len(val) == 7: @@ -350,6 +350,7 @@ def color_to_excel(self, val: Optional[str]): return self.NAMED_COLORS[val] except KeyError: warnings.warn(f"Unhandled color format: {repr(val)}", CSSWarning) + return None class ExcelFormatter: From b8fbe95cf4c1cf05221c9c16e34d821993644c55 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 20:16:37 +0700 Subject: [PATCH 10/17] REF: extract methods for handling hex color --- pandas/io/formats/excel.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index c1c5a992a3001..14c1bfe6e7427 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -342,16 +342,39 @@ def _select_font_family(self, font_names) -> Optional[int]: def color_to_excel(self, val: Optional[str]) -> Optional[str]: if val is None: return None - if val.startswith("#") and len(val) == 7: - return val[1:].upper() - if val.startswith("#") and len(val) == 4: - return (val[1] * 2 + val[2] * 2 + val[3] * 2).upper() + + if self._is_hex_color(val): + return self._convert_hex_to_excel(val) + try: return self.NAMED_COLORS[val] except KeyError: warnings.warn(f"Unhandled color format: {repr(val)}", CSSWarning) return None + def _is_hex_color(self, color_string: str) -> bool: + return bool(color_string.startswith("#")) + + def _convert_hex_to_excel(self, color_string: str) -> str: + code = color_string.lstrip("#") + if self._is_shorthand_color(color_string): + return (code[0] * 2 + code[1] * 2 + code[2] * 2).upper() + else: + return code.upper() + + def _is_shorthand_color(self, color_string: str) -> bool: + """Check if color code is shorthand. + + #FFF is a shorthand as opposed to full #FFFFFF. + """ + code = color_string.lstrip("#") + if len(code) == 3: + return True + elif len(code) == 6: + return False + else: + raise ValueError(f"Unexpected color {color_string}") + class ExcelFormatter: """ From d4f3c20fa4f176a28ba06514afaf98c5ebb416f0 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 20:29:53 +0700 Subject: [PATCH 11/17] REF: extract method _get_decoration --- pandas/io/formats/excel.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 14c1bfe6e7427..839d458c2eeb6 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -266,13 +266,8 @@ def build_number_format(self, props: Dict) -> Dict[str, Optional[str]]: return {"format_code": props.get("number-format")} def build_font(self, props) -> Dict[str, Optional[Union[bool, int, float, str]]]: - decoration = props.get("text-decoration") - if decoration is not None: - decoration = decoration.split() - else: - decoration = () - font_names = self._get_font_names(props) + decoration = self._get_decoration(props) return { "name": font_names[0] if font_names else None, @@ -297,6 +292,13 @@ def build_font(self, props) -> Dict[str, Optional[Union[bool, int, float, str]]] # 'condense': , } + def _get_decoration(self, props: Mapping[str, str]) -> Sequence[str]: + decoration = props.get("text-decoration") + if decoration is not None: + return decoration.split() + else: + return () + def _get_font_names(self, props: Mapping[str, str]) -> Sequence[str]: font_names_tmp = re.findall( r"""(?x) From 2a98ee39d612767a6a25d066d606311e0972b2b2 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 22:15:07 +0700 Subject: [PATCH 12/17] REF: extract methods for bold and italic This was necessary to ensure that mypy does not throw errors. Mypy does not like chained get operators. Link to mypy issue: https://github.com/python/mypy/issues/9430 --- pandas/io/formats/excel.py | 39 +++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 839d458c2eeb6..b9ab44820ffb0 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -265,25 +265,24 @@ def build_fill(self, props: Dict[str, str]): def build_number_format(self, props: Dict) -> Dict[str, Optional[str]]: return {"format_code": props.get("number-format")} - def build_font(self, props) -> Dict[str, Optional[Union[bool, int, float, str]]]: + def build_font( + self, props: Mapping[str, str] + ) -> Dict[str, Optional[Union[bool, int, float, str]]]: font_names = self._get_font_names(props) decoration = self._get_decoration(props) - return { "name": font_names[0] if font_names else None, "family": self._select_font_family(font_names), "size": self._get_font_size(props), - "bold": self.BOLD_MAP.get(props.get("font-weight")), - "italic": self.ITALIC_MAP.get(props.get("font-style")), + # Ignored mypy errors because of changed get. + # Link to mypy issue: https://github.com/python/mypy/issues/9430 + "bold": self._get_is_bold(props), + "italic": self._get_is_italic(props), "underline": ("single" if "underline" in decoration else None), "strike": ("line-through" in decoration) or None, "color": self.color_to_excel(props.get("color")), # shadow if nonzero digit before shadow color - "shadow": ( - bool(re.search("^[^#(]*[1-9]", props["text-shadow"])) - if "text-shadow" in props - else None - ), + "shadow": self._get_shadow(props), # FIXME: dont leave commented-out # 'vertAlign':, # 'charset': , @@ -292,6 +291,18 @@ def build_font(self, props) -> Dict[str, Optional[Union[bool, int, float, str]]] # 'condense': , } + def _get_is_bold(self, props: Mapping[str, str]) -> Optional[bool]: + weight = props.get("font-weight") + if weight: + return self.BOLD_MAP.get(weight) + return None + + def _get_is_italic(self, props: Mapping[str, str]) -> Optional[bool]: + font_style = props.get("font-style") + if font_style: + return self.ITALIC_MAP.get(font_style) + return None + def _get_decoration(self, props: Mapping[str, str]) -> Sequence[str]: decoration = props.get("text-decoration") if decoration is not None: @@ -299,6 +310,16 @@ def _get_decoration(self, props: Mapping[str, str]) -> Sequence[str]: else: return () + def _get_underline(self, decoration: Sequence[str]) -> Optional[str]: + if "underline" in decoration: + return "single" + return None + + def _get_shadow(self, props: Mapping[str, str]) -> Optional[bool]: + if "text-shadow" in props: + return bool(re.search("^[^#(]*[1-9]", props["text-shadow"])) + return None + def _get_font_names(self, props: Mapping[str, str]) -> Sequence[str]: font_names_tmp = re.findall( r"""(?x) From 151184d0e91e0048df7327dc9a13e90756143671 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 22:24:09 +0700 Subject: [PATCH 13/17] CLN: delete irrelevant comment --- pandas/io/formats/excel.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index b9ab44820ffb0..874d19c6a2f10 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -274,8 +274,6 @@ def build_font( "name": font_names[0] if font_names else None, "family": self._select_font_family(font_names), "size": self._get_font_size(props), - # Ignored mypy errors because of changed get. - # Link to mypy issue: https://github.com/python/mypy/issues/9430 "bold": self._get_is_bold(props), "italic": self._get_is_italic(props), "underline": ("single" if "underline" in decoration else None), From c8655d01edc0adb99607801b891b1056de3c0233 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 22:28:16 +0700 Subject: [PATCH 14/17] REF: extract method _get_is_wrap_text --- pandas/io/formats/excel.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 874d19c6a2f10..d32ca0d81996f 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -182,13 +182,14 @@ def build_alignment(self, props) -> Dict[str, Optional[Union[bool, str]]]: return { "horizontal": props.get("text-align"), "vertical": self.VERTICAL_MAP.get(props.get("vertical-align")), - "wrap_text": ( - None - if props.get("white-space") is None - else props["white-space"] not in ("nowrap", "pre", "pre-line") - ), + "wrap_text": self._get_is_wrap_text(props), } + def _get_is_wrap_text(self, props: Mapping[str, str]) -> Optional[bool]: + if props.get("white-space") is None: + return None + return bool(props["white-space"] not in ("nowrap", "pre", "pre-line")) + def build_border(self, props: Dict) -> Dict[str, Dict[str, Optional[str]]]: return { side: { From 73d3f9eb34aae9a764e140041a26bb227b37b782 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Wed, 23 Sep 2020 22:33:55 +0700 Subject: [PATCH 15/17] REF: extract method _get_vertical_alignment --- pandas/io/formats/excel.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index d32ca0d81996f..b24905e6a8a92 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -153,7 +153,7 @@ def __call__(self, declarations_str: str) -> Dict[str, Dict[str, str]]: properties = self.compute_css(declarations_str, self.inherited) return self.build_xlstyle(properties) - def build_xlstyle(self, props: Dict[str, str]) -> Dict[str, Dict[str, str]]: + def build_xlstyle(self, props: Mapping[str, str]) -> Dict[str, Dict[str, str]]: out = { "alignment": self.build_alignment(props), "border": self.build_border(props), @@ -177,20 +177,30 @@ def remove_none(d: Dict[str, str]) -> None: remove_none(out) return out - def build_alignment(self, props) -> Dict[str, Optional[Union[bool, str]]]: + def build_alignment( + self, props: Mapping[str, str] + ) -> Dict[str, Optional[Union[bool, str]]]: # TODO: text-indent, padding-left -> alignment.indent return { "horizontal": props.get("text-align"), - "vertical": self.VERTICAL_MAP.get(props.get("vertical-align")), + "vertical": self._get_vertical_alignment(props), "wrap_text": self._get_is_wrap_text(props), } + def _get_vertical_alignment(self, props: Mapping[str, str]) -> Optional[str]: + vertical_align = props.get("vertical-align") + if vertical_align: + return self.VERTICAL_MAP.get(vertical_align) + return None + def _get_is_wrap_text(self, props: Mapping[str, str]) -> Optional[bool]: if props.get("white-space") is None: return None return bool(props["white-space"] not in ("nowrap", "pre", "pre-line")) - def build_border(self, props: Dict) -> Dict[str, Dict[str, Optional[str]]]: + def build_border( + self, props: Mapping[str, str] + ) -> Dict[str, Dict[str, Optional[str]]]: return { side: { "style": self._border_style( @@ -256,14 +266,14 @@ def _width_to_float(self, width: Optional[str]) -> float: width = "2pt" return float(width[:-2]) - def build_fill(self, props: Dict[str, str]): + def build_fill(self, props: Mapping[str, str]): # TODO: perhaps allow for special properties # -excel-pattern-bgcolor and -excel-pattern-type fill_color = props.get("background-color") if fill_color not in (None, "transparent", "none"): return {"fgColor": self.color_to_excel(fill_color), "patternType": "solid"} - def build_number_format(self, props: Dict) -> Dict[str, Optional[str]]: + def build_number_format(self, props: Mapping[str, str]) -> Dict[str, Optional[str]]: return {"format_code": props.get("number-format")} def build_font( From e511e7b138dff63645fa40682703bf20c9f78019 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Thu, 24 Sep 2020 01:30:03 +0700 Subject: [PATCH 16/17] REF: replace indexing with more clear pt strip --- pandas/io/formats/excel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index b24905e6a8a92..e6cc300915a9f 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -264,7 +264,8 @@ def _get_width_name(self, width_input: Optional[str]) -> Optional[str]: def _width_to_float(self, width: Optional[str]) -> float: if width is None: width = "2pt" - return float(width[:-2]) + assert width.endswith("pt") + return float(width.rstrip("pt")) def build_fill(self, props: Mapping[str, str]): # TODO: perhaps allow for special properties From af570946662d8e1611cc984c190b9737e7f14374 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Thu, 24 Sep 2020 01:32:41 +0700 Subject: [PATCH 17/17] REF: extract method _pt_to_float --- pandas/io/formats/excel.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index e6cc300915a9f..79f1b5d73f122 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -264,8 +264,11 @@ def _get_width_name(self, width_input: Optional[str]) -> Optional[str]: def _width_to_float(self, width: Optional[str]) -> float: if width is None: width = "2pt" - assert width.endswith("pt") - return float(width.rstrip("pt")) + return self._pt_to_float(width) + + def _pt_to_float(self, pt_string: str) -> float: + assert pt_string.endswith("pt") + return float(pt_string.rstrip("pt")) def build_fill(self, props: Mapping[str, str]): # TODO: perhaps allow for special properties @@ -360,8 +363,7 @@ def _get_font_size(self, props: Mapping[str, str]) -> Optional[float]: size = props.get("font-size") if size is None: return size - assert size.endswith("pt") - return float(size.rstrip("pt")) + return self._pt_to_float(size) def _select_font_family(self, font_names) -> Optional[int]: family = None