1from __future__ import annotations
2
3from collections.abc import (
4 Callable,
5 Hashable,
6 Iterable,
7 MutableMapping,
8 Sequence,
9)
10from typing import (
11 TYPE_CHECKING,
12 Any,
13 Literal,
14 TypeVar,
15 overload,
16)
17
18from pandas.compat._optional import import_optional_dependency
19
20from pandas.core.dtypes.common import (
21 is_integer,
22 is_list_like,
23)
24
25if TYPE_CHECKING:
26 from pandas.io.excel._base import ExcelWriter
27
28 ExcelWriter_t = type[ExcelWriter]
29 usecols_func = TypeVar("usecols_func", bound=Callable[[Hashable], object])
30
31_writers: MutableMapping[str, ExcelWriter_t] = {}
32
33
34def register_writer(klass: ExcelWriter_t) -> None:
35 """
36 Add engine to the excel writer registry.io.excel.
37
38 You must use this method to integrate with ``to_excel``.
39
40 Parameters
41 ----------
42 klass : ExcelWriter
43 """
44 if not callable(klass):
45 raise ValueError("Can only register callables as engines")
46 engine_name = klass._engine
47 _writers[engine_name] = klass
48
49
50def get_default_engine(ext: str, mode: Literal["reader", "writer"] = "reader") -> str:
51 """
52 Return the default reader/writer for the given extension.
53
54 Parameters
55 ----------
56 ext : str
57 The excel file extension for which to get the default engine.
58 mode : str {'reader', 'writer'}
59 Whether to get the default engine for reading or writing.
60 Either 'reader' or 'writer'
61
62 Returns
63 -------
64 str
65 The default engine for the extension.
66 """
67 _default_readers = {
68 "xlsx": "openpyxl",
69 "xlsm": "openpyxl",
70 "xlsb": "pyxlsb",
71 "xls": "xlrd",
72 "ods": "odf",
73 }
74 _default_writers = {
75 "xlsx": "openpyxl",
76 "xlsm": "openpyxl",
77 "xlsb": "pyxlsb",
78 "ods": "odf",
79 }
80 assert mode in ["reader", "writer"]
81 if mode == "writer":
82 # Prefer xlsxwriter over openpyxl if installed
83 xlsxwriter = import_optional_dependency("xlsxwriter", errors="warn")
84 if xlsxwriter:
85 _default_writers["xlsx"] = "xlsxwriter"
86 return _default_writers[ext]
87 else:
88 return _default_readers[ext]
89
90
91def get_writer(engine_name: str) -> ExcelWriter_t:
92 try:
93 return _writers[engine_name]
94 except KeyError as err:
95 raise ValueError(f"No Excel writer '{engine_name}'") from err
96
97
98def _excel2num(x: str) -> int:
99 """
100 Convert Excel column name like 'AB' to 0-based column index.
101
102 Parameters
103 ----------
104 x : str
105 The Excel column name to convert to a 0-based column index.
106
107 Returns
108 -------
109 num : int
110 The column index corresponding to the name.
111
112 Raises
113 ------
114 ValueError
115 Part of the Excel column name was invalid.
116 """
117 index = 0
118
119 for c in x.upper().strip():
120 cp = ord(c)
121
122 if cp < ord("A") or cp > ord("Z"):
123 raise ValueError(f"Invalid column name: {x}")
124
125 index = index * 26 + cp - ord("A") + 1
126
127 return index - 1
128
129
130def _range2cols(areas: str) -> list[int]:
131 """
132 Convert comma separated list of column names and ranges to indices.
133
134 Parameters
135 ----------
136 areas : str
137 A string containing a sequence of column ranges (or areas).
138
139 Returns
140 -------
141 cols : list
142 A list of 0-based column indices.
143
144 Examples
145 --------
146 >>> _range2cols("A:E")
147 [0, 1, 2, 3, 4]
148 >>> _range2cols("A,C,Z:AB")
149 [0, 2, 25, 26, 27]
150 """
151 cols: list[int] = []
152
153 for rng in areas.split(","):
154 if ":" in rng:
155 rngs = rng.split(":")
156 cols.extend(range(_excel2num(rngs[0]), _excel2num(rngs[1]) + 1))
157 else:
158 cols.append(_excel2num(rng))
159
160 return cols
161
162
163@overload
164def maybe_convert_usecols(usecols: str | list[int]) -> list[int]: ...
165
166
167@overload
168def maybe_convert_usecols(usecols: list[str]) -> list[str]: ...
169
170
171@overload
172def maybe_convert_usecols(usecols: usecols_func) -> usecols_func: ...
173
174
175@overload
176def maybe_convert_usecols(usecols: None) -> None: ...
177
178
179def maybe_convert_usecols(
180 usecols: str | list[int] | list[str] | usecols_func | None,
181) -> None | list[int] | list[str] | usecols_func:
182 """
183 Convert `usecols` into a compatible format for parsing in `parsers.py`.
184
185 Parameters
186 ----------
187 usecols : object
188 The use-columns object to potentially convert.
189
190 Returns
191 -------
192 converted : object
193 The compatible format of `usecols`.
194 """
195 if usecols is None:
196 return usecols
197
198 if is_integer(usecols):
199 raise ValueError(
200 "Passing an integer for `usecols` is no longer supported. "
201 "Please pass in a list of int from 0 to `usecols` inclusive instead."
202 )
203
204 if isinstance(usecols, str):
205 return _range2cols(usecols)
206
207 return usecols
208
209
210@overload
211def validate_freeze_panes(freeze_panes: tuple[int, int]) -> Literal[True]: ...
212
213
214@overload
215def validate_freeze_panes(freeze_panes: None) -> Literal[False]: ...
216
217
218def validate_freeze_panes(freeze_panes: tuple[int, int] | None) -> bool:
219 if freeze_panes is not None:
220 if len(freeze_panes) == 2 and all(
221 isinstance(item, int) for item in freeze_panes
222 ):
223 return True
224
225 raise ValueError(
226 "freeze_panes must be of form (row, column) "
227 "where row and column are integers"
228 )
229
230 # freeze_panes wasn't specified, return False so it won't be applied
231 # to output sheet
232 return False
233
234
235def fill_mi_header(
236 row: list[Hashable], control_row: list[bool]
237) -> tuple[list[Hashable], list[bool]]:
238 """
239 Forward fill blank entries in row but only inside the same parent index.
240
241 Used for creating headers in Multiindex.
242
243 Parameters
244 ----------
245 row : list
246 List of items in a single row.
247 control_row : list of bool
248 Helps to determine if particular column is in same parent index as the
249 previous value. Used to stop propagation of empty cells between
250 different indexes.
251
252 Returns
253 -------
254 Returns changed row and control_row
255 """
256 last = row[0]
257 for i in range(1, len(row)):
258 if not control_row[i]:
259 last = row[i]
260
261 if row[i] == "" or row[i] is None:
262 row[i] = last
263 else:
264 control_row[i] = False
265 last = row[i]
266
267 return row, control_row
268
269
270def pop_header_name(
271 row: list[Hashable], index_col: int | Sequence[int]
272) -> tuple[Hashable | None, list[Hashable]]:
273 """
274 Pop the header name for MultiIndex parsing.
275
276 Parameters
277 ----------
278 row : list
279 The data row to parse for the header name.
280 index_col : int, list
281 The index columns for our data. Assumed to be non-null.
282
283 Returns
284 -------
285 header_name : str
286 The extracted header name.
287 trimmed_row : list
288 The original data row with the header name removed.
289 """
290 # Pop out header name and fill w/blank.
291 if is_list_like(index_col):
292 assert isinstance(index_col, Iterable)
293 i = max(index_col)
294 else:
295 assert not isinstance(index_col, Iterable)
296 i = index_col
297
298 header_name = row[i]
299 header_name = None if header_name == "" else header_name
300
301 return header_name, [*row[:i], "", *row[i + 1 :]]
302
303
304def combine_kwargs(engine_kwargs: dict[str, Any] | None, kwargs: dict) -> dict:
305 """
306 Used to combine two sources of kwargs for the backend engine.
307
308 Use of kwargs is deprecated, this function is solely for use in 1.3 and should
309 be removed in 1.4/2.0. Also _base.ExcelWriter.__new__ ensures either engine_kwargs
310 or kwargs must be None or empty respectively.
311
312 Parameters
313 ----------
314 engine_kwargs: dict
315 kwargs to be passed through to the engine.
316 kwargs: dict
317 kwargs to be psased through to the engine (deprecated)
318
319 Returns
320 -------
321 engine_kwargs combined with kwargs
322 """
323 if engine_kwargs is None:
324 result = {}
325 else:
326 result = engine_kwargs.copy()
327 result.update(kwargs)
328 return result