Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/io/excel/_odswriter.py: 18%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

147 statements  

1from __future__ import annotations 

2 

3from collections import defaultdict 

4import datetime 

5import json 

6from typing import ( 

7 TYPE_CHECKING, 

8 Any, 

9 DefaultDict, 

10 cast, 

11 overload, 

12) 

13 

14from pandas.io.excel._base import ExcelWriter 

15from pandas.io.excel._util import ( 

16 combine_kwargs, 

17 validate_freeze_panes, 

18) 

19 

20if TYPE_CHECKING: 

21 from odf.opendocument import OpenDocumentSpreadsheet 

22 

23 from pandas._typing import ( 

24 ExcelWriterIfSheetExists, 

25 FilePath, 

26 StorageOptions, 

27 WriteExcelBuffer, 

28 ) 

29 

30 from pandas.io.formats.excel import ExcelCell 

31 

32 

33class ODSWriter(ExcelWriter): 

34 _engine = "odf" 

35 _supported_extensions = (".ods",) 

36 

37 def __init__( # pyright: ignore[reportInconsistentConstructor] 

38 self, 

39 path: FilePath | WriteExcelBuffer | ExcelWriter, 

40 engine: str | None = None, 

41 date_format: str | None = None, 

42 datetime_format: str | None = None, 

43 mode: str = "w", 

44 storage_options: StorageOptions | None = None, 

45 if_sheet_exists: ExcelWriterIfSheetExists | None = None, 

46 engine_kwargs: dict[str, Any] | None = None, 

47 **kwargs: Any, 

48 ) -> None: 

49 from odf.opendocument import OpenDocumentSpreadsheet 

50 

51 if mode == "a": 

52 raise ValueError("Append mode is not supported with odf!") 

53 

54 engine_kwargs = combine_kwargs(engine_kwargs, kwargs) 

55 self._book = OpenDocumentSpreadsheet(**engine_kwargs) 

56 

57 super().__init__( 

58 path, 

59 mode=mode, 

60 storage_options=storage_options, 

61 if_sheet_exists=if_sheet_exists, 

62 engine_kwargs=engine_kwargs, 

63 ) 

64 

65 self._style_dict: dict[str, str] = {} 

66 

67 @property 

68 def book(self) -> OpenDocumentSpreadsheet: 

69 """ 

70 Book instance of class odf.opendocument.OpenDocumentSpreadsheet. 

71 

72 This attribute can be used to access engine-specific features. 

73 """ 

74 return self._book 

75 

76 @property 

77 def sheets(self) -> dict[str, Any]: 

78 """Mapping of sheet names to sheet objects.""" 

79 from odf.table import Table 

80 

81 result = { 

82 sheet.getAttribute("name"): sheet 

83 for sheet in self.book.getElementsByType(Table) 

84 } 

85 return result 

86 

87 def _save(self) -> None: 

88 """ 

89 Save workbook to disk. 

90 """ 

91 for sheet in self.sheets.values(): 

92 self.book.spreadsheet.addElement(sheet) 

93 self.book.save(self._handles.handle) 

94 

95 def _write_cells( 

96 self, 

97 cells: list[ExcelCell], 

98 sheet_name: str | None = None, 

99 startrow: int = 0, 

100 startcol: int = 0, 

101 freeze_panes: tuple[int, int] | None = None, 

102 autofilter_range: str | None = None, 

103 ) -> None: 

104 """ 

105 Write the frame cells using odf 

106 """ 

107 

108 if autofilter_range: 

109 raise ValueError("Autofilter is not supported with odf!") 

110 

111 from odf.table import ( 

112 Table, 

113 TableCell, 

114 TableRow, 

115 ) 

116 from odf.text import P 

117 

118 sheet_name = self._get_sheet_name(sheet_name) 

119 assert sheet_name is not None 

120 

121 if sheet_name in self.sheets: 

122 wks = self.sheets[sheet_name] 

123 else: 

124 wks = Table(name=sheet_name) 

125 self.book.spreadsheet.addElement(wks) 

126 

127 if validate_freeze_panes(freeze_panes): 

128 freeze_panes = cast(tuple[int, int], freeze_panes) 

