Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-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

146 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 pandas._typing import ( 

22 ExcelWriterIfSheetExists, 

23 FilePath, 

24 StorageOptions, 

25 WriteExcelBuffer, 

26 ) 

27 

28 from pandas.io.formats.excel import ExcelCell 

29 

30 

31class ODSWriter(ExcelWriter): 

32 _engine = "odf" 

33 _supported_extensions = (".ods",) 

34 

35 def __init__( 

36 self, 

37 path: FilePath | WriteExcelBuffer | ExcelWriter, 

38 engine: str | None = None, 

39 date_format: str | None = None, 

40 datetime_format=None, 

41 mode: str = "w", 

42 storage_options: StorageOptions | None = None, 

43 if_sheet_exists: ExcelWriterIfSheetExists | None = None, 

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

45 **kwargs, 

46 ) -> None: 

47 from odf.opendocument import OpenDocumentSpreadsheet 

48 

49 if mode == "a": 

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

51 

52 engine_kwargs = combine_kwargs(engine_kwargs, kwargs) 

53 self._book = OpenDocumentSpreadsheet(**engine_kwargs) 

54 

55 super().__init__( 

56 path, 

57 mode=mode, 

58 storage_options=storage_options, 

59 if_sheet_exists=if_sheet_exists, 

60 engine_kwargs=engine_kwargs, 

61 ) 

62 

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

64 

65 @property 

66 def book(self): 

67 """ 

68 Book instance of class odf.opendocument.OpenDocumentSpreadsheet. 

69 

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

71 """ 

72 return self._book 

73 

74 @property 

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

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

77 from odf.table import Table 

78 

79 result = { 

80 sheet.getAttribute("name"): sheet 

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

82 } 

83 return result 

84 

85 def _save(self) -> None: 

86 """ 

87 Save workbook to disk. 

88 """ 

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

90 self.book.spreadsheet.addElement(sheet) 

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

92 

93 def _write_cells( 

94 self, 

95 cells: list[ExcelCell], 

96 sheet_name: str | None = None, 

97 startrow: int = 0, 

98 startcol: int = 0, 

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

100 ) -> None: 

101 """ 

102 Write the frame cells using odf 

103 """ 

104 from odf.table import ( 

105 Table, 

106 TableCell, 

107 TableRow, 

108 ) 

109 from odf.text import P 

110 

111 sheet_name = self._get_sheet_name(sheet_name) 

112 assert sheet_name is not None 

113 

114 if sheet_name in self.sheets: 

115 wks = self.sheets[sheet_name] 

116 else: 

117 wks = Table(name=sheet_name) 

118 self.book.spreadsheet.addElement(wks) 

119 

120 if validate_freeze_panes(freeze_panes): 

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

122 self._create_freeze_panes(sheet_name, freeze_panes) 

123 

124 for _ in range(startrow): 

125 wks.addElement(TableRow()) 

126 

127 rows: DefaultDict = defaultdict(TableRow) 

128 col_count: DefaultDict = defaultdict(int) 

129 

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

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

132 if not col_count[cell.row]: 

133 for _ in range(startcol): 

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

135 

136 # fill with empty cells if needed 

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

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

139 col_count[cell.row] += 1 

140 

141 pvalue, tc = self._make_table_cell(cell) 

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

143 col_count[cell.row] += 1 

144 p = P(text=pvalue) 

145 tc.addElement(p) 

146 

147 # add all rows to the sheet 

148 if len(rows) > 0: 

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

150 wks.addElement(rows[row_nr]) 

151 

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

153 """Convert cell attributes to OpenDocument attributes 

154 

155 Parameters 

156 ---------- 

157 cell : ExcelCell 

158 Spreadsheet cell data 

159 

160 Returns 

161 ------- 

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

163 Dictionary with attributes and attribute values 

164 """ 

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

166 style_name = self._process_style(cell.style) 

167 if style_name is not None: 

168 attributes["stylename"] = style_name 

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

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

171 attributes["numbercolumnsspanned"] = cell.mergeend 

172 return attributes 

173 

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

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

176 

177 Parameters 

178 ---------- 

179 cell : ExcelCell 

180 Spreadsheet cell data 

181 

182 Returns 

183 ------- 

184 pvalue, cell : Tuple[str, TableCell] 

185 Display value, Cell value 

