2626if 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
579579class OpenpyxlReader (BaseExcelReader ["Workbook" ]):
0 commit comments