Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions pandas/io/excel/_openpyxl.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ def __init__( # pyright: ignore[reportInconsistentConstructor]
engine_kwargs=engine_kwargs,
)

# Persist engine kwargs for later feature toggles (e.g., autofilter/header bold)
self._engine_kwargs = engine_kwargs

# ExcelWriter replaced "a" by "r+" to allow us to first read the excel file from
# the file and later write to it
if "r+" in self._mode: # Load from existing workbook
Expand Down Expand Up @@ -486,6 +489,15 @@ def _write_cells(
row=freeze_panes[0] + 1, column=freeze_panes[1] + 1
)

# Track bounds for autofilter application
min_row = None
min_col = None
max_row = None
max_col = None

# Prepare header bold setting
header_bold = bool(self._engine_kwargs.get("header_bold", False)) if hasattr(self, "_engine_kwargs") else False

for cell in cells:
xcell = wks.cell(
row=startrow + cell.row + 1, column=startcol + cell.col + 1
Expand All @@ -506,6 +518,26 @@ def _write_cells(
for k, v in style_kwargs.items():
setattr(xcell, k, v)

# Update bounds
crow = startrow + cell.row + 1
ccol = startcol + cell.col + 1
if min_row is None or crow < min_row:
min_row = crow
if min_col is None or ccol < min_col:
min_col = ccol
if max_row is None or crow > max_row:
max_row = crow
if max_col is None or ccol > max_col:
max_col = ccol

# Apply bold to first header row cells if requested
if header_bold and (cell.row == 0):
try:
from openpyxl.styles import Font
xcell.font = Font(bold=True)
except Exception:
pass

if cell.mergestart is not None and cell.mergeend is not None:
wks.merge_cells(
start_row=startrow + cell.row + 1,
Expand All @@ -532,6 +564,17 @@ def _write_cells(
for k, v in style_kwargs.items():
setattr(xcell, k, v)

# Apply autofilter over the used range if requested
if hasattr(self, "_engine_kwargs") and bool(self._engine_kwargs.get("autofilter_header", False)):
if min_row is not None and min_col is not None and max_row is not None and max_col is not None:
try:
from openpyxl.utils import get_column_letter
start_ref = f"{get_column_letter(min_col)}{min_row}"
end_ref = f"{get_column_letter(max_col)}{max_row}"
wks.auto_filter.ref = f"{start_ref}:{end_ref}"
except Exception:
pass


class OpenpyxlReader(BaseExcelReader["Workbook"]):
@doc(storage_options=_shared_docs["storage_options"])
Expand Down
37 changes: 34 additions & 3 deletions pandas/io/excel/_xlsxwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ def __init__( # pyright: ignore[reportInconsistentConstructor]
engine_kwargs=engine_kwargs,
)

self._engine_kwargs = engine_kwargs

try:
self._book = Workbook(self._handles.handle, **engine_kwargs) # type: ignore[arg-type]
except TypeError:
Expand Down Expand Up @@ -258,6 +260,14 @@ def _write_cells(
if validate_freeze_panes(freeze_panes):
wks.freeze_panes(*(freeze_panes))

min_row = None
min_col = None
max_row = None
max_col = None

header_bold = bool(self._engine_kwargs.get("header_bold", False)) if hasattr(self, "_engine_kwargs") else False
bold_format = self.book.add_format({"bold": True}) if header_bold else None

for cell in cells:
val, fmt = self._value_with_fmt(cell.val)

Expand All @@ -271,14 +281,35 @@ def _write_cells(
style = self.book.add_format(_XlsxStyler.convert(cell.style, fmt))
style_dict[stylekey] = style

crow = startrow + cell.row
ccol = startcol + cell.col
if min_row is None or crow < min_row:
min_row = crow
if min_col is None or ccol < min_col:
min_col = ccol
if max_row is None or crow > max_row:
max_row = crow
if max_col is None or ccol > max_col:
max_col = ccol

if cell.mergestart is not None and cell.mergeend is not None:
wks.merge_range(
startrow + cell.row,
startcol + cell.col,
crow,
ccol,
startrow + cell.mergestart,
startcol + cell.mergeend,
val,
style,
)
else:
wks.write(startrow + cell.row, startcol + cell.col, val, style)
if bold_format is not None and (startrow == crow):
wks.write(crow, ccol, val, bold_format)
else:
wks.write(crow, ccol, val, style)

if hasattr(self, "_engine_kwargs") and bool(self._engine_kwargs.get("autofilter_header", False)):
if min_row is not None and min_col is not None and max_row is not None and max_col is not None:
try:
wks.autofilter(min_row, min_col, max_row, max_col)
except Exception:
pass
9 changes: 9 additions & 0 deletions pandas/io/formats/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,15 @@ def write(
formatted_cells = self.get_formatted_cells()
if isinstance(writer, ExcelWriter):
need_save = False
# Propagate engine_kwargs to an existing writer instance if provided
if engine_kwargs:
try:
current = getattr(writer, "_engine_kwargs", {}) or {}
merged = {**current, **engine_kwargs}
setattr(writer, "_engine_kwargs", merged)
except Exception:
# Best-effort propagation; ignore if engine does not support it
pass
else:
writer = ExcelWriter(
writer,
Expand Down
24 changes: 24 additions & 0 deletions pandas/tests/io/excel/test_autofilter_openpyxl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import io
import pytest
import pandas as pd

openpyxl = pytest.importorskip("openpyxl")


def test_to_excel_openpyxl_autofilter_and_bold():
df = pd.DataFrame({"A": [1, 2], "B": [3, 4]})
buf = io.BytesIO()
with pd.ExcelWriter(buf, engine="openpyxl") as writer:
df.to_excel(
writer,
index=False,
engine_kwargs={"autofilter_header": True, "header_bold": True},
)
buf.seek(0)
wb = openpyxl.load_workbook(buf)
ws = wb.active
# Autofilter should be set spanning header+data
assert ws.auto_filter is not None
assert ws.auto_filter.ref is not None and ws.auto_filter.ref != ""
# Header row (row 1) should be bold
assert all(ws.cell(row=1, column=c).font.bold for c in range(1, df.shape[1] + 1))
25 changes: 25 additions & 0 deletions pandas/tests/io/excel/test_autofilter_xlsxwriter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import io
import pytest
import pandas as pd

pytest.importorskip("xlsxwriter")
openpyxl = pytest.importorskip("openpyxl")


def test_to_excel_xlsxwriter_autofilter_and_bold():
df = pd.DataFrame({"A": [1, 2], "B": [3, 4]})
buf = io.BytesIO()
with pd.ExcelWriter(buf, engine="xlsxwriter") as writer:
df.to_excel(
writer,
index=False,
engine_kwargs={"autofilter_header": True, "header_bold": True},
)
buf.seek(0)
wb = openpyxl.load_workbook(buf)
ws = wb.active
# Autofilter should be set spanning header+data
assert ws.auto_filter is not None
assert ws.auto_filter.ref is not None and ws.auto_filter.ref != ""
# Header row (row 1) should be bold
assert all(ws.cell(row=1, column=c).font.bold for c in range(1, df.shape[1] + 1))
Loading