186 """ 

187 from odf.table import TableCell 

188 

189 attributes = self._make_table_cell_attributes(cell) 

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

191 pvalue = value = val 

192 if isinstance(val, bool): 

193 value = str(val).lower() 

194 pvalue = str(val).upper() 

195 return ( 

196 pvalue, 

197 TableCell( 

198 valuetype="boolean", 

199 booleanvalue=value, 

200 attributes=attributes, 

201 ), 

202 ) 

203 elif isinstance(val, datetime.datetime): 

204 # Fast formatting 

205 value = val.isoformat() 

206 # Slow but locale-dependent 

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

208 return ( 

209 pvalue, 

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

211 ) 

212 elif isinstance(val, datetime.date): 

213 # Fast formatting 

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

215 # Slow but locale-dependent 

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

217 return ( 

218 pvalue, 

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

220 ) 

221 elif isinstance(val, str): 

222 return ( 

223 pvalue, 

224 TableCell( 

225 valuetype="string", 

226 stringvalue=value, 

227 attributes=attributes, 

228 ), 

229 ) 

230 else: 

231 return ( 

232 pvalue, 

233 TableCell( 

234 valuetype="float", 

235 value=value, 

236 attributes=attributes, 

237 ), 

238 ) 

239 

240 @overload 

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

242 ... 

243 

244 @overload 

245 def _process_style(self, style: None) -> None: 

246 ... 

247 

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

249 """Convert a style dictionary to a OpenDocument style sheet 

250 

251 Parameters 

252 ---------- 

253 style : Dict 

254 Style dictionary 

255 

256 Returns 

257 ------- 

258 style_key : str 

259 Unique style key for later reference in sheet 

260 """ 

261 from odf.style import ( 

262 ParagraphProperties, 

263 Style, 

264 TableCellProperties, 

265 TextProperties, 

266 ) 

267 

268 if style is None: 

269 return None 

270 style_key = json.dumps(style) 

271 if style_key in self._style_dict: 

272 return self._style_dict[style_key] 

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

274 self._style_dict[style_key] = name 

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

276 if "font" in style: 

277 font = style["font"] 

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

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

280 if "borders" in style: 

281 borders = style["borders"] 

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

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

284 odf_style.addElement( 

285 TableCellProperties( 

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

287 ) 

288 ) 

289 if "alignment" in style: 

290 alignment = style["alignment"] 

291 horizontal = alignment.get("horizontal") 

292 if horizontal: 

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

294 vertical = alignment.get("vertical") 

295 if vertical: 

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

297 self.book.styles.addElement(odf_style) 

298 return name 

299 

300 def _create_freeze_panes( 

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

302 ) -> None: 

303 """ 

304 Create freeze panes in the sheet. 

305 

306 Parameters 

307 ---------- 

308 sheet_name : str 

309 Name of the spreadsheet 

310 freeze_panes : tuple of (int, int) 

311 Freeze pane location x and y 

312 """ 

313 from odf.config import ( 

314 ConfigItem, 

315 ConfigItemMapEntry, 

316 ConfigItemMapIndexed, 

317 ConfigItemMapNamed, 

318 ConfigItemSet, 

319 ) 

320 

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

322 self.book.settings.addElement(config_item_set) 

323 

324 config_item_map_indexed = ConfigItemMapIndexed(name="Views") 

325 config_item_set.addElement(config_item_map_indexed) 

326 

327 config_item_map_entry = ConfigItemMapEntry() 

328 config_item_map_indexed.addElement(config_item_map_entry) 

329 

330 config_item_map_named = ConfigItemMapNamed(name="Tables") 

331 config_item_map_entry.addElement(config_item_map_named) 

332 

333 config_item_map_entry = ConfigItemMapEntry(name=sheet_name) 

334 config_item_map_named.addElement(config_item_map_entry) 

335 

336 config_item_map_entry.addElement( 

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

338 ) 

339 config_item_map_entry.addElement( 

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

341 ) 

342 config_item_map_entry.addElement( 

343 ConfigItem( 

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

345 ) 

346 ) 

347 config_item_map_entry.addElement( 

348 ConfigItem( 

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

350 ) 

351 ) 

352 config_item_map_entry.addElement( 

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

354 ) 

355 config_item_map_entry.addElement( 

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

357 )