Skip to content

Commit 59f2bc4

Browse files
committed
Address reviewer feedback on autofilter PR
- Remove duplicate to_excel function code in generic.py - Add NotImplementedError for odfpy engine when autofilter=True - Remove broad exception handling from autofilter implementations - Add comprehensive tests for nonzero startrow/startcol - Add tests for MultiIndex columns with merge_cells=True and False - Improve tests to verify each column has autofilter - Remove redundant test_to_excel test - Remove redundant pytest.importorskip from test functions
1 parent 89058bf commit 59f2bc4

File tree

7 files changed

+172
-162
lines changed

7 files changed

+172
-162
lines changed

pandas/core/generic.py

Lines changed: 0 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -2313,149 +2313,6 @@ def to_excel(
23132313
if not isinstance(excel_writer, ExcelWriter):
23142314
# we need to close the writer if we created it
23152315
excel_writer.close()
2316-
) -> None:
2317-
"""
2318-
Write {klass} to an Excel sheet.
2319-
2320-
To write a single {klass} to an Excel .xlsx file it is only necessary to
2321-
specify a target file name. To write to multiple sheets it is necessary to
2322-
create an `ExcelWriter` object with a target file name, and specify a sheet
2323-
in the file to write to.
2324-
2325-
Multiple sheets may be written to by specifying unique `sheet_name`.
2326-
With all data written to the file it is necessary to save the changes.
2327-
Note that creating an `ExcelWriter` object with a file name that already
2328-
exists will result in the contents of the existing file being erased.
2329-
2330-
Parameters
2331-
----------
2332-
excel_writer : path-like, file-like, or ExcelWriter object
2333-
File path or existing ExcelWriter.
2334-
sheet_name : str, default 'Sheet1'
2335-
Name of sheet which will contain DataFrame.
2336-
na_rep : str, default ''
2337-
Missing data representation.
2338-
float_format : str, optional
2339-
Format string for floating point numbers. For example
2340-
``float_format="%.2f"`` will format 0.1234 to 0.12.
2341-
columns : sequence or list of str, optional
2342-
Columns to write.
2343-
header : bool or list of str, default True
2344-
Write out the column names. If a list of string is given it is
2345-
assumed to be aliases for the column names.
2346-
index : bool, default True
2347-
Write row names (index).
2348-
index_label : str or sequence, optional
2349-
Column label for index column(s) if desired. If not specified, and
2350-
`header` and `index` are True, then the index names are used. A
2351-
sequence should be given if the DataFrame uses MultiIndex.
2352-
startrow : int, default 0
2353-
Upper left cell row to dump data frame.
2354-
startcol : int, default 0
2355-
Upper left cell column to dump data frame.
2356-
engine : str, optional
2357-
Write engine to use, 'openpyxl' or 'xlsxwriter'. You can also set this
2358-
via the options ``io.excel.xlsx.writer`` or
2359-
``io.excel.xlsm.writer``.
2360-
2361-
merge_cells : bool or 'columns', default False
2362-
If True, write MultiIndex index and columns as merged cells.
2363-
If 'columns', merge MultiIndex column cells only.
2364-
{encoding_parameter}
2365-
inf_rep : str, default 'inf'
2366-
Representation for infinity (there is no native representation for
2367-
infinity in Excel).
2368-
{verbose_parameter}
2369-
freeze_panes : tuple of int (length 2), optional
2370-
Specifies the one-based bottommost row and rightmost column that
2371-
is to be frozen.
2372-
{storage_options}
2373-
2374-
.. versionadded:: {storage_options_versionadded}
2375-
{extra_parameters}
2376-
See Also
2377-
--------
2378-
to_csv : Write DataFrame to a comma-separated values (csv) file.
2379-
ExcelWriter : Class for writing DataFrame objects into excel sheets.
2380-
read_excel : Read an Excel file into a pandas DataFrame.
2381-
read_csv : Read a comma-separated values (csv) file into DataFrame.
2382-
io.formats.style.Styler.to_excel : Add styles to Excel sheet.
2383-
2384-
Notes
2385-
-----
2386-
For compatibility with :meth:`~DataFrame.to_csv`,
2387-
to_excel serializes lists and dicts to strings before writing.
2388-
2389-
Once a workbook has been saved it is not possible to write further
2390-
data without rewriting the whole workbook.
2391-
2392-
pandas will check the number of rows, columns,
2393-
and cell character count does not exceed Excel's limitations.
2394-
All other limitations must be checked by the user.
2395-
2396-
Examples
2397-
--------
2398-
2399-
Create, write to and save a workbook:
2400-
2401-
>>> df1 = pd.DataFrame(
2402-
... [["a", "b"], ["c", "d"]],
2403-
... index=["row 1", "row 2"],
2404-
... columns=["col 1", "col 2"],
2405-
... )
2406-
>>> df1.to_excel("output.xlsx") # doctest: +SKIP
2407-
2408-
To specify the sheet name:
2409-
2410-
>>> df1.to_excel("output.xlsx", sheet_name="Sheet_name_1") # doctest: +SKIP
2411-
2412-
If you wish to write to more than one sheet in the workbook, it is
2413-
necessary to specify an ExcelWriter object:
2414-
2415-
>>> df2 = df1.copy()
2416-
>>> with pd.ExcelWriter("output.xlsx") as writer: # doctest: +SKIP
2417-
... df1.to_excel(writer, sheet_name="Sheet_name_1")
2418-
... df2.to_excel(writer, sheet_name="Sheet_name_2")
2419-
2420-
ExcelWriter can also be used to append to an existing Excel file:
2421-
2422-
>>> with pd.ExcelWriter("output.xlsx", mode="a") as writer: # doctest: +SKIP
2423-
... df1.to_excel(writer, sheet_name="Sheet_name_3")
2424-
2425-
To set the library that is used to write the Excel file,
2426-
you can pass the `engine` keyword (the default engine is
2427-
automatically chosen depending on the file extension):
2428-
2429-
>>> df1.to_excel("output1.xlsx", engine="xlsxwriter") # doctest: +SKIP
2430-
"""
2431-
if engine_kwargs is None:
2432-
engine_kwargs = {}
2433-
2434-
df = self if isinstance(self, ABCDataFrame) else self.to_frame()
2435-
2436-
from pandas.io.formats.excel import ExcelFormatter
2437-
2438-
formatter = ExcelFormatter(
2439-
df,
2440-
na_rep=na_rep,
2441-
cols=columns,
2442-
header=header,
2443-
float_format=float_format,
2444-
index=index,
2445-
index_label=index_label,
2446-
merge_cells=merge_cells,
2447-
inf_rep=inf_rep,
2448-
)
2449-
formatter.write(
2450-
excel_writer,
2451-
sheet_name=sheet_name,
2452-
startrow=startrow,
2453-
startcol=startcol,
2454-
freeze_panes=freeze_panes,
2455-
engine=engine,
2456-
storage_options=storage_options,
2457-
engine_kwargs=engine_kwargs,
2458-
)
24592316

24602317
@final
24612318
@doc(

pandas/io/excel/_odswriter.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,17 @@ def _write_cells(
9999
startrow: int = 0,
100100
startcol: int = 0,
101101
freeze_panes: tuple[int, int] | None = None,
102+
autofilter: bool = False,
102103
) -> None:
103104
"""
104105
Write the frame cells using odf
105106
"""
107+
if autofilter:
108+
raise NotImplementedError(
109+
"Autofilter is not supported with the 'odf' engine. "
110+
"Please use 'openpyxl' or 'xlsxwriter' engine instead."
111+
)
112+
106113
from odf.table import (
107114
Table,
108115
TableCell,

pandas/io/excel/_openpyxl.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -552,15 +552,12 @@ def _write_cells(
552552
setattr(xcell, k, v)
553553

554554
if autofilter and min_r is not None and min_c is not None and max_r is not None and max_c is not None:
555-
try:
556-
# Convert numeric bounds to Excel-style range e.g. A1:D10
557-
from openpyxl.utils import get_column_letter
558-
559-
start_ref = f"{get_column_letter(min_c)}{min_r}"
560-
end_ref = f"{get_column_letter(max_c)}{max_r}"
561-
wks.auto_filter.ref = f"{start_ref}:{end_ref}"
562-
except Exception:
563-
pass
555+
# Convert numeric bounds to Excel-style range e.g. A1:D10
556+
from openpyxl.utils import get_column_letter
557+
558+
start_ref = f"{get_column_letter(min_c)}{min_r}"
559+
end_ref = f"{get_column_letter(max_c)}{max_r}"
560+
wks.auto_filter.ref = f"{start_ref}:{end_ref}"
564561

565562

566563
class OpenpyxlReader(BaseExcelReader["Workbook"]):

pandas/io/excel/_xlsxwriter.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,4 @@ def _write_cells(
304304

305305
if autofilter and min_r is not None and min_c is not None and max_r is not None and max_c is not None:
306306
# Apply autofilter over the used range. xlsxwriter uses 0-based indices.
307-
try:
308-
wks.autofilter(min_r, min_c, max_r, max_c)
309-
except Exception:
310-
# Be resilient if engine version doesn't support or range invalid
311-
pass
307+
wks.autofilter(min_r, min_c, max_r, max_c)

pandas/tests/io/excel/test_odswriter.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,13 @@ def test_cell_value_type(
104104
cell = sheet_cells[0]
105105
assert cell.attributes.get((OFFICENS, "value-type")) == cell_value_type
106106
assert cell.attributes.get((OFFICENS, cell_value_attribute)) == cell_value
107+
108+
109+
def test_to_excel_autofilter_odfpy_raises(tmp_excel):
110+
# Test that autofilter=True raises NotImplementedError with odfpy engine
111+
from pandas import DataFrame
112+
113+
df = DataFrame({"A": [1, 2], "B": [3, 4]})
114+
msg = "Autofilter is not supported with the 'odf' engine"
115+
with pytest.raises(NotImplementedError, match=msg):
116+
df.to_excel(tmp_excel, engine="odf", autofilter=True)

pandas/tests/io/excel/test_openpyxl.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,74 @@ def test_to_excel_autofilter_openpyxl(tmp_excel):
165165
# Expect filter over the full range, e.g. A1:B3 (header + 2 rows)
166166
assert ws.auto_filter is not None
167167
assert ws.auto_filter.ref is not None
168+
# Verify filter covers all columns (A and B)
169+
assert "A" in ws.auto_filter.ref
170+
assert "B" in ws.auto_filter.ref
171+
172+
173+
def test_to_excel_autofilter_startrow_startcol_openpyxl(tmp_excel):
174+
# Test autofilter with nonzero startrow and startcol
175+
df = DataFrame({"A": [1, 2], "B": [3, 4]})
176+
df.to_excel(
177+
tmp_excel,
178+
engine="openpyxl",
179+
index=False,
180+
autofilter=True,
181+
startrow=2,
182+
startcol=1,
183+
)
184+
185+
with contextlib.closing(openpyxl.load_workbook(tmp_excel)) as wb:
186+
ws = wb[wb.sheetnames[0]]
187+
assert ws.auto_filter is not None
188+
assert ws.auto_filter.ref is not None
189+
# Filter should be offset by startrow=2 and startcol=1 (B3:D5)
190+
assert ws.auto_filter.ref.startswith("B")
191+
assert "3" in ws.auto_filter.ref
192+
193+
194+
def test_to_excel_autofilter_multiindex_merge_cells_openpyxl(tmp_excel):
195+
# Test autofilter with MultiIndex columns and merge_cells=True
196+
df = DataFrame(
197+
[[1, 2, 3, 4], [5, 6, 7, 8]],
198+
columns=pd.MultiIndex.from_tuples(
199+
[("A", "a"), ("A", "b"), ("B", "a"), ("B", "b")]
200+
),
201+
)
202+
df.to_excel(
203+
tmp_excel,
204+
engine="openpyxl",
205+
index=False,
206+
autofilter=True,
207+
merge_cells=True,
208+
)
209+
210+
with contextlib.closing(openpyxl.load_workbook(tmp_excel)) as wb:
211+
ws = wb[wb.sheetnames[0]]
212+
assert ws.auto_filter is not None
213+
assert ws.auto_filter.ref is not None
214+
215+
216+
def test_to_excel_autofilter_multiindex_no_merge_openpyxl(tmp_excel):
217+
# Test autofilter with MultiIndex columns and merge_cells=False
218+
df = DataFrame(
219+
[[1, 2, 3, 4], [5, 6, 7, 8]],
220+
columns=pd.MultiIndex.from_tuples(
221+
[("A", "a"), ("A", "b"), ("B", "a"), ("B", "b")]
222+
),
223+
)
224+
df.to_excel(
225+
tmp_excel,
226+
engine="openpyxl",
227+
index=False,
228+
autofilter=True,
229+
merge_cells=False,
230+
)
231+
232+
with contextlib.closing(openpyxl.load_workbook(tmp_excel)) as wb:
233+
ws = wb[wb.sheetnames[0]]
234+
assert ws.auto_filter is not None
235+
assert ws.auto_filter.ref is not None
168236

169237

170238
@pytest.mark.parametrize("kwarg_name", ["read_only", "data_only"])

pandas/tests/io/excel/test_xlsxwriter.py

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,93 @@ def test_book_and_sheets_consistent(tmp_excel):
8686
assert writer.sheets == {"test_name": sheet}
8787

8888

89-
def test_to_excel(tmp_excel):
90-
DataFrame([[1, 2]]).to_excel(tmp_excel)
91-
92-
9389
def test_to_excel_autofilter_xlsxwriter(tmp_excel):
94-
pytest.importorskip("xlsxwriter")
9590
openpyxl = pytest.importorskip("openpyxl")
9691

9792
df = DataFrame({"A": [1, 2], "B": [3, 4]})
9893
# Write with xlsxwriter, verify via openpyxl that an autofilter exists
9994
df.to_excel(tmp_excel, engine="xlsxwriter", index=False, autofilter=True)
10095

96+
wb = openpyxl.load_workbook(tmp_excel)
97+
try:
98+
ws = wb[wb.sheetnames[0]]
99+
assert ws.auto_filter is not None
100+
assert ws.auto_filter.ref is not None
101+
# Verify filter covers all columns (A and B)
102+
assert "A" in ws.auto_filter.ref
103+
assert "B" in ws.auto_filter.ref
104+
finally:
105+
wb.close()
106+
107+
108+
def test_to_excel_autofilter_startrow_startcol_xlsxwriter(tmp_excel):
109+
openpyxl = pytest.importorskip("openpyxl")
110+
111+
df = DataFrame({"A": [1, 2], "B": [3, 4]})
112+
df.to_excel(
113+
tmp_excel,
114+
engine="xlsxwriter",
115+
index=False,
116+
autofilter=True,
117+
startrow=2,
118+
startcol=1,
119+
)
120+
121+
wb = openpyxl.load_workbook(tmp_excel)
122+
try:
123+
ws = wb[wb.sheetnames[0]]
124+
assert ws.auto_filter is not None
125+
assert ws.auto_filter.ref is not None
126+
# Filter should be offset by startrow=2 and startcol=1 (B3:D5)
127+
assert ws.auto_filter.ref.startswith("B")
128+
assert "3" in ws.auto_filter.ref
129+
finally:
130+
wb.close()
131+
132+
133+
def test_to_excel_autofilter_multiindex_merge_cells_xlsxwriter(tmp_excel):
134+
openpyxl = pytest.importorskip("openpyxl")
135+
136+
df = DataFrame(
137+
[[1, 2, 3, 4], [5, 6, 7, 8]],
138+
columns=pd.MultiIndex.from_tuples(
139+
[("A", "a"), ("A", "b"), ("B", "a"), ("B", "b")]
140+
),
141+
)
142+
df.to_excel(
143+
tmp_excel,
144+
engine="xlsxwriter",
145+
index=False,
146+
autofilter=True,
147+
merge_cells=True,
148+
)
149+
150+
wb = openpyxl.load_workbook(tmp_excel)
151+
try:
152+
ws = wb[wb.sheetnames[0]]
153+
assert ws.auto_filter is not None
154+
assert ws.auto_filter.ref is not None
155+
finally:
156+
wb.close()
157+
158+
159+
def test_to_excel_autofilter_multiindex_no_merge_xlsxwriter(tmp_excel):
160+
openpyxl = pytest.importorskip("openpyxl")
161+
162+
df = DataFrame(
163+
[[1, 2, 3, 4], [5, 6, 7, 8]],
164+
columns=pd.MultiIndex.from_tuples(
165+
[("A", "a"), ("A", "b"), ("B", "a"), ("B", "b")]
166+
),
167+
)
168+
df.to_excel(
169+
tmp_excel,
170+
engine="xlsxwriter",
171+
index=False,
172+
autofilter=True,
173+
merge_cells=False,
174+
)
175+
101176
wb = openpyxl.load_workbook(tmp_excel)
102177
try:
103178
ws = wb[wb.sheetnames[0]]

0 commit comments

Comments
 (0)