Skip to content

Commit dee9451

Browse files
authored
Update _xlsxwriter.py
1 parent 382156a commit dee9451

File tree

1 file changed

+104
-72
lines changed

1 file changed

+104
-72
lines changed

pandas/io/excel/_xlsxwriter.py

Lines changed: 104 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,15 @@
66
Any,
77
)
88

9-
from pandas.io.excel._base import ExcelWriter
10-
from pandas.io.excel._util import (
11-
combine_kwargs,
12-
validate_freeze_panes,
13-
)
14-
159
if TYPE_CHECKING:
16-
from pandas._typing import (
17-
ExcelWriterIfSheetExists,
18-
FilePath,
19-
StorageOptions,
20-
WriteExcelBuffer,
21-
)
10+
from pandas._typing import FilePath, StorageOptions, WriteExcelBuffer
11+
12+
from xlsxwriter import Workbook
13+
14+
from pandas.compat._optional import import_optional_dependency
15+
16+
from pandas.io.excel._base import ExcelWriter
17+
from pandas.io.excel._util import validate_freeze_panes
2218

2319

2420
class _XlsxStyler:
@@ -93,28 +89,44 @@ class _XlsxStyler:
9389
}
9490

9591
@classmethod
96-
def convert(cls, style_dict, num_format_str=None) -> dict[str, Any]:
97-
"""
98-
converts a style_dict to an xlsxwriter format dict
99-
100-
Parameters
101-
----------
102-
style_dict : style dictionary to convert
103-
num_format_str : optional number format string
104-
"""
105-
# Create a XlsxWriter format object.
106-
props = {}
107-
108-
if num_format_str is not None:
109-
props["num_format"] = num_format_str
110-
111-
if style_dict is None:
112-
return props
113-
92+
def convert(
93+
cls,
94+
style_dict: dict,
95+
num_format_str: str | None = None,
96+
) -> dict[str, Any]:
97+
"""Convert a style_dict to an xlsxwriter format dict."""
98+
# Create a copy to avoid modifying the input
99+
style_dict = style_dict.copy()
100+
101+
# Map CSS font-weight to xlsxwriter font-weight (bold)
102+
if style_dict.get("font-weight") in ("bold", "bolder", 700, "700") or (
103+
isinstance(style_dict.get("font"), dict)
104+
and style_dict["font"].get("weight") in ("bold", "bolder", 700, "700")
105+
):
106+
# For XLSXWriter, we need to set the font with bold=True
107+
style_dict = {"font": {"bold": True, "name": "Calibri", "size": 11}}
108+
# Also set the b property directly as it might be needed
109+
style_dict["b"] = True
110+
111+
# Handle font styles
112+
if "font-style" in style_dict and style_dict["font-style"] == "italic":
113+
style_dict["italic"] = True
114+
del style_dict["font-style"]
115+
116+
# Convert CSS border styles to xlsxwriter format
117+
# border_map = {
118+
# "border-top": "top",
119+
# "border-right": "right",
120+
# "border-bottom": "bottom",
121+
# "border-left": "left",
122+
# }
114123
if "borders" in style_dict:
115124
style_dict = style_dict.copy()
116125
style_dict["border"] = style_dict.pop("borders")
117126

127+
# Initialize props to track which properties we've processed
128+
props = {}
129+
118130
for style_group_key, style_group in style_dict.items():
119131
for src, dst in cls.STYLE_MAPPING.get(style_group_key, []):
120132
# src is a sequence of keys into a nested dict
@@ -189,36 +201,29 @@ def __init__( # pyright: ignore[reportInconsistentConstructor]
189201
datetime_format: str | None = None,
190202
mode: str = "w",
191203
storage_options: StorageOptions | None = None,
192-
if_sheet_exists: ExcelWriterIfSheetExists | None = None,
193-
engine_kwargs: dict[str, Any] | None = None,
194-
**kwargs,
204+
if_sheet_exists: str | None = None,
205+
engine_kwargs: dict | None = None,
206+
autofilter: bool = False,
195207
) -> None:
196208
# Use the xlsxwriter module as the Excel writer.
197-
from xlsxwriter import Workbook
198-
199-
engine_kwargs = combine_kwargs(engine_kwargs, kwargs)
200-
201-
if mode == "a":
202-
raise ValueError("Append mode is not supported with xlsxwriter!")
203-
209+
import_optional_dependency("xlsxwriter")
204210
super().__init__(
205211
path,
206-
engine=engine,
207-
date_format=date_format,
208-
datetime_format=datetime_format,
209212
mode=mode,
210213
storage_options=storage_options,
211214
if_sheet_exists=if_sheet_exists,
212215
engine_kwargs=engine_kwargs,
213216
)
214217

215-
self._engine_kwargs = engine_kwargs
218+
self._engine_kwargs = engine_kwargs or {}
219+
self.autofilter = autofilter
220+
self._book = None
216221

217222
try:
218-
self._book = Workbook(self._handles.handle, **engine_kwargs)
219-
except TypeError:
223+
self._book = Workbook(self._handles.handle, **self._engine_kwargs) # type: ignore[arg-type]
224+
except TypeError as e:
220225
self._handles.handle.close()
221-
raise
226+
raise RuntimeError("Failed to create XlsxWriter workbook") from e
222227

223228
@property
224229
def book(self):
@@ -260,14 +265,32 @@ def _write_cells(
260265
if validate_freeze_panes(freeze_panes):
261266
wks.freeze_panes(*(freeze_panes))
262267

263-
min_row = None
264-
min_col = None
265-
max_row = None
266-
max_col = None
268+
# Initialize bounds with first cell
269+
first_cell = next(cells, None)
270+
if first_cell is None:
271+
return
272+
273+
# Initialize with first cell's position
274+
min_row = startrow + first_cell.row
275+
min_col = startcol + first_cell.col
276+
max_row = min_row
277+
max_col = min_col
267278

268-
header_bold = bool(self._engine_kwargs.get("header_bold", False)) if hasattr(self, "_engine_kwargs") else False
269-
bold_format = self.book.add_format({"bold": True}) if header_bold else None
279+
# Process first cell
280+
val, fmt = self._value_with_fmt(first_cell.val)
281+
stylekey = json.dumps(first_cell.style)
282+
if fmt:
283+
stylekey += fmt
270284

285+
if stylekey in style_dict:
286+
style = style_dict[stylekey]
287+
else:
288+
style = self.book.add_format(_XlsxStyler.convert(first_cell.style, fmt))
289+
style_dict[stylekey] = style
290+
291+
wks.write(startrow + first_cell.row, startcol + first_cell.col, val, style)
292+
293+
# Process remaining cells
271294
for cell in cells:
272295
val, fmt = self._value_with_fmt(cell.val)
273296

@@ -281,34 +304,43 @@ def _write_cells(
281304
style = self.book.add_format(_XlsxStyler.convert(cell.style, fmt))
282305
style_dict[stylekey] = style
283306

284-
crow = startrow + cell.row
285-
ccol = startcol + cell.col
286-
if min_row is None or crow < min_row:
287-
min_row = crow
288-
if min_col is None or ccol < min_col:
289-
min_col = ccol
290-
if max_row is None or crow > max_row:
291-
max_row = crow
292-
if max_col is None or ccol > max_col:
293-
max_col = ccol
307+
row = startrow + cell.row
308+
col = startcol + cell.col
309+
310+
# Write the cell
311+
wks.write(row, col, val, style)
312+
313+
# Update bounds
314+
min_row = min(min_row, row) if min_row is not None else row
315+
min_col = min(min_col, col) if min_col is not None else col
316+
max_row = max(max_row, row) if max_row is not None else row
317+
max_col = max(max_col, col) if max_col is not None else col
294318

295319
if cell.mergestart is not None and cell.mergeend is not None:
296320
wks.merge_range(
297-
crow,
298-
ccol,
321+
row,
322+
col,
299323
startrow + cell.mergestart,
300324
startcol + cell.mergeend,
301325
val,
302326
style,
303327
)
304328
else:
305-
if bold_format is not None and (startrow == crow):
306-
wks.write(crow, ccol, val, bold_format)
307-
else:
308-
wks.write(crow, ccol, val, style)
309-
310-
if hasattr(self, "_engine_kwargs") and bool(self._engine_kwargs.get("autofilter_header", False)):
311-
if min_row is not None and min_col is not None and max_row is not None and max_col is not None:
329+
wks.write(row, col, val, style)
330+
331+
# Apply autofilter if requested
332+
if getattr(self, "autofilter", False):
333+
wks.autofilter(min_row, min_col, max_row, max_col)
334+
335+
if hasattr(self, "_engine_kwargs") and bool(
336+
self._engine_kwargs.get("autofilter_header", False)
337+
):
338+
if (
339+
min_row is not None
340+
and min_col is not None
341+
and max_row is not None
342+
and max_col is not None
343+
):
312344
try:
313345
wks.autofilter(min_row, min_col, max_row, max_col)
314346
except Exception:

0 commit comments

Comments
 (0)