test_openpyxl.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. import contextlib
  2. from pathlib import Path
  3. import re
  4. import numpy as np
  5. import pytest
  6. import pandas as pd
  7. from pandas import DataFrame
  8. import pandas._testing as tm
  9. from pandas.io.excel import (
  10. ExcelWriter,
  11. _OpenpyxlWriter,
  12. )
  13. openpyxl = pytest.importorskip("openpyxl")
  14. pytestmark = pytest.mark.parametrize("ext", [".xlsx"])
  15. def test_to_excel_styleconverter(ext):
  16. from openpyxl import styles
  17. hstyle = {
  18. "font": {"color": "00FF0000", "bold": True},
  19. "borders": {"top": "thin", "right": "thin", "bottom": "thin", "left": "thin"},
  20. "alignment": {"horizontal": "center", "vertical": "top"},
  21. "fill": {"patternType": "solid", "fgColor": {"rgb": "006666FF", "tint": 0.3}},
  22. "number_format": {"format_code": "0.00"},
  23. "protection": {"locked": True, "hidden": False},
  24. }
  25. font_color = styles.Color("00FF0000")
  26. font = styles.Font(bold=True, color=font_color)
  27. side = styles.Side(style=styles.borders.BORDER_THIN)
  28. border = styles.Border(top=side, right=side, bottom=side, left=side)
  29. alignment = styles.Alignment(horizontal="center", vertical="top")
  30. fill_color = styles.Color(rgb="006666FF", tint=0.3)
  31. fill = styles.PatternFill(patternType="solid", fgColor=fill_color)
  32. number_format = "0.00"
  33. protection = styles.Protection(locked=True, hidden=False)
  34. kw = _OpenpyxlWriter._convert_to_style_kwargs(hstyle)
  35. assert kw["font"] == font
  36. assert kw["border"] == border
  37. assert kw["alignment"] == alignment
  38. assert kw["fill"] == fill
  39. assert kw["number_format"] == number_format
  40. assert kw["protection"] == protection
  41. def test_write_cells_merge_styled(ext):
  42. from pandas.io.formats.excel import ExcelCell
  43. sheet_name = "merge_styled"
  44. sty_b1 = {"font": {"color": "00FF0000"}}
  45. sty_a2 = {"font": {"color": "0000FF00"}}
  46. initial_cells = [
  47. ExcelCell(col=1, row=0, val=42, style=sty_b1),
  48. ExcelCell(col=0, row=1, val=99, style=sty_a2),
  49. ]
  50. sty_merged = {"font": {"color": "000000FF", "bold": True}}
  51. sty_kwargs = _OpenpyxlWriter._convert_to_style_kwargs(sty_merged)
  52. openpyxl_sty_merged = sty_kwargs["font"]
  53. merge_cells = [
  54. ExcelCell(
  55. col=0, row=0, val="pandas", mergestart=1, mergeend=1, style=sty_merged
  56. )
  57. ]
  58. with tm.ensure_clean(ext) as path:
  59. with _OpenpyxlWriter(path) as writer:
  60. writer._write_cells(initial_cells, sheet_name=sheet_name)
  61. writer._write_cells(merge_cells, sheet_name=sheet_name)
  62. wks = writer.sheets[sheet_name]
  63. xcell_b1 = wks["B1"]
  64. xcell_a2 = wks["A2"]
  65. assert xcell_b1.font == openpyxl_sty_merged
  66. assert xcell_a2.font == openpyxl_sty_merged
  67. @pytest.mark.parametrize("iso_dates", [True, False])
  68. def test_engine_kwargs_write(ext, iso_dates):
  69. # GH 42286 GH 43445
  70. engine_kwargs = {"iso_dates": iso_dates}
  71. with tm.ensure_clean(ext) as f:
  72. with ExcelWriter(f, engine="openpyxl", engine_kwargs=engine_kwargs) as writer:
  73. assert writer.book.iso_dates == iso_dates
  74. # ExcelWriter won't allow us to close without writing something
  75. DataFrame().to_excel(writer)
  76. def test_engine_kwargs_append_invalid(ext):
  77. # GH 43445
  78. # test whether an invalid engine kwargs actually raises
  79. with tm.ensure_clean(ext) as f:
  80. DataFrame(["hello", "world"]).to_excel(f)
  81. with pytest.raises(
  82. TypeError,
  83. match=re.escape(
  84. "load_workbook() got an unexpected keyword argument 'apple_banana'"
  85. ),
  86. ):
  87. with ExcelWriter(
  88. f, engine="openpyxl", mode="a", engine_kwargs={"apple_banana": "fruit"}
  89. ) as writer:
  90. # ExcelWriter needs us to write something to close properly
  91. DataFrame(["good"]).to_excel(writer, sheet_name="Sheet2")
  92. @pytest.mark.parametrize("data_only, expected", [(True, 0), (False, "=1+1")])
  93. def test_engine_kwargs_append_data_only(ext, data_only, expected):
  94. # GH 43445
  95. # tests whether the data_only engine_kwarg actually works well for
  96. # openpyxl's load_workbook
  97. with tm.ensure_clean(ext) as f:
  98. DataFrame(["=1+1"]).to_excel(f)
  99. with ExcelWriter(
  100. f, engine="openpyxl", mode="a", engine_kwargs={"data_only": data_only}
  101. ) as writer:
  102. assert writer.sheets["Sheet1"]["B2"].value == expected
  103. # ExcelWriter needs us to writer something to close properly?
  104. DataFrame().to_excel(writer, sheet_name="Sheet2")
  105. @pytest.mark.parametrize(
  106. "mode,expected", [("w", ["baz"]), ("a", ["foo", "bar", "baz"])]
  107. )
  108. def test_write_append_mode(ext, mode, expected):
  109. df = DataFrame([1], columns=["baz"])
  110. with tm.ensure_clean(ext) as f:
  111. wb = openpyxl.Workbook()
  112. wb.worksheets[0].title = "foo"
  113. wb.worksheets[0]["A1"].value = "foo"
  114. wb.create_sheet("bar")
  115. wb.worksheets[1]["A1"].value = "bar"
  116. wb.save(f)
  117. with ExcelWriter(f, engine="openpyxl", mode=mode) as writer:
  118. df.to_excel(writer, sheet_name="baz", index=False)
  119. with contextlib.closing(openpyxl.load_workbook(f)) as wb2:
  120. result = [sheet.title for sheet in wb2.worksheets]
  121. assert result == expected
  122. for index, cell_value in enumerate(expected):
  123. assert wb2.worksheets[index]["A1"].value == cell_value
  124. @pytest.mark.parametrize(
  125. "if_sheet_exists,num_sheets,expected",
  126. [
  127. ("new", 2, ["apple", "banana"]),
  128. ("replace", 1, ["pear"]),
  129. ("overlay", 1, ["pear", "banana"]),
  130. ],
  131. )
  132. def test_if_sheet_exists_append_modes(ext, if_sheet_exists, num_sheets, expected):
  133. # GH 40230
  134. df1 = DataFrame({"fruit": ["apple", "banana"]})
  135. df2 = DataFrame({"fruit": ["pear"]})
  136. with tm.ensure_clean(ext) as f:
  137. df1.to_excel(f, engine="openpyxl", sheet_name="foo", index=False)
  138. with ExcelWriter(
  139. f, engine="openpyxl", mode="a", if_sheet_exists=if_sheet_exists
  140. ) as writer:
  141. df2.to_excel(writer, sheet_name="foo", index=False)
  142. with contextlib.closing(openpyxl.load_workbook(f)) as wb:
  143. assert len(wb.sheetnames) == num_sheets
  144. assert wb.sheetnames[0] == "foo"
  145. result = pd.read_excel(wb, "foo", engine="openpyxl")
  146. assert list(result["fruit"]) == expected
  147. if len(wb.sheetnames) == 2:
  148. result = pd.read_excel(wb, wb.sheetnames[1], engine="openpyxl")
  149. tm.assert_frame_equal(result, df2)
  150. @pytest.mark.parametrize(
  151. "startrow, startcol, greeting, goodbye",
  152. [
  153. (0, 0, ["poop", "world"], ["goodbye", "people"]),
  154. (0, 1, ["hello", "world"], ["poop", "people"]),
  155. (1, 0, ["hello", "poop"], ["goodbye", "people"]),
  156. (1, 1, ["hello", "world"], ["goodbye", "poop"]),
  157. ],
  158. )
  159. def test_append_overlay_startrow_startcol(ext, startrow, startcol, greeting, goodbye):
  160. df1 = DataFrame({"greeting": ["hello", "world"], "goodbye": ["goodbye", "people"]})
  161. df2 = DataFrame(["poop"])
  162. with tm.ensure_clean(ext) as f:
  163. df1.to_excel(f, engine="openpyxl", sheet_name="poo", index=False)
  164. with ExcelWriter(
  165. f, engine="openpyxl", mode="a", if_sheet_exists="overlay"
  166. ) as writer:
  167. # use startrow+1 because we don't have a header
  168. df2.to_excel(
  169. writer,
  170. index=False,
  171. header=False,
  172. startrow=startrow + 1,
  173. startcol=startcol,
  174. sheet_name="poo",
  175. )
  176. result = pd.read_excel(f, sheet_name="poo", engine="openpyxl")
  177. expected = DataFrame({"greeting": greeting, "goodbye": goodbye})
  178. tm.assert_frame_equal(result, expected)
  179. @pytest.mark.parametrize(
  180. "if_sheet_exists,msg",
  181. [
  182. (
  183. "invalid",
  184. "'invalid' is not valid for if_sheet_exists. Valid options "
  185. "are 'error', 'new', 'replace' and 'overlay'.",
  186. ),
  187. (
  188. "error",
  189. "Sheet 'foo' already exists and if_sheet_exists is set to 'error'.",
  190. ),
  191. (
  192. None,
  193. "Sheet 'foo' already exists and if_sheet_exists is set to 'error'.",
  194. ),
  195. ],
  196. )
  197. def test_if_sheet_exists_raises(ext, if_sheet_exists, msg):
  198. # GH 40230
  199. df = DataFrame({"fruit": ["pear"]})
  200. with tm.ensure_clean(ext) as f:
  201. with pytest.raises(ValueError, match=re.escape(msg)):
  202. df.to_excel(f, "foo", engine="openpyxl")
  203. with ExcelWriter(
  204. f, engine="openpyxl", mode="a", if_sheet_exists=if_sheet_exists
  205. ) as writer:
  206. df.to_excel(writer, sheet_name="foo")
  207. def test_to_excel_with_openpyxl_engine(ext):
  208. # GH 29854
  209. with tm.ensure_clean(ext) as filename:
  210. df1 = DataFrame({"A": np.linspace(1, 10, 10)})
  211. df2 = DataFrame({"B": np.linspace(1, 20, 10)})
  212. df = pd.concat([df1, df2], axis=1)
  213. styled = df.style.applymap(
  214. lambda val: f"color: {'red' if val < 0 else 'black'}"
  215. ).highlight_max()
  216. styled.to_excel(filename, engine="openpyxl")
  217. @pytest.mark.parametrize("read_only", [True, False])
  218. def test_read_workbook(datapath, ext, read_only):
  219. # GH 39528
  220. filename = datapath("io", "data", "excel", "test1" + ext)
  221. with contextlib.closing(
  222. openpyxl.load_workbook(filename, read_only=read_only)
  223. ) as wb:
  224. result = pd.read_excel(wb, engine="openpyxl")
  225. expected = pd.read_excel(filename)
  226. tm.assert_frame_equal(result, expected)
  227. @pytest.mark.parametrize(
  228. "header, expected_data",
  229. [
  230. (
  231. 0,
  232. {
  233. "Title": [np.nan, "A", 1, 2, 3],
  234. "Unnamed: 1": [np.nan, "B", 4, 5, 6],
  235. "Unnamed: 2": [np.nan, "C", 7, 8, 9],
  236. },
  237. ),
  238. (2, {"A": [1, 2, 3], "B": [4, 5, 6], "C": [7, 8, 9]}),
  239. ],
  240. )
  241. @pytest.mark.parametrize(
  242. "filename", ["dimension_missing", "dimension_small", "dimension_large"]
  243. )
  244. # When read_only is None, use read_excel instead of a workbook
  245. @pytest.mark.parametrize("read_only", [True, False, None])
  246. def test_read_with_bad_dimension(
  247. datapath, ext, header, expected_data, filename, read_only
  248. ):
  249. # GH 38956, 39001 - no/incorrect dimension information
  250. path = datapath("io", "data", "excel", f"{filename}{ext}")
  251. if read_only is None:
  252. result = pd.read_excel(path, header=header)
  253. else:
  254. with contextlib.closing(
  255. openpyxl.load_workbook(path, read_only=read_only)
  256. ) as wb:
  257. result = pd.read_excel(wb, engine="openpyxl", header=header)
  258. expected = DataFrame(expected_data)
  259. tm.assert_frame_equal(result, expected)
  260. def test_append_mode_file(ext):
  261. # GH 39576
  262. df = DataFrame()
  263. with tm.ensure_clean(ext) as f:
  264. df.to_excel(f, engine="openpyxl")
  265. with ExcelWriter(
  266. f, mode="a", engine="openpyxl", if_sheet_exists="new"
  267. ) as writer:
  268. df.to_excel(writer)
  269. # make sure that zip files are not concatenated by making sure that
  270. # "docProps/app.xml" only occurs twice in the file
  271. data = Path(f).read_bytes()
  272. first = data.find(b"docProps/app.xml")
  273. second = data.find(b"docProps/app.xml", first + 1)
  274. third = data.find(b"docProps/app.xml", second + 1)
  275. assert second != -1 and third == -1
  276. # When read_only is None, use read_excel instead of a workbook
  277. @pytest.mark.parametrize("read_only", [True, False, None])
  278. def test_read_with_empty_trailing_rows(datapath, ext, read_only):
  279. # GH 39181
  280. path = datapath("io", "data", "excel", f"empty_trailing_rows{ext}")
  281. if read_only is None:
  282. result = pd.read_excel(path)
  283. else:
  284. with contextlib.closing(
  285. openpyxl.load_workbook(path, read_only=read_only)
  286. ) as wb:
  287. result = pd.read_excel(wb, engine="openpyxl")
  288. expected = DataFrame(
  289. {
  290. "Title": [np.nan, "A", 1, 2, 3],
  291. "Unnamed: 1": [np.nan, "B", 4, 5, 6],
  292. "Unnamed: 2": [np.nan, "C", 7, 8, 9],
  293. }
  294. )
  295. tm.assert_frame_equal(result, expected)
  296. # When read_only is None, use read_excel instead of a workbook
  297. @pytest.mark.parametrize("read_only", [True, False, None])
  298. def test_read_empty_with_blank_row(datapath, ext, read_only):
  299. # GH 39547 - empty excel file with a row that has no data
  300. path = datapath("io", "data", "excel", f"empty_with_blank_row{ext}")
  301. if read_only is None:
  302. result = pd.read_excel(path)
  303. else:
  304. with contextlib.closing(
  305. openpyxl.load_workbook(path, read_only=read_only)
  306. ) as wb:
  307. result = pd.read_excel(wb, engine="openpyxl")
  308. expected = DataFrame()
  309. tm.assert_frame_equal(result, expected)
  310. def test_book_and_sheets_consistent(ext):
  311. # GH#45687 - Ensure sheets is updated if user modifies book
  312. with tm.ensure_clean(ext) as f:
  313. with ExcelWriter(f, engine="openpyxl") as writer:
  314. assert writer.sheets == {}
  315. sheet = writer.book.create_sheet("test_name", 0)
  316. assert writer.sheets == {"test_name": sheet}
  317. def test_ints_spelled_with_decimals(datapath, ext):
  318. # GH 46988 - openpyxl returns this sheet with floats
  319. path = datapath("io", "data", "excel", f"ints_spelled_with_decimals{ext}")
  320. result = pd.read_excel(path)
  321. expected = DataFrame(range(2, 12), columns=[1])
  322. tm.assert_frame_equal(result, expected)
  323. def test_read_multiindex_header_no_index_names(datapath, ext):
  324. # GH#47487
  325. path = datapath("io", "data", "excel", f"multiindex_no_index_names{ext}")
  326. result = pd.read_excel(path, index_col=[0, 1, 2], header=[0, 1, 2])
  327. expected = DataFrame(
  328. [[np.nan, "x", "x", "x"], ["x", np.nan, np.nan, np.nan]],
  329. columns=pd.MultiIndex.from_tuples(
  330. [("X", "Y", "A1"), ("X", "Y", "A2"), ("XX", "YY", "B1"), ("XX", "YY", "B2")]
  331. ),
  332. index=pd.MultiIndex.from_tuples([("A", "AA", "AAA"), ("A", "BB", "BBB")]),
  333. )
  334. tm.assert_frame_equal(result, expected)