166 lines
6.3 KiB
Python
166 lines
6.3 KiB
Python
|
import numpy as np
|
||
|
import pytest
|
||
|
|
||
|
from pandas import DataFrame
|
||
|
import pandas._testing as tm
|
||
|
|
||
|
from pandas.io.excel import ExcelWriter
|
||
|
from pandas.io.formats.excel import ExcelFormatter
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"engine",
|
||
|
[
|
||
|
pytest.param(
|
||
|
"xlwt",
|
||
|
marks=pytest.mark.xfail(
|
||
|
reason="xlwt does not support openpyxl-compatible style dicts"
|
||
|
),
|
||
|
),
|
||
|
"xlsxwriter",
|
||
|
"openpyxl",
|
||
|
],
|
||
|
)
|
||
|
def test_styler_to_excel(engine):
|
||
|
def style(df):
|
||
|
# TODO: RGB colors not supported in xlwt
|
||
|
return DataFrame(
|
||
|
[
|
||
|
["font-weight: bold", "", ""],
|
||
|
["", "color: blue", ""],
|
||
|
["", "", "text-decoration: underline"],
|
||
|
["border-style: solid", "", ""],
|
||
|
["", "font-style: italic", ""],
|
||
|
["", "", "text-align: right"],
|
||
|
["background-color: red", "", ""],
|
||
|
["number-format: 0%", "", ""],
|
||
|
["", "", ""],
|
||
|
["", "", ""],
|
||
|
["", "", ""],
|
||
|
],
|
||
|
index=df.index,
|
||
|
columns=df.columns,
|
||
|
)
|
||
|
|
||
|
def assert_equal_style(cell1, cell2, engine):
|
||
|
if engine in ["xlsxwriter", "openpyxl"]:
|
||
|
pytest.xfail(
|
||
|
reason=(f"GH25351: failing on some attribute comparisons in {engine}")
|
||
|
)
|
||
|
# TODO: should find a better way to check equality
|
||
|
assert cell1.alignment.__dict__ == cell2.alignment.__dict__
|
||
|
assert cell1.border.__dict__ == cell2.border.__dict__
|
||
|
assert cell1.fill.__dict__ == cell2.fill.__dict__
|
||
|
assert cell1.font.__dict__ == cell2.font.__dict__
|
||
|
assert cell1.number_format == cell2.number_format
|
||
|
assert cell1.protection.__dict__ == cell2.protection.__dict__
|
||
|
|
||
|
def custom_converter(css):
|
||
|
# use bold iff there is custom style attached to the cell
|
||
|
if css.strip(" \n;"):
|
||
|
return {"font": {"bold": True}}
|
||
|
return {}
|
||
|
|
||
|
pytest.importorskip("jinja2")
|
||
|
pytest.importorskip(engine)
|
||
|
|
||
|
# Prepare spreadsheets
|
||
|
|
||
|
df = DataFrame(np.random.randn(11, 3))
|
||
|
with tm.ensure_clean(".xlsx" if engine != "xlwt" else ".xls") as path:
|
||
|
with ExcelWriter(path, engine=engine) as writer:
|
||
|
df.to_excel(writer, sheet_name="frame")
|
||
|
df.style.to_excel(writer, sheet_name="unstyled")
|
||
|
styled = df.style.apply(style, axis=None)
|
||
|
styled.to_excel(writer, sheet_name="styled")
|
||
|
ExcelFormatter(styled, style_converter=custom_converter).write(
|
||
|
writer, sheet_name="custom"
|
||
|
)
|
||
|
|
||
|
if engine not in ("openpyxl", "xlsxwriter"):
|
||
|
# For other engines, we only smoke test
|
||
|
return
|
||
|
openpyxl = pytest.importorskip("openpyxl")
|
||
|
wb = openpyxl.load_workbook(path)
|
||
|
|
||
|
# (1) compare DataFrame.to_excel and Styler.to_excel when unstyled
|
||
|
n_cells = 0
|
||
|
for col1, col2 in zip(wb["frame"].columns, wb["unstyled"].columns):
|
||
|
assert len(col1) == len(col2)
|
||
|
for cell1, cell2 in zip(col1, col2):
|
||
|
assert cell1.value == cell2.value
|
||
|
assert_equal_style(cell1, cell2, engine)
|
||
|
n_cells += 1
|
||
|
|
||
|
# ensure iteration actually happened:
|
||
|
assert n_cells == (11 + 1) * (3 + 1)
|
||
|
|
||
|
# (2) check styling with default converter
|
||
|
|
||
|
# TODO: openpyxl (as at 2.4) prefixes colors with 00, xlsxwriter with FF
|
||
|
alpha = "00" if engine == "openpyxl" else "FF"
|
||
|
|
||
|
n_cells = 0
|
||
|
for col1, col2 in zip(wb["frame"].columns, wb["styled"].columns):
|
||
|
assert len(col1) == len(col2)
|
||
|
for cell1, cell2 in zip(col1, col2):
|
||
|
ref = f"{cell2.column}{cell2.row:d}"
|
||
|
# TODO: this isn't as strong a test as ideal; we should
|
||
|
# confirm that differences are exclusive
|
||
|
if ref == "B2":
|
||
|
assert not cell1.font.bold
|
||
|
assert cell2.font.bold
|
||
|
elif ref == "C3":
|
||
|
assert cell1.font.color.rgb != cell2.font.color.rgb
|
||
|
assert cell2.font.color.rgb == alpha + "0000FF"
|
||
|
elif ref == "D4":
|
||
|
assert cell1.font.underline != cell2.font.underline
|
||
|
assert cell2.font.underline == "single"
|
||
|
elif ref == "B5":
|
||
|
assert not cell1.border.left.style
|
||
|
assert (
|
||
|
cell2.border.top.style
|
||
|
== cell2.border.right.style
|
||
|
== cell2.border.bottom.style
|
||
|
== cell2.border.left.style
|
||
|
== "medium"
|
||
|
)
|
||
|
elif ref == "C6":
|
||
|
assert not cell1.font.italic
|
||
|
assert cell2.font.italic
|
||
|
elif ref == "D7":
|
||
|
assert cell1.alignment.horizontal != cell2.alignment.horizontal
|
||
|
assert cell2.alignment.horizontal == "right"
|
||
|
elif ref == "B8":
|
||
|
assert cell1.fill.fgColor.rgb != cell2.fill.fgColor.rgb
|
||
|
assert cell1.fill.patternType != cell2.fill.patternType
|
||
|
assert cell2.fill.fgColor.rgb == alpha + "FF0000"
|
||
|
assert cell2.fill.patternType == "solid"
|
||
|
elif ref == "B9":
|
||
|
assert cell1.number_format == "General"
|
||
|
assert cell2.number_format == "0%"
|
||
|
else:
|
||
|
assert_equal_style(cell1, cell2, engine)
|
||
|
|
||
|
assert cell1.value == cell2.value
|
||
|
n_cells += 1
|
||
|
|
||
|
assert n_cells == (11 + 1) * (3 + 1)
|
||
|
|
||
|
# (3) check styling with custom converter
|
||
|
n_cells = 0
|
||
|
for col1, col2 in zip(wb["frame"].columns, wb["custom"].columns):
|
||
|
assert len(col1) == len(col2)
|
||
|
for cell1, cell2 in zip(col1, col2):
|
||
|
ref = f"{cell2.column}{cell2.row:d}"
|
||
|
if ref in ("B2", "C3", "D4", "B5", "C6", "D7", "B8", "B9"):
|
||
|
assert not cell1.font.bold
|
||
|
assert cell2.font.bold
|
||
|
else:
|
||
|
assert_equal_style(cell1, cell2, engine)
|
||
|
|
||
|
assert cell1.value == cell2.value
|
||
|
n_cells += 1
|
||
|
|
||
|
assert n_cells == (11 + 1) * (3 + 1)
|