test_style.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import contextlib
  2. import time
  3. import numpy as np
  4. import pytest
  5. import pandas.util._test_decorators as td
  6. from pandas import (
  7. DataFrame,
  8. read_excel,
  9. )
  10. import pandas._testing as tm
  11. from pandas.io.excel import ExcelWriter
  12. from pandas.io.formats.excel import ExcelFormatter
  13. pytest.importorskip("jinja2")
  14. # jinja2 is currently required for Styler.__init__(). Technically Styler.to_excel
  15. # could compute styles and render to excel without jinja2, since there is no
  16. # 'template' file, but this needs the import error to delayed until render time.
  17. def assert_equal_cell_styles(cell1, cell2):
  18. # TODO: should find a better way to check equality
  19. assert cell1.alignment.__dict__ == cell2.alignment.__dict__
  20. assert cell1.border.__dict__ == cell2.border.__dict__
  21. assert cell1.fill.__dict__ == cell2.fill.__dict__
  22. assert cell1.font.__dict__ == cell2.font.__dict__
  23. assert cell1.number_format == cell2.number_format
  24. assert cell1.protection.__dict__ == cell2.protection.__dict__
  25. @pytest.mark.parametrize(
  26. "engine",
  27. ["xlsxwriter", "openpyxl"],
  28. )
  29. def test_styler_to_excel_unstyled(engine):
  30. # compare DataFrame.to_excel and Styler.to_excel when no styles applied
  31. pytest.importorskip(engine)
  32. df = DataFrame(np.random.randn(2, 2))
  33. with tm.ensure_clean(".xlsx") as path:
  34. with ExcelWriter(path, engine=engine) as writer:
  35. df.to_excel(writer, sheet_name="dataframe")
  36. df.style.to_excel(writer, sheet_name="unstyled")
  37. openpyxl = pytest.importorskip("openpyxl") # test loading only with openpyxl
  38. with contextlib.closing(openpyxl.load_workbook(path)) as wb:
  39. for col1, col2 in zip(wb["dataframe"].columns, wb["unstyled"].columns):
  40. assert len(col1) == len(col2)
  41. for cell1, cell2 in zip(col1, col2):
  42. assert cell1.value == cell2.value
  43. assert_equal_cell_styles(cell1, cell2)
  44. shared_style_params = [
  45. (
  46. "background-color: #111222",
  47. ["fill", "fgColor", "rgb"],
  48. {"xlsxwriter": "FF111222", "openpyxl": "00111222"},
  49. ),
  50. (
  51. "color: #111222",
  52. ["font", "color", "value"],
  53. {"xlsxwriter": "FF111222", "openpyxl": "00111222"},
  54. ),
  55. ("font-family: Arial;", ["font", "name"], "arial"),
  56. ("font-weight: bold;", ["font", "b"], True),
  57. ("font-style: italic;", ["font", "i"], True),
  58. ("text-decoration: underline;", ["font", "u"], "single"),
  59. ("number-format: $??,???.00;", ["number_format"], "$??,???.00"),
  60. ("text-align: left;", ["alignment", "horizontal"], "left"),
  61. (
  62. "vertical-align: bottom;",
  63. ["alignment", "vertical"],
  64. {"xlsxwriter": None, "openpyxl": "bottom"}, # xlsxwriter Fails
  65. ),
  66. ("vertical-align: middle;", ["alignment", "vertical"], "center"),
  67. # Border widths
  68. ("border-left: 2pt solid red", ["border", "left", "style"], "medium"),
  69. ("border-left: 1pt dotted red", ["border", "left", "style"], "dotted"),
  70. ("border-left: 2pt dotted red", ["border", "left", "style"], "mediumDashDotDot"),
  71. ("border-left: 1pt dashed red", ["border", "left", "style"], "dashed"),
  72. ("border-left: 2pt dashed red", ["border", "left", "style"], "mediumDashed"),
  73. ("border-left: 1pt solid red", ["border", "left", "style"], "thin"),
  74. ("border-left: 3pt solid red", ["border", "left", "style"], "thick"),
  75. # Border expansion
  76. (
  77. "border-left: 2pt solid #111222",
  78. ["border", "left", "color", "rgb"],
  79. {"xlsxwriter": "FF111222", "openpyxl": "00111222"},
  80. ),
  81. ("border: 1pt solid red", ["border", "top", "style"], "thin"),
  82. (
  83. "border: 1pt solid #111222",
  84. ["border", "top", "color", "rgb"],
  85. {"xlsxwriter": "FF111222", "openpyxl": "00111222"},
  86. ),
  87. ("border: 1pt solid red", ["border", "right", "style"], "thin"),
  88. (
  89. "border: 1pt solid #111222",
  90. ["border", "right", "color", "rgb"],
  91. {"xlsxwriter": "FF111222", "openpyxl": "00111222"},
  92. ),
  93. ("border: 1pt solid red", ["border", "bottom", "style"], "thin"),
  94. (
  95. "border: 1pt solid #111222",
  96. ["border", "bottom", "color", "rgb"],
  97. {"xlsxwriter": "FF111222", "openpyxl": "00111222"},
  98. ),
  99. ("border: 1pt solid red", ["border", "left", "style"], "thin"),
  100. (
  101. "border: 1pt solid #111222",
  102. ["border", "left", "color", "rgb"],
  103. {"xlsxwriter": "FF111222", "openpyxl": "00111222"},
  104. ),
  105. # Border styles
  106. (
  107. "border-left-style: hair; border-left-color: black",
  108. ["border", "left", "style"],
  109. "hair",
  110. ),
  111. ]
  112. @pytest.mark.parametrize(
  113. "engine",
  114. ["xlsxwriter", "openpyxl"],
  115. )
  116. @pytest.mark.parametrize("css, attrs, expected", shared_style_params)
  117. def test_styler_to_excel_basic(engine, css, attrs, expected):
  118. pytest.importorskip(engine)
  119. df = DataFrame(np.random.randn(1, 1))
  120. styler = df.style.applymap(lambda x: css)
  121. with tm.ensure_clean(".xlsx") as path:
  122. with ExcelWriter(path, engine=engine) as writer:
  123. df.to_excel(writer, sheet_name="dataframe")
  124. styler.to_excel(writer, sheet_name="styled")
  125. openpyxl = pytest.importorskip("openpyxl") # test loading only with openpyxl
  126. with contextlib.closing(openpyxl.load_workbook(path)) as wb:
  127. # test unstyled data cell does not have expected styles
  128. # test styled cell has expected styles
  129. u_cell, s_cell = wb["dataframe"].cell(2, 2), wb["styled"].cell(2, 2)
  130. for attr in attrs:
  131. u_cell, s_cell = getattr(u_cell, attr, None), getattr(s_cell, attr)
  132. if isinstance(expected, dict):
  133. assert u_cell is None or u_cell != expected[engine]
  134. assert s_cell == expected[engine]
  135. else:
  136. assert u_cell is None or u_cell != expected
  137. assert s_cell == expected
  138. @pytest.mark.parametrize(
  139. "engine",
  140. ["xlsxwriter", "openpyxl"],
  141. )
  142. @pytest.mark.parametrize("css, attrs, expected", shared_style_params)
  143. def test_styler_to_excel_basic_indexes(engine, css, attrs, expected):
  144. pytest.importorskip(engine)
  145. df = DataFrame(np.random.randn(1, 1))
  146. styler = df.style
  147. styler.applymap_index(lambda x: css, axis=0)
  148. styler.applymap_index(lambda x: css, axis=1)
  149. null_styler = df.style
  150. null_styler.applymap(lambda x: "null: css;")
  151. null_styler.applymap_index(lambda x: "null: css;", axis=0)
  152. null_styler.applymap_index(lambda x: "null: css;", axis=1)
  153. with tm.ensure_clean(".xlsx") as path:
  154. with ExcelWriter(path, engine=engine) as writer:
  155. null_styler.to_excel(writer, sheet_name="null_styled")
  156. styler.to_excel(writer, sheet_name="styled")
  157. openpyxl = pytest.importorskip("openpyxl") # test loading only with openpyxl
  158. with contextlib.closing(openpyxl.load_workbook(path)) as wb:
  159. # test null styled index cells does not have expected styles
  160. # test styled cell has expected styles
  161. ui_cell, si_cell = wb["null_styled"].cell(2, 1), wb["styled"].cell(2, 1)
  162. uc_cell, sc_cell = wb["null_styled"].cell(1, 2), wb["styled"].cell(1, 2)
  163. for attr in attrs:
  164. ui_cell, si_cell = getattr(ui_cell, attr, None), getattr(si_cell, attr)
  165. uc_cell, sc_cell = getattr(uc_cell, attr, None), getattr(sc_cell, attr)
  166. if isinstance(expected, dict):
  167. assert ui_cell is None or ui_cell != expected[engine]
  168. assert si_cell == expected[engine]
  169. assert uc_cell is None or uc_cell != expected[engine]
  170. assert sc_cell == expected[engine]
  171. else:
  172. assert ui_cell is None or ui_cell != expected
  173. assert si_cell == expected
  174. assert uc_cell is None or uc_cell != expected
  175. assert sc_cell == expected
  176. # From https://openpyxl.readthedocs.io/en/stable/api/openpyxl.styles.borders.html
  177. # Note: Leaving behavior of "width"-type styles undefined; user should use border-width
  178. # instead
  179. excel_border_styles = [
  180. # "thin",
  181. "dashed",
  182. "mediumDashDot",
  183. "dashDotDot",
  184. "hair",
  185. "dotted",
  186. "mediumDashDotDot",
  187. # "medium",
  188. "double",
  189. "dashDot",
  190. "slantDashDot",
  191. # "thick",
  192. "mediumDashed",
  193. ]
  194. @pytest.mark.parametrize(
  195. "engine",
  196. ["xlsxwriter", "openpyxl"],
  197. )
  198. @pytest.mark.parametrize("border_style", excel_border_styles)
  199. def test_styler_to_excel_border_style(engine, border_style):
  200. css = f"border-left: {border_style} black thin"
  201. attrs = ["border", "left", "style"]
  202. expected = border_style
  203. pytest.importorskip(engine)
  204. df = DataFrame(np.random.randn(1, 1))
  205. styler = df.style.applymap(lambda x: css)
  206. with tm.ensure_clean(".xlsx") as path:
  207. with ExcelWriter(path, engine=engine) as writer:
  208. df.to_excel(writer, sheet_name="dataframe")
  209. styler.to_excel(writer, sheet_name="styled")
  210. openpyxl = pytest.importorskip("openpyxl") # test loading only with openpyxl
  211. with contextlib.closing(openpyxl.load_workbook(path)) as wb:
  212. # test unstyled data cell does not have expected styles
  213. # test styled cell has expected styles
  214. u_cell, s_cell = wb["dataframe"].cell(2, 2), wb["styled"].cell(2, 2)
  215. for attr in attrs:
  216. u_cell, s_cell = getattr(u_cell, attr, None), getattr(s_cell, attr)
  217. if isinstance(expected, dict):
  218. assert u_cell is None or u_cell != expected[engine]
  219. assert s_cell == expected[engine]
  220. else:
  221. assert u_cell is None or u_cell != expected
  222. assert s_cell == expected
  223. def test_styler_custom_converter():
  224. openpyxl = pytest.importorskip("openpyxl")
  225. def custom_converter(css):
  226. return {"font": {"color": {"rgb": "111222"}}}
  227. df = DataFrame(np.random.randn(1, 1))
  228. styler = df.style.applymap(lambda x: "color: #888999")
  229. with tm.ensure_clean(".xlsx") as path:
  230. with ExcelWriter(path, engine="openpyxl") as writer:
  231. ExcelFormatter(styler, style_converter=custom_converter).write(
  232. writer, sheet_name="custom"
  233. )
  234. with contextlib.closing(openpyxl.load_workbook(path)) as wb:
  235. assert wb["custom"].cell(2, 2).font.color.value == "00111222"
  236. @pytest.mark.single_cpu
  237. @td.skip_if_not_us_locale
  238. def test_styler_to_s3(s3_resource, s3so):
  239. # GH#46381
  240. mock_bucket_name, target_file = "pandas-test", "test.xlsx"
  241. df = DataFrame({"x": [1, 2, 3], "y": [2, 4, 6]})
  242. styler = df.style.set_sticky(axis="index")
  243. styler.to_excel(f"s3://{mock_bucket_name}/{target_file}", storage_options=s3so)
  244. timeout = 5
  245. while True:
  246. if target_file in (
  247. obj.key for obj in s3_resource.Bucket("pandas-test").objects.all()
  248. ):
  249. break
  250. time.sleep(0.1)
  251. timeout -= 0.1
  252. assert timeout > 0, "Timed out waiting for file to appear on moto"
  253. result = read_excel(
  254. f"s3://{mock_bucket_name}/{target_file}", index_col=0, storage_options=s3so
  255. )
  256. tm.assert_frame_equal(result, df)