Skip to content

Commit fd95026

Browse files
authored
Pass through Engine kwargs in ExcelWriter (#43445)
1 parent 56b3e59 commit fd95026

File tree

8 files changed

+125
-41
lines changed

8 files changed

+125
-41
lines changed

doc/source/whatsnew/v1.4.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,7 @@ I/O
659659
- Bug in :func:`json_normalize` where reading data with missing multi-level metadata would not respect errors="ignore" (:issue:`44312`)
660660
- Bug in :func:`read_csv` with :code:`float_precision="round_trip"` which did not skip initial/trailing whitespace (:issue:`43713`)
661661
- Bug in dumping/loading a :class:`DataFrame` with ``yaml.dump(frame)`` (:issue:`42748`)
662+
- Bug in :class:`ExcelWriter`, where ``engine_kwargs`` were not passed through to all engines (:issue:`43442`)
662663
- Bug in :func:`read_csv` raising ``ValueError`` when ``parse_dates`` was used with ``MultiIndex`` columns (:issue:`8991`)
663664
- Bug in :func:`read_csv` raising ``AttributeError`` when attempting to read a .csv file and infer index column dtype from an nullable integer type (:issue:`44079`)
664665
- :meth:`DataFrame.to_csv` and :meth:`Series.to_csv` with ``compression`` set to ``'zip'`` no longer create a zip file containing a file ending with ".zip". Instead, they try to infer the inner file name more smartly. (:issue:`39465`)

pandas/io/excel/_base.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,13 @@ class ExcelWriter(metaclass=abc.ABCMeta):
721721
Added ``overlay`` option
722722
723723
engine_kwargs : dict, optional
724-
Keyword arguments to be passed into the engine.
724+
Keyword arguments to be passed into the engine. These will be passed to
725+
the following functions of the respective engines:
726+
727+
* xlsxwriter: ``xlsxwriter.Workbook(file, **engine_kwargs)``
728+
* openpyxl (write mode): ``openpyxl.Workbook(**engine_kwargs)``
729+
* openpyxl (append mode): ``openpyxl.load_workbook(file, **engine_kwargs)``
730+
* odswriter: ``odf.opendocument.OpenDocumentSpreadsheet(**engine_kwargs)``
725731
726732
.. versionadded:: 1.3.0
727733
**kwargs : dict, optional
@@ -823,6 +829,26 @@ class ExcelWriter(metaclass=abc.ABCMeta):
823829
... with zf.open("filename.xlsx", "w") as buffer:
824830
... with pd.ExcelWriter(buffer) as writer:
825831
... df.to_excel(writer)
832+
833+
You can specify additional arguments to the underlying engine:
834+
835+
>>> with pd.ExcelWriter(
836+
... "path_to_file.xlsx",
837+
... engine="xlsxwriter",
838+
... engine_kwargs={"options": {"nan_inf_to_errors": True}}
839+
... ) as writer:
840+
... df.to_excel(writer)
841+
842+
In append mode, ``engine_kwargs`` are passed through to
843+
openpyxl's ``load_workbook``:
844+
845+
>>> with pd.ExcelWriter(
846+
... "path_to_file.xlsx",
847+
... engine="openpyxl",
848+
... mode="a",
849+
... engine_kwargs={"keep_vba": True}
850+
... ) as writer:
851+
... df.to_excel(writer, sheet_name="Sheet2")
826852
"""
827853

828854
# Defining an ExcelWriter implementation (see abstract methods for more...)

pandas/io/excel/_odswriter.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
from pandas._typing import StorageOptions
1212

1313
from pandas.io.excel._base import ExcelWriter
14-
from pandas.io.excel._util import validate_freeze_panes
14+
from pandas.io.excel._util import (
15+
combine_kwargs,
16+
validate_freeze_panes,
17+
)
1518
from pandas.io.formats.excel import ExcelCell
1619

1720

@@ -44,7 +47,9 @@ def __init__(
4447
engine_kwargs=engine_kwargs,
4548
)
4649

47-
self.book = OpenDocumentSpreadsheet()
50+
engine_kwargs = combine_kwargs(engine_kwargs, kwargs)
51+
52+
self.book = OpenDocumentSpreadsheet(**engine_kwargs)
4853
self._style_dict: dict[str, str] = {}
4954

5055
def save(self) -> None:

pandas/io/excel/_openpyxl.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,13 @@ def __init__(
6363
if "r+" in self.mode: # Load from existing workbook
6464
from openpyxl import load_workbook
6565

66-
self.book = load_workbook(self.handles.handle)
66+
self.book = load_workbook(self.handles.handle, **engine_kwargs)
6767
self.handles.handle.seek(0)
6868
self.sheets = {name: self.book[name] for name in self.book.sheetnames}
6969

7070
else:
7171
# Create workbook object with default optimized_write=True.
72-
self.book = Workbook()
72+
self.book = Workbook(**engine_kwargs)
7373

7474
if self.book.worksheets:
7575
self.book.remove(self.book.worksheets[0])

pandas/io/excel/_xlwt.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def __init__(
5353

5454
if encoding is None:
5555
encoding = "ascii"
56-
self.book = xlwt.Workbook(encoding=encoding)
56+
self.book = xlwt.Workbook(encoding=encoding, **engine_kwargs)
5757
self.fm_datetime = xlwt.easyxf(num_format_str=self.datetime_format)
5858
self.fm_date = xlwt.easyxf(num_format_str=self.date_format)
5959

pandas/tests/io/excel/test_odswriter.py

+32-15
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,40 @@ def test_write_append_mode_raises(ext):
1919
ExcelWriter(f, engine="odf", mode="a")
2020

2121

22-
@pytest.mark.parametrize("nan_inf_to_errors", [True, False])
23-
def test_kwargs(ext, nan_inf_to_errors):
22+
def test_kwargs(ext):
2423
# GH 42286
25-
# odswriter doesn't utilize kwargs, nothing to check except that it works
26-
kwargs = {"options": {"nan_inf_to_errors": nan_inf_to_errors}}
24+
# GH 43445
25+
# test for error: OpenDocumentSpreadsheet does not accept any arguments
26+
kwargs = {"kwarg": 1}
2727
with tm.ensure_clean(ext) as f:
2828
msg = re.escape("Use of **kwargs is deprecated")
29-
with tm.assert_produces_warning(FutureWarning, match=msg):
30-
with ExcelWriter(f, engine="odf", **kwargs) as _:
31-
pass
32-
33-
34-
@pytest.mark.parametrize("nan_inf_to_errors", [True, False])
35-
def test_engine_kwargs(ext, nan_inf_to_errors):
29+
error = re.escape(
30+
"OpenDocumentSpreadsheet() got an unexpected keyword argument 'kwarg'"
31+
)
32+
with pytest.raises(
33+
TypeError,
34+
match=error,
35+
):
36+
with tm.assert_produces_warning(FutureWarning, match=msg):
37+
with ExcelWriter(f, engine="odf", **kwargs) as _:
38+
pass
39+
40+
41+
@pytest.mark.parametrize("engine_kwargs", [None, {"kwarg": 1}])
42+
def test_engine_kwargs(ext, engine_kwargs):
3643
# GH 42286
37-
# odswriter doesn't utilize engine_kwargs, nothing to check except that it works
38-
engine_kwargs = {"options": {"nan_inf_to_errors": nan_inf_to_errors}}
44+
# GH 43445
45+
# test for error: OpenDocumentSpreadsheet does not accept any arguments
3946
with tm.ensure_clean(ext) as f:
40-
with ExcelWriter(f, engine="odf", engine_kwargs=engine_kwargs) as _:
41-
pass
47+
if engine_kwargs is not None:
48+
error = re.escape(
49+
"OpenDocumentSpreadsheet() got an unexpected keyword argument 'kwarg'"
50+
)
51+
with pytest.raises(
52+
TypeError,
53+
match=error,
54+
):
55+
ExcelWriter(f, engine="odf", engine_kwargs=engine_kwargs)
56+
else:
57+
with ExcelWriter(f, engine="odf", engine_kwargs=engine_kwargs) as _:
58+
pass

pandas/tests/io/excel/test_openpyxl.py

+43-10
Original file line numberDiff line numberDiff line change
@@ -85,30 +85,63 @@ def test_write_cells_merge_styled(ext):
8585
assert xcell_a2.font == openpyxl_sty_merged
8686

8787

88-
@pytest.mark.parametrize("write_only", [True, False])
89-
def test_kwargs(ext, write_only):
90-
# GH 42286
91-
# openpyxl doesn't utilize kwargs, only test that supplying a kwarg works
92-
kwargs = {"write_only": write_only}
88+
@pytest.mark.parametrize("iso_dates", [True, False])
89+
def test_kwargs(ext, iso_dates):
90+
# GH 42286 GH 43445
91+
kwargs = {"iso_dates": iso_dates}
9392
with tm.ensure_clean(ext) as f:
9493
msg = re.escape("Use of **kwargs is deprecated")
9594
with tm.assert_produces_warning(FutureWarning, match=msg):
9695
with ExcelWriter(f, engine="openpyxl", **kwargs) as writer:
96+
assert writer.book.iso_dates == iso_dates
9797
# ExcelWriter won't allow us to close without writing something
9898
DataFrame().to_excel(writer)
9999

100100

101-
@pytest.mark.parametrize("write_only", [True, False])
102-
def test_engine_kwargs(ext, write_only):
103-
# GH 42286
104-
# openpyxl doesn't utilize kwargs, only test that supplying a engine_kwarg works
105-
engine_kwargs = {"write_only": write_only}
101+
@pytest.mark.parametrize("iso_dates", [True, False])
102+
def test_engine_kwargs_write(ext, iso_dates):
103+
# GH 42286 GH 43445
104+
engine_kwargs = {"iso_dates": iso_dates}
106105
with tm.ensure_clean(ext) as f:
107106
with ExcelWriter(f, engine="openpyxl", engine_kwargs=engine_kwargs) as writer:
107+
assert writer.book.iso_dates == iso_dates
108108
# ExcelWriter won't allow us to close without writing something
109109
DataFrame().to_excel(writer)
110110

111111

112+
def test_engine_kwargs_append_invalid(ext):
113+
# GH 43445
114+
# test whether an invalid engine kwargs actually raises
115+
with tm.ensure_clean(ext) as f:
116+
DataFrame(["hello", "world"]).to_excel(f)
117+
with pytest.raises(
118+
TypeError,
119+
match=re.escape(
120+
"load_workbook() got an unexpected keyword argument 'apple_banana'"
121+
),
122+
):
123+
with ExcelWriter(
124+
f, engine="openpyxl", mode="a", engine_kwargs={"apple_banana": "fruit"}
125+
) as writer:
126+
# ExcelWriter needs us to write something to close properly
127+
DataFrame(["good"]).to_excel(writer, sheet_name="Sheet2")
128+
129+
130+
@pytest.mark.parametrize("data_only, expected", [(True, 0), (False, "=1+1")])
131+
def test_engine_kwargs_append_data_only(ext, data_only, expected):
132+
# GH 43445
133+
# tests whether the data_only engine_kwarg actually works well for
134+
# openpyxl's load_workbook
135+
with tm.ensure_clean(ext) as f:
136+
DataFrame(["=1+1"]).to_excel(f)
137+
with ExcelWriter(
138+
f, engine="openpyxl", mode="a", engine_kwargs={"data_only": data_only}
139+
) as writer:
140+
assert writer.sheets["Sheet1"]["B2"].value == expected
141+
# ExcelWriter needs us to writer something to close properly?
142+
DataFrame().to_excel(writer, sheet_name="Sheet2")
143+
144+
112145
@pytest.mark.parametrize(
113146
"mode,expected", [("w", ["baz"]), ("a", ["foo", "bar", "baz"])]
114147
)

pandas/tests/io/excel/test_xlwt.py

+12-10
Original file line numberDiff line numberDiff line change
@@ -101,25 +101,27 @@ def test_option_xls_writer_deprecated(ext):
101101
options.io.excel.xls.writer = "xlwt"
102102

103103

104-
@pytest.mark.parametrize("write_only", [True, False])
105-
def test_kwargs(ext, write_only):
104+
@pytest.mark.parametrize("style_compression", [0, 2])
105+
def test_kwargs(ext, style_compression):
106106
# GH 42286
107-
# xlwt doesn't utilize kwargs, only test that supplying a kwarg works
108-
kwargs = {"write_only": write_only}
107+
kwargs = {"style_compression": style_compression}
109108
with tm.ensure_clean(ext) as f:
110109
msg = re.escape("Use of **kwargs is deprecated")
111110
with tm.assert_produces_warning(FutureWarning, match=msg):
112-
with ExcelWriter(f, engine="openpyxl", **kwargs) as writer:
111+
with ExcelWriter(f, engine="xlwt", **kwargs) as writer:
112+
assert (
113+
writer.book._Workbook__styles.style_compression == style_compression
114+
)
113115
# xlwt won't allow us to close without writing something
114116
DataFrame().to_excel(writer)
115117

116118

117-
@pytest.mark.parametrize("write_only", [True, False])
118-
def test_engine_kwargs(ext, write_only):
119+
@pytest.mark.parametrize("style_compression", [0, 2])
120+
def test_engine_kwargs(ext, style_compression):
119121
# GH 42286
120-
# xlwt doesn't utilize kwargs, only test that supplying a engine_kwarg works
121-
engine_kwargs = {"write_only": write_only}
122+
engine_kwargs = {"style_compression": style_compression}
122123
with tm.ensure_clean(ext) as f:
123-
with ExcelWriter(f, engine="openpyxl", engine_kwargs=engine_kwargs) as writer:
124+
with ExcelWriter(f, engine="xlwt", engine_kwargs=engine_kwargs) as writer:
125+
assert writer.book._Workbook__styles.style_compression == style_compression
124126
# xlwt won't allow us to close without writing something
125127
DataFrame().to_excel(writer)

0 commit comments

Comments
 (0)