_xlsxwriter.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. from __future__ import annotations
  2. from typing import Any
  3. from pandas._libs import json
  4. from pandas._typing import (
  5. FilePath,
  6. StorageOptions,
  7. WriteExcelBuffer,
  8. )
  9. from pandas.io.excel._base import ExcelWriter
  10. from pandas.io.excel._util import (
  11. combine_kwargs,
  12. validate_freeze_panes,
  13. )
  14. class _XlsxStyler:
  15. # Map from openpyxl-oriented styles to flatter xlsxwriter representation
  16. # Ordering necessary for both determinism and because some are keyed by
  17. # prefixes of others.
  18. STYLE_MAPPING: dict[str, list[tuple[tuple[str, ...], str]]] = {
  19. "font": [
  20. (("name",), "font_name"),
  21. (("sz",), "font_size"),
  22. (("size",), "font_size"),
  23. (("color", "rgb"), "font_color"),
  24. (("color",), "font_color"),
  25. (("b",), "bold"),
  26. (("bold",), "bold"),
  27. (("i",), "italic"),
  28. (("italic",), "italic"),
  29. (("u",), "underline"),
  30. (("underline",), "underline"),
  31. (("strike",), "font_strikeout"),
  32. (("vertAlign",), "font_script"),
  33. (("vertalign",), "font_script"),
  34. ],
  35. "number_format": [(("format_code",), "num_format"), ((), "num_format")],
  36. "protection": [(("locked",), "locked"), (("hidden",), "hidden")],
  37. "alignment": [
  38. (("horizontal",), "align"),
  39. (("vertical",), "valign"),
  40. (("text_rotation",), "rotation"),
  41. (("wrap_text",), "text_wrap"),
  42. (("indent",), "indent"),
  43. (("shrink_to_fit",), "shrink"),
  44. ],
  45. "fill": [
  46. (("patternType",), "pattern"),
  47. (("patterntype",), "pattern"),
  48. (("fill_type",), "pattern"),
  49. (("start_color", "rgb"), "fg_color"),
  50. (("fgColor", "rgb"), "fg_color"),
  51. (("fgcolor", "rgb"), "fg_color"),
  52. (("start_color",), "fg_color"),
  53. (("fgColor",), "fg_color"),
  54. (("fgcolor",), "fg_color"),
  55. (("end_color", "rgb"), "bg_color"),
  56. (("bgColor", "rgb"), "bg_color"),
  57. (("bgcolor", "rgb"), "bg_color"),
  58. (("end_color",), "bg_color"),
  59. (("bgColor",), "bg_color"),
  60. (("bgcolor",), "bg_color"),
  61. ],
  62. "border": [
  63. (("color", "rgb"), "border_color"),
  64. (("color",), "border_color"),
  65. (("style",), "border"),
  66. (("top", "color", "rgb"), "top_color"),
  67. (("top", "color"), "top_color"),
  68. (("top", "style"), "top"),
  69. (("top",), "top"),
  70. (("right", "color", "rgb"), "right_color"),
  71. (("right", "color"), "right_color"),
  72. (("right", "style"), "right"),
  73. (("right",), "right"),
  74. (("bottom", "color", "rgb"), "bottom_color"),
  75. (("bottom", "color"), "bottom_color"),
  76. (("bottom", "style"), "bottom"),
  77. (("bottom",), "bottom"),
  78. (("left", "color", "rgb"), "left_color"),
  79. (("left", "color"), "left_color"),
  80. (("left", "style"), "left"),
  81. (("left",), "left"),
  82. ],
  83. }
  84. @classmethod
  85. def convert(cls, style_dict, num_format_str=None):
  86. """
  87. converts a style_dict to an xlsxwriter format dict
  88. Parameters
  89. ----------
  90. style_dict : style dictionary to convert
  91. num_format_str : optional number format string
  92. """
  93. # Create a XlsxWriter format object.
  94. props = {}
  95. if num_format_str is not None:
  96. props["num_format"] = num_format_str
  97. if style_dict is None:
  98. return props
  99. if "borders" in style_dict:
  100. style_dict = style_dict.copy()
  101. style_dict["border"] = style_dict.pop("borders")
  102. for style_group_key, style_group in style_dict.items():
  103. for src, dst in cls.STYLE_MAPPING.get(style_group_key, []):
  104. # src is a sequence of keys into a nested dict
  105. # dst is a flat key
  106. if dst in props:
  107. continue
  108. v = style_group
  109. for k in src:
  110. try:
  111. v = v[k]
  112. except (KeyError, TypeError):
  113. break
  114. else:
  115. props[dst] = v
  116. if isinstance(props.get("pattern"), str):
  117. # TODO: support other fill patterns
  118. props["pattern"] = 0 if props["pattern"] == "none" else 1
  119. for k in ["border", "top", "right", "bottom", "left"]:
  120. if isinstance(props.get(k), str):
  121. try:
  122. props[k] = [
  123. "none",
  124. "thin",
  125. "medium",
  126. "dashed",
  127. "dotted",
  128. "thick",
  129. "double",
  130. "hair",
  131. "mediumDashed",
  132. "dashDot",
  133. "mediumDashDot",
  134. "dashDotDot",
  135. "mediumDashDotDot",
  136. "slantDashDot",
  137. ].index(props[k])
  138. except ValueError:
  139. props[k] = 2
  140. if isinstance(props.get("font_script"), str):
  141. props["font_script"] = ["baseline", "superscript", "subscript"].index(
  142. props["font_script"]
  143. )
  144. if isinstance(props.get("underline"), str):
  145. props["underline"] = {
  146. "none": 0,
  147. "single": 1,
  148. "double": 2,
  149. "singleAccounting": 33,
  150. "doubleAccounting": 34,
  151. }[props["underline"]]
  152. # GH 30107 - xlsxwriter uses different name
  153. if props.get("valign") == "center":
  154. props["valign"] = "vcenter"
  155. return props
  156. class XlsxWriter(ExcelWriter):
  157. _engine = "xlsxwriter"
  158. _supported_extensions = (".xlsx",)
  159. def __init__(
  160. self,
  161. path: FilePath | WriteExcelBuffer | ExcelWriter,
  162. engine: str | None = None,
  163. date_format: str | None = None,
  164. datetime_format: str | None = None,
  165. mode: str = "w",
  166. storage_options: StorageOptions = None,
  167. if_sheet_exists: str | None = None,
  168. engine_kwargs: dict[str, Any] | None = None,
  169. **kwargs,
  170. ) -> None:
  171. # Use the xlsxwriter module as the Excel writer.
  172. from xlsxwriter import Workbook
  173. engine_kwargs = combine_kwargs(engine_kwargs, kwargs)
  174. if mode == "a":
  175. raise ValueError("Append mode is not supported with xlsxwriter!")
  176. super().__init__(
  177. path,
  178. engine=engine,
  179. date_format=date_format,
  180. datetime_format=datetime_format,
  181. mode=mode,
  182. storage_options=storage_options,
  183. if_sheet_exists=if_sheet_exists,
  184. engine_kwargs=engine_kwargs,
  185. )
  186. self._book = Workbook(self._handles.handle, **engine_kwargs)
  187. @property
  188. def book(self):
  189. """
  190. Book instance of class xlsxwriter.Workbook.
  191. This attribute can be used to access engine-specific features.
  192. """
  193. return self._book
  194. @property
  195. def sheets(self) -> dict[str, Any]:
  196. result = self.book.sheetnames
  197. return result
  198. def _save(self) -> None:
  199. """
  200. Save workbook to disk.
  201. """
  202. self.book.close()
  203. def _write_cells(
  204. self,
  205. cells,
  206. sheet_name: str | None = None,
  207. startrow: int = 0,
  208. startcol: int = 0,
  209. freeze_panes: tuple[int, int] | None = None,
  210. ) -> None:
  211. # Write the frame cells using xlsxwriter.
  212. sheet_name = self._get_sheet_name(sheet_name)
  213. wks = self.book.get_worksheet_by_name(sheet_name)
  214. if wks is None:
  215. wks = self.book.add_worksheet(sheet_name)
  216. style_dict = {"null": None}
  217. if validate_freeze_panes(freeze_panes):
  218. wks.freeze_panes(*(freeze_panes))
  219. for cell in cells:
  220. val, fmt = self._value_with_fmt(cell.val)
  221. stylekey = json.dumps(cell.style)
  222. if fmt:
  223. stylekey += fmt
  224. if stylekey in style_dict:
  225. style = style_dict[stylekey]
  226. else:
  227. style = self.book.add_format(_XlsxStyler.convert(cell.style, fmt))
  228. style_dict[stylekey] = style
  229. if cell.mergestart is not None and cell.mergeend is not None:
  230. wks.merge_range(
  231. startrow + cell.row,
  232. startcol + cell.col,
  233. startrow + cell.mergestart,
  234. startcol + cell.mergeend,
  235. val,
  236. style,
  237. )
  238. else:
  239. wks.write(startrow + cell.row, startcol + cell.col, val, style)