123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- """Tests formatting as writer-agnostic ExcelCells
- ExcelFormatter is tested implicitly in pandas/tests/io/excel
- """
- import string
- import pytest
- from pandas.errors import CSSWarning
- import pandas.util._test_decorators as td
- import pandas._testing as tm
- from pandas.io.formats.excel import (
- CssExcelCell,
- CSSToExcelConverter,
- )
- @pytest.mark.parametrize(
- "css,expected",
- [
- # FONT
- # - name
- ("font-family: foo,bar", {"font": {"name": "foo"}}),
- ('font-family: "foo bar",baz', {"font": {"name": "foo bar"}}),
- ("font-family: foo,\nbar", {"font": {"name": "foo"}}),
- ("font-family: foo, bar, baz", {"font": {"name": "foo"}}),
- ("font-family: bar, foo", {"font": {"name": "bar"}}),
- ("font-family: 'foo bar', baz", {"font": {"name": "foo bar"}}),
- ("font-family: 'foo \\'bar', baz", {"font": {"name": "foo 'bar"}}),
- ('font-family: "foo \\"bar", baz', {"font": {"name": 'foo "bar'}}),
- ('font-family: "foo ,bar", baz', {"font": {"name": "foo ,bar"}}),
- # - family
- ("font-family: serif", {"font": {"name": "serif", "family": 1}}),
- ("font-family: Serif", {"font": {"name": "serif", "family": 1}}),
- ("font-family: roman, serif", {"font": {"name": "roman", "family": 1}}),
- ("font-family: roman, sans-serif", {"font": {"name": "roman", "family": 2}}),
- ("font-family: roman, sans serif", {"font": {"name": "roman"}}),
- ("font-family: roman, sansserif", {"font": {"name": "roman"}}),
- ("font-family: roman, cursive", {"font": {"name": "roman", "family": 4}}),
- ("font-family: roman, fantasy", {"font": {"name": "roman", "family": 5}}),
- # - size
- ("font-size: 1em", {"font": {"size": 12}}),
- ("font-size: xx-small", {"font": {"size": 6}}),
- ("font-size: x-small", {"font": {"size": 7.5}}),
- ("font-size: small", {"font": {"size": 9.6}}),
- ("font-size: medium", {"font": {"size": 12}}),
- ("font-size: large", {"font": {"size": 13.5}}),
- ("font-size: x-large", {"font": {"size": 18}}),
- ("font-size: xx-large", {"font": {"size": 24}}),
- ("font-size: 50%", {"font": {"size": 6}}),
- # - bold
- ("font-weight: 100", {"font": {"bold": False}}),
- ("font-weight: 200", {"font": {"bold": False}}),
- ("font-weight: 300", {"font": {"bold": False}}),
- ("font-weight: 400", {"font": {"bold": False}}),
- ("font-weight: normal", {"font": {"bold": False}}),
- ("font-weight: lighter", {"font": {"bold": False}}),
- ("font-weight: bold", {"font": {"bold": True}}),
- ("font-weight: bolder", {"font": {"bold": True}}),
- ("font-weight: 700", {"font": {"bold": True}}),
- ("font-weight: 800", {"font": {"bold": True}}),
- ("font-weight: 900", {"font": {"bold": True}}),
- # - italic
- ("font-style: italic", {"font": {"italic": True}}),
- ("font-style: oblique", {"font": {"italic": True}}),
- # - underline
- ("text-decoration: underline", {"font": {"underline": "single"}}),
- ("text-decoration: overline", {}),
- ("text-decoration: none", {}),
- # - strike
- ("text-decoration: line-through", {"font": {"strike": True}}),
- (
- "text-decoration: underline line-through",
- {"font": {"strike": True, "underline": "single"}},
- ),
- (
- "text-decoration: underline; text-decoration: line-through",
- {"font": {"strike": True}},
- ),
- # - color
- ("color: red", {"font": {"color": "FF0000"}}),
- ("color: #ff0000", {"font": {"color": "FF0000"}}),
- ("color: #f0a", {"font": {"color": "FF00AA"}}),
- # - shadow
- ("text-shadow: none", {"font": {"shadow": False}}),
- ("text-shadow: 0px -0em 0px #CCC", {"font": {"shadow": False}}),
- ("text-shadow: 0px -0em 0px #999", {"font": {"shadow": False}}),
- ("text-shadow: 0px -0em 0px", {"font": {"shadow": False}}),
- ("text-shadow: 2px -0em 0px #CCC", {"font": {"shadow": True}}),
- ("text-shadow: 0px -2em 0px #CCC", {"font": {"shadow": True}}),
- ("text-shadow: 0px -0em 2px #CCC", {"font": {"shadow": True}}),
- ("text-shadow: 0px -0em 2px", {"font": {"shadow": True}}),
- ("text-shadow: 0px -2em", {"font": {"shadow": True}}),
- # FILL
- # - color, fillType
- (
- "background-color: red",
- {"fill": {"fgColor": "FF0000", "patternType": "solid"}},
- ),
- (
- "background-color: #ff0000",
- {"fill": {"fgColor": "FF0000", "patternType": "solid"}},
- ),
- (
- "background-color: #f0a",
- {"fill": {"fgColor": "FF00AA", "patternType": "solid"}},
- ),
- # BORDER
- # - style
- (
- "border-style: solid",
- {
- "border": {
- "top": {"style": "medium"},
- "bottom": {"style": "medium"},
- "left": {"style": "medium"},
- "right": {"style": "medium"},
- }
- },
- ),
- (
- "border-style: solid; border-width: thin",
- {
- "border": {
- "top": {"style": "thin"},
- "bottom": {"style": "thin"},
- "left": {"style": "thin"},
- "right": {"style": "thin"},
- }
- },
- ),
- (
- "border-top-style: solid; border-top-width: thin",
- {"border": {"top": {"style": "thin"}}},
- ),
- (
- "border-top-style: solid; border-top-width: 1pt",
- {"border": {"top": {"style": "thin"}}},
- ),
- ("border-top-style: solid", {"border": {"top": {"style": "medium"}}}),
- (
- "border-top-style: solid; border-top-width: medium",
- {"border": {"top": {"style": "medium"}}},
- ),
- (
- "border-top-style: solid; border-top-width: 2pt",
- {"border": {"top": {"style": "medium"}}},
- ),
- (
- "border-top-style: solid; border-top-width: thick",
- {"border": {"top": {"style": "thick"}}},
- ),
- (
- "border-top-style: solid; border-top-width: 4pt",
- {"border": {"top": {"style": "thick"}}},
- ),
- (
- "border-top-style: dotted",
- {"border": {"top": {"style": "mediumDashDotDot"}}},
- ),
- (
- "border-top-style: dotted; border-top-width: thin",
- {"border": {"top": {"style": "dotted"}}},
- ),
- ("border-top-style: dashed", {"border": {"top": {"style": "mediumDashed"}}}),
- (
- "border-top-style: dashed; border-top-width: thin",
- {"border": {"top": {"style": "dashed"}}},
- ),
- ("border-top-style: double", {"border": {"top": {"style": "double"}}}),
- # - color
- (
- "border-style: solid; border-color: #0000ff",
- {
- "border": {
- "top": {"style": "medium", "color": "0000FF"},
- "right": {"style": "medium", "color": "0000FF"},
- "bottom": {"style": "medium", "color": "0000FF"},
- "left": {"style": "medium", "color": "0000FF"},
- }
- },
- ),
- (
- "border-top-style: double; border-top-color: blue",
- {"border": {"top": {"style": "double", "color": "0000FF"}}},
- ),
- (
- "border-top-style: solid; border-top-color: #06c",
- {"border": {"top": {"style": "medium", "color": "0066CC"}}},
- ),
- (
- "border-top-color: blue",
- {"border": {"top": {"color": "0000FF", "style": "none"}}},
- ),
- # ALIGNMENT
- # - horizontal
- ("text-align: center", {"alignment": {"horizontal": "center"}}),
- ("text-align: left", {"alignment": {"horizontal": "left"}}),
- ("text-align: right", {"alignment": {"horizontal": "right"}}),
- ("text-align: justify", {"alignment": {"horizontal": "justify"}}),
- # - vertical
- ("vertical-align: top", {"alignment": {"vertical": "top"}}),
- ("vertical-align: text-top", {"alignment": {"vertical": "top"}}),
- ("vertical-align: middle", {"alignment": {"vertical": "center"}}),
- ("vertical-align: bottom", {"alignment": {"vertical": "bottom"}}),
- ("vertical-align: text-bottom", {"alignment": {"vertical": "bottom"}}),
- # - wrap_text
- ("white-space: nowrap", {"alignment": {"wrap_text": False}}),
- ("white-space: pre", {"alignment": {"wrap_text": False}}),
- ("white-space: pre-line", {"alignment": {"wrap_text": False}}),
- ("white-space: normal", {"alignment": {"wrap_text": True}}),
- # NUMBER FORMAT
- ("number-format: 0%", {"number_format": {"format_code": "0%"}}),
- (
- "number-format: 0§[Red](0)§-§@;",
- {"number_format": {"format_code": "0;[red](0);-;@"}}, # GH 46152
- ),
- ],
- )
- def test_css_to_excel(css, expected):
- convert = CSSToExcelConverter()
- assert expected == convert(css)
- def test_css_to_excel_multiple():
- convert = CSSToExcelConverter()
- actual = convert(
- """
- font-weight: bold;
- text-decoration: underline;
- color: red;
- border-width: thin;
- text-align: center;
- vertical-align: top;
- unused: something;
- """
- )
- assert {
- "font": {"bold": True, "underline": "single", "color": "FF0000"},
- "border": {
- "top": {"style": "thin"},
- "right": {"style": "thin"},
- "bottom": {"style": "thin"},
- "left": {"style": "thin"},
- },
- "alignment": {"horizontal": "center", "vertical": "top"},
- } == actual
- @pytest.mark.parametrize(
- "css,inherited,expected",
- [
- ("font-weight: bold", "", {"font": {"bold": True}}),
- ("", "font-weight: bold", {"font": {"bold": True}}),
- (
- "font-weight: bold",
- "font-style: italic",
- {"font": {"bold": True, "italic": True}},
- ),
- ("font-style: normal", "font-style: italic", {"font": {"italic": False}}),
- ("font-style: inherit", "", {}),
- (
- "font-style: normal; font-style: inherit",
- "font-style: italic",
- {"font": {"italic": True}},
- ),
- ],
- )
- def test_css_to_excel_inherited(css, inherited, expected):
- convert = CSSToExcelConverter(inherited)
- assert expected == convert(css)
- @pytest.mark.parametrize(
- "input_color,output_color",
- (
- list(CSSToExcelConverter.NAMED_COLORS.items())
- + [("#" + rgb, rgb) for rgb in CSSToExcelConverter.NAMED_COLORS.values()]
- + [("#F0F", "FF00FF"), ("#ABC", "AABBCC")]
- ),
- )
- def test_css_to_excel_good_colors(input_color, output_color):
- # see gh-18392
- css = (
- f"border-top-color: {input_color}; "
- f"border-right-color: {input_color}; "
- f"border-bottom-color: {input_color}; "
- f"border-left-color: {input_color}; "
- f"background-color: {input_color}; "
- f"color: {input_color}"
- )
- expected = {}
- expected["fill"] = {"patternType": "solid", "fgColor": output_color}
- expected["font"] = {"color": output_color}
- expected["border"] = {
- k: {"color": output_color, "style": "none"}
- for k in ("top", "right", "bottom", "left")
- }
- with tm.assert_produces_warning(None):
- convert = CSSToExcelConverter()
- assert expected == convert(css)
- @pytest.mark.parametrize("input_color", [None, "not-a-color"])
- def test_css_to_excel_bad_colors(input_color):
- # see gh-18392
- css = (
- f"border-top-color: {input_color}; "
- f"border-right-color: {input_color}; "
- f"border-bottom-color: {input_color}; "
- f"border-left-color: {input_color}; "
- f"background-color: {input_color}; "
- f"color: {input_color}"
- )
- expected = {}
- if input_color is not None:
- expected["fill"] = {"patternType": "solid"}
- with tm.assert_produces_warning(CSSWarning):
- convert = CSSToExcelConverter()
- assert expected == convert(css)
- def tests_css_named_colors_valid():
- upper_hexs = set(map(str.upper, string.hexdigits))
- for color in CSSToExcelConverter.NAMED_COLORS.values():
- assert len(color) == 6 and all(c in upper_hexs for c in color)
- @td.skip_if_no_mpl
- def test_css_named_colors_from_mpl_present():
- from matplotlib.colors import CSS4_COLORS as mpl_colors
- 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_cached.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_cached.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_cached.cache_clear()
- 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_cached.cache_info()
- converter._call_cached.cache_clear()
- assert cache_info.hits == cache_hits
- assert cache_info.misses == cache_misses
|