129 self._create_freeze_panes(sheet_name, freeze_panes) 

130 

131 for _ in range(startrow): 

132 wks.addElement(TableRow()) 

133 

134 rows: DefaultDict = defaultdict(TableRow) 

135 col_count: DefaultDict = defaultdict(int) 

136 

137 for cell in sorted(cells, key=lambda cell: (cell.row, cell.col)): 

138 # only add empty cells if the row is still empty 

139 if not col_count[cell.row]: 

140 for _ in range(startcol): 

141 rows[cell.row].addElement(TableCell()) 

142 

143 # fill with empty cells if needed 

144 for _ in range(cell.col - col_count[cell.row]): 

145 rows[cell.row].addElement(TableCell()) 

146 col_count[cell.row] += 1 

147 

148 pvalue, tc = self._make_table_cell(cell) 

149 rows[cell.row].addElement(tc) 

150 col_count[cell.row] += 1 

151 p = P(text=pvalue) 

152 tc.addElement(p) 

153 

154 # add all rows to the sheet 

155 if len(rows) > 0: 

156 for row_nr in range(max(rows.keys()) + 1): 

157 wks.addElement(rows[row_nr]) 

158 

159 def _make_table_cell_attributes(self, cell: ExcelCell) -> dict[str, int | str]: 

160 """Convert cell attributes to OpenDocument attributes 

161 

162 Parameters 

163 ---------- 

164 cell : ExcelCell 

165 Spreadsheet cell data 

166 

167 Returns 

168 ------- 

169 attributes : Dict[str, Union[int, str]] 

170 Dictionary with attributes and attribute values 

171 """ 

172 attributes: dict[str, int | str] = {} 

173 style_name = self._process_style(cell.style) 

174 if style_name is not None: 

175 attributes["stylename"] = style_name 

176 if cell.mergestart is not None and cell.mergeend is not None: 

177 attributes["numberrowsspanned"] = max(1, cell.mergestart) 

178 attributes["numbercolumnsspanned"] = cell.mergeend 

179 return attributes 

180 

181 def _make_table_cell(self, cell: ExcelCell) -> tuple[object, Any]: 

182 """Convert cell data to an OpenDocument spreadsheet cell 

183 

184 Parameters 

185 ---------- 

186 cell : ExcelCell 

187 Spreadsheet cell data 

188 

189 Returns 

190 ------- 

191 pvalue, cell : Tuple[str, TableCell] 

192 Display value, Cell value 

193 """ 

194 from odf.table import TableCell 

195 

196 attributes = self._make_table_cell_attributes(cell) 

197 val, fmt = self._value_with_fmt(cell.val) 

198 pvalue = value = val 

199 if isinstance(val, bool): 

200 value = str(val).lower() 

201 pvalue = str(val).upper() 

202 return ( 

203 pvalue, 

204 TableCell( 

205 valuetype="boolean", 

206 booleanvalue=value, 

207 attributes=attributes, 

208 ), 

209 ) 

210 elif isinstance(val, datetime.datetime): 

211 # Fast formatting 

212 value = val.isoformat() 

213 # Slow but locale-dependent 

214 pvalue = val.strftime("%c") 

215 return ( 

216 pvalue, 

217 TableCell(valuetype="date", datevalue=value, attributes=attributes), 

218 ) 

219 elif isinstance(val, datetime.date): 

220 # Fast formatting 

221 value = f"{val.year}-{val.month:02d}-{val.day:02d}" 

222 # Slow but locale-dependent 

223 pvalue = val.strftime("%x") 

224 return ( 

225 pvalue, 

226 TableCell(valuetype="date", datevalue=value, attributes=attributes), 

227 ) 

228 elif isinstance(val, str): 

229 return ( 

230 pvalue, 

231 TableCell( 

232 valuetype="string", 

233 stringvalue=value, 

234 attributes=attributes, 

235 ), 

236 ) 

237 else: 

238 return ( 

239 pvalue, 

240 TableCell( 

241 valuetype="float", 

242 value=value, 

243 attributes=attributes, 

244 ), 

245 ) 

246 

247 @overload 

248 def _process_style(self, style: dict[str, Any]) -> str: ... 

249 

250 @overload 

251 def _process_style(self, style: None) -> None: ... 

252 

253 def _process_style(self, style: dict[str, Any] | None) -> str | None: 

254 """Convert a style dictionary to an OpenDocument style sheet 

255 

256 Parameters 

257 ---------- 

258 style : Dict 

259 Style dictionary 

260 

261 Returns 

262 ------- 

263 style_key : str 

264 Unique style key for later reference in sheet 

265 """ 

266 from odf.style import ( 

267 ParagraphProperties, 

268 Style, 

269 TableCellProperties, 

270 TextProperties, 

271 ) 

272 

273 if style is None: 

274 return None 

275 style_key = json.dumps(style) 

276 if style_key in self._style_dict: 

277 return self._style_dict[style_key] 

278 name = f"pd{len(self._style_dict) + 1}" 

279 self._style_dict[style_key] = name 

280 odf_style = Style(name=name, family="table-cell") 

281 if "font" in style: 

282 font = style["font"] 

283 if font.get("bold", False): 

284 odf_style.addElement(TextProperties(fontweight="bold")) 

285 if "borders" in style: 

286 borders = style["borders"] 

287 for side, thickness in borders.items(): 

288 thickness_translation = {"thin": "0.75pt solid #000000"} 

289 odf_style.addElement( 

290 TableCellProperties( 

291 attributes={f"border{side}": thickness_translation[thickness]} 

292 ) 

293 ) 

294 if "alignment" in style: 

295 alignment = style["alignment"] 

296 horizontal = alignment.get("horizontal") 

297 if horizontal: 

298 odf_style.addElement(ParagraphProperties(textalign=horizontal)) 

299 vertical = alignment.get("vertical") 

300 if vertical: 

301 odf_style.addElement(TableCellProperties(verticalalign=vertical)) 

302 self.book.styles.addElement(odf_style) 

303 return name 

304 

305 def _create_freeze_panes( 

306 self, sheet_name: str, freeze_panes: tuple[int, int] 

307 ) -> None: 

308 """ 

309 Create freeze panes in the sheet. 

310 

311 Parameters 

312 ---------- 

313 sheet_name : str 

314 Name of the spreadsheet 

315 freeze_panes : tuple of (int, int) 

316 Freeze pane location x and y 

317 """ 

318 from odf.config import ( 

319 ConfigItem, 

320 ConfigItemMapEntry, 

321 ConfigItemMapIndexed, 

322 ConfigItemMapNamed, 

323 ConfigItemSet, 

324 ) 

325 

326 config_item_set = ConfigItemSet(name="ooo:view-settings") 

327 self.book.settings.addElement(config_item_set) 

328 

329 config_item_map_indexed = ConfigItemMapIndexed(name="Views") 

330 config_item_set.addElement(config_item_map_indexed) 

331 

332 config_item_map_entry = ConfigItemMapEntry() 

333 config_item_map_indexed.addElement(config_item_map_entry) 

334 

335 config_item_map_named = ConfigItemMapNamed(name="Tables") 

336 config_item_map_entry.addElement(config_item_map_named) 

337 

338 config_item_map_entry = ConfigItemMapEntry(name=sheet_name) 

339 config_item_map_named.addElement(config_item_map_entry) 

340 

341 config_item_map_entry.addElement( 

342 ConfigItem(name="HorizontalSplitMode", type="short", text="2") 

343 ) 

344 config_item_map_entry.addElement( 

345 ConfigItem(name="VerticalSplitMode", type="short", text="2") 

346 ) 

347 config_item_map_entry.addElement( 

348 ConfigItem( 

349 name="HorizontalSplitPosition", type="int", text=str(freeze_panes[0]) 

350 ) 

351 ) 

352 config_item_map_entry.addElement( 

353 ConfigItem( 

354 name="VerticalSplitPosition", type="int", text=str(freeze_panes[1]) 

355 ) 

356 ) 

357 config_item_map_entry.addElement( 

358 ConfigItem(name="PositionRight", type="int", text=str(freeze_panes[0])) 

359 ) 

360 config_item_map_entry.addElement( 

361 ConfigItem(name="PositionBottom", type="int", text=str(freeze_panes[1])) 

362 )