Skip to content

Commit 382156a

Browse files
authored
Update _openpyxl.py
1 parent cf597c4 commit 382156a

File tree

1 file changed

+103
-103
lines changed

1 file changed

+103
-103
lines changed

pandas/io/excel/_openpyxl.py

Lines changed: 103 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
if TYPE_CHECKING:
2727
from openpyxl import Workbook
2828
from openpyxl.descriptors.serialisable import Serialisable
29-
from openpyxl.styles import Fill
29+
from openpyxl.styles import (
30+
Fill,
31+
Font,
32+
)
3033

3134
from pandas._typing import (
3235
ExcelWriterIfSheetExists,
@@ -52,6 +55,7 @@ def __init__( # pyright: ignore[reportInconsistentConstructor]
5255
storage_options: StorageOptions | None = None,
5356
if_sheet_exists: ExcelWriterIfSheetExists | None = None,
5457
engine_kwargs: dict[str, Any] | None = None,
58+
autofilter: bool = False,
5559
**kwargs,
5660
) -> None:
5761
# Use the openpyxl module as the Excel writer.
@@ -67,8 +71,8 @@ def __init__( # pyright: ignore[reportInconsistentConstructor]
6771
engine_kwargs=engine_kwargs,
6872
)
6973

70-
# Persist engine kwargs for later feature toggles (e.g., autofilter/header bold)
71-
self._engine_kwargs = engine_kwargs
74+
self._engine_kwargs = engine_kwargs or {}
75+
self.autofilter = autofilter
7276

7377
# ExcelWriter replaced "a" by "r+" to allow us to first read the excel file from
7478
# the file and later write to it
@@ -184,50 +188,65 @@ def _convert_to_color(cls, color_spec):
184188
return Color(**color_spec)
185189

186190
@classmethod
187-
def _convert_to_font(cls, font_dict):
188-
"""
189-
Convert ``font_dict`` to an openpyxl v2 Font object.
191+
def _convert_to_font(cls, style_dict: dict) -> Font:
192+
"""Convert style_dict to an openpyxl Font object.
190193
191194
Parameters
192195
----------
193-
font_dict : dict
194-
A dict with zero or more of the following keys (or their synonyms).
195-
'name'
196-
'size' ('sz')
197-
'bold' ('b')
198-
'italic' ('i')
199-
'underline' ('u')
200-
'strikethrough' ('strike')
201-
'color'
202-
'vertAlign' ('vertalign')
203-
'charset'
204-
'scheme'
205-
'family'
206-
'outline'
207-
'shadow'
208-
'condense'
196+
style_dict : dict
197+
Dictionary of style properties
209198
210199
Returns
211200
-------
212-
font : openpyxl.styles.Font
201+
openpyxl.styles.Font
202+
The converted font object
213203
"""
214204
from openpyxl.styles import Font
215205

216-
_font_key_map = {
217-
"sz": "size",
206+
if not style_dict:
207+
return Font()
208+
209+
# Check for font-weight in different formats
210+
is_bold = False
211+
212+
# Check for 'font-weight' directly in style_dict
213+
if style_dict.get("font-weight") in ("bold", "bolder", 700, "700"):
214+
is_bold = True
215+
# Check for 'font' dictionary with 'weight' key
216+
elif isinstance(style_dict.get("font"), dict) and style_dict["font"].get(
217+
"weight"
218+
) in ("bold", "bolder", 700, "700"):
219+
is_bold = True
220+
# Check for 'b' or 'bold' keys
221+
elif style_dict.get("b") or style_dict.get("bold"):
222+
is_bold = True
223+
224+
# Map style keys to Font constructor arguments
225+
key_map = {
218226
"b": "bold",
219227
"i": "italic",
220228
"u": "underline",
221229
"strike": "strikethrough",
222-
"vertalign": "vertAlign",
230+
"vertAlign": "vertAlign",
231+
"sz": "size",
232+
"color": "color",
233+
"name": "name",
234+
"family": "family",
235+
"scheme": "scheme",
223236
}
224237

225-
font_kwargs = {}
226-
for k, v in font_dict.items():
227-
k = _font_key_map.get(k, k)
228-
if k == "color":
229-
v = cls._convert_to_color(v)
230-
font_kwargs[k] = v
238+
font_kwargs = {"bold": is_bold} # Set bold based on our checks
239+
240+
# Process other font properties
241+
for style_key, font_key in key_map.items():
242+
if style_key in style_dict and style_key not in (
243+
"b",
244+
"bold",
245+
): # Skip b/bold as we've already handled it
246+
value = style_dict[style_key]
247+
if font_key == "color" and value is not None:
248+
value = cls._convert_to_color(value)
249+
font_kwargs[font_key] = value
231250

232251
return Font(**font_kwargs)
233252

@@ -455,9 +474,9 @@ def _write_cells(
455474
) -> None:
456475
# Write the frame cells using openpyxl.
457476
sheet_name = self._get_sheet_name(sheet_name)
477+
_style_cache: dict[str, dict[str, Any]] = {}
458478

459-
_style_cache: dict[str, dict[str, Serialisable]] = {}
460-
479+
# Initialize worksheet
461480
if sheet_name in self.sheets and self._if_sheet_exists != "new":
462481
if "r+" in self._mode:
463482
if self._if_sheet_exists == "replace":
@@ -490,90 +509,71 @@ def _write_cells(
490509
)
491510

492511
# Track bounds for autofilter application
493-
min_row = None
494-
min_col = None
495-
max_row = None
496-
max_col = None
497-
498-
# Prepare header bold setting
499-
header_bold = bool(self._engine_kwargs.get("header_bold", False)) if hasattr(self, "_engine_kwargs") else False
512+
min_row = min_col = max_row = max_col = None
500513

514+
# Process cells
501515
for cell in cells:
502-
xcell = wks.cell(
503-
row=startrow + cell.row + 1, column=startcol + cell.col + 1
504-
)
516+
xrow = startrow + cell.row
517+
xcol = startcol + cell.col
518+
xcell = wks.cell(row=xrow + 1, column=xcol + 1) # +1 for 1-based indexing
519+
520+
# Apply cell value and format
505521
xcell.value, fmt = self._value_with_fmt(cell.val)
506522
if fmt:
507523
xcell.number_format = fmt
508524

509-
style_kwargs: dict[str, Serialisable] | None = {}
525+
# Apply cell style if provided
510526
if cell.style:
511527
key = str(cell.style)
512-
style_kwargs = _style_cache.get(key)
513-
if style_kwargs is None:
528+
if key not in _style_cache:
514529
style_kwargs = self._convert_to_style_kwargs(cell.style)
515530
_style_cache[key] = style_kwargs
531+
else:
532+
style_kwargs = _style_cache[key]
516533

517-
if style_kwargs:
534+
# Apply the style
518535
for k, v in style_kwargs.items():
519536
setattr(xcell, k, v)
520537

521538
# Update bounds
522-
crow = startrow + cell.row + 1
523-
ccol = startcol + cell.col + 1
524-
if min_row is None or crow < min_row:
525-
min_row = crow
526-
if min_col is None or ccol < min_col:
527-
min_col = ccol
528-
if max_row is None or crow > max_row:
529-
max_row = crow
530-
if max_col is None or ccol > max_col:
531-
max_col = ccol
532-
533-
# Apply bold to first header row cells if requested
534-
if header_bold and (cell.row == 0):
535-
try:
536-
from openpyxl.styles import Font
537-
xcell.font = Font(bold=True)
538-
except Exception:
539-
pass
540-
541-
if cell.mergestart is not None and cell.mergeend is not None:
542-
wks.merge_cells(
543-
start_row=startrow + cell.row + 1,
544-
start_column=startcol + cell.col + 1,
545-
end_column=startcol + cell.mergeend + 1,
546-
end_row=startrow + cell.mergestart + 1,
547-
)
548-
549-
# When cells are merged only the top-left cell is preserved
550-
# The behaviour of the other cells in a merged range is
551-
# undefined
552-
if style_kwargs:
553-
first_row = startrow + cell.row + 1
554-
last_row = startrow + cell.mergestart + 1
555-
first_col = startcol + cell.col + 1
556-
last_col = startcol + cell.mergeend + 1
557-
558-
for row in range(first_row, last_row + 1):
559-
for col in range(first_col, last_col + 1):
560-
if row == first_row and col == first_col:
561-
# Ignore first cell. It is already handled.
562-
continue
563-
xcell = wks.cell(column=col, row=row)
564-
for k, v in style_kwargs.items():
565-
setattr(xcell, k, v)
566-
567-
# Apply autofilter over the used range if requested
568-
if hasattr(self, "_engine_kwargs") and bool(self._engine_kwargs.get("autofilter_header", False)):
569-
if min_row is not None and min_col is not None and max_row is not None and max_col is not None:
570-
try:
571-
from openpyxl.utils import get_column_letter
572-
start_ref = f"{get_column_letter(min_col)}{min_row}"
573-
end_ref = f"{get_column_letter(max_col)}{max_row}"
574-
wks.auto_filter.ref = f"{start_ref}:{end_ref}"
575-
except Exception:
576-
pass
539+
if min_row is None or xrow < min_row:
540+
min_row = xrow
541+
if max_row is None or xrow > max_row:
542+
max_row = xrow
543+
if min_col is None or xcol < min_col:
544+
min_col = xcol
545+
if max_col is None or xcol > max_col:
546+
max_col = xcol
547+
548+
# Apply autofilter if requested
549+
if getattr(self, "autofilter", False) and all(
550+
v is not None for v in [min_row, min_col, max_row, max_col]
551+
):
552+
try:
553+
from openpyxl.utils import get_column_letter
554+
555+
start_ref = f"{get_column_letter(min_col + 1)}{min_row + 1}"
556+
end_ref = f"{get_column_letter(max_col + 1)}{max_row + 1}"
557+
wks.auto_filter.ref = f"{start_ref}:{end_ref}"
558+
except Exception:
559+
pass
560+
561+
562+
def _update_bounds(self, wks, cell, startrow, startcol):
563+
"""Helper method to update the bounds for autofilter"""
564+
global min_row, max_row, min_col, max_col
565+
566+
crow = startrow + cell.row + 1
567+
ccol = startcol + cell.col + 1
568+
569+
if min_row is None or crow < min_row:
570+
min_row = crow
571+
if max_row is None or crow > max_row:
572+
max_row = crow
573+
if min_col is None or ccol < min_col:
574+
min_col = ccol
575+
if max_col is None or ccol > max_col:
576+
max_col = ccol
577577

578578

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

0 commit comments

Comments
 (0)