Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/pandas/io/excel/_odswriter.py: 14%

143 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1from __future__ import annotations 

2 

3from collections import defaultdict 

4import datetime 

5from typing import ( 

6 TYPE_CHECKING, 

7 Any, 

8 DefaultDict, 

9 Tuple, 

10 cast, 

11) 

12 

13import pandas._libs.json as json 

14from pandas._typing import ( 

15 FilePath, 

16 StorageOptions, 

17 WriteExcelBuffer, 

18) 

19 

20from pandas.io.excel._base import ExcelWriter 

21from pandas.io.excel._util import ( 

22 combine_kwargs, 

23 validate_freeze_panes, 

24) 

25 

26if TYPE_CHECKING: 26 ↛ 27line 26 didn't jump to line 27, because the condition on line 26 was never true

27 from odf.opendocument import OpenDocumentSpreadsheet 

28 

29 from pandas.io.formats.excel import ExcelCell 

30 

31 

32class ODSWriter(ExcelWriter): 

33 _engine = "odf" 

34 _supported_extensions = (".ods",) 

35 

36 def __init__( 

37 self, 

38 path: FilePath | WriteExcelBuffer | ExcelWriter, 

39 engine: str | None = None, 

40 date_format: str | None = None, 

41 datetime_format=None, 

42 mode: str = "w", 

43 storage_options: StorageOptions = None, 

44 if_sheet_exists: str | None = None, 

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

46 **kwargs, 

47 ) -> None: 

48 from odf.opendocument import OpenDocumentSpreadsheet 

49 

50 if mode == "a": 

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

52 

53 super().__init__( 

54 path, 

55 mode=mode, 

56 storage_options=storage_options, 

57 if_sheet_exists=if_sheet_exists, 

58 engine_kwargs=engine_kwargs, 

59 ) 

60 

61 engine_kwargs = combine_kwargs(engine_kwargs, kwargs) 

62 

63 self._book = OpenDocumentSpreadsheet(**engine_kwargs) 

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

65 

66 @property 

67 def book(self): 

68 """ 

69 Book instance of class odf.opendocument.OpenDocumentSpreadsheet. 

70 

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

72 """ 

73 return self._book 

74 

75 @book.setter 

76 def book(self, other: OpenDocumentSpreadsheet) -> None: 

77 """ 

78 Set book instance. Class type will depend on the engine used. 

79 """ 

80 self._deprecate_set_book() 

81 self._book = other 

82 

83 @property 

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

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

86 from odf.table import Table 

87 

88 result = { 

89 sheet.getAttribute("name"): sheet 

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

91 } 

92 return result 

93 

94 def _save(self) -> None: 

95 """ 

96 Save workbook to disk. 

97 """ 

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

99 self.book.spreadsheet.addElement(sheet) 

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

101 

102 def _write_cells( 

103 self, 

104 cells: list[ExcelCell], 

105 sheet_name: str | None = None, 

106 startrow: int = 0, 

107 startcol: int = 0, 

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

109 ) -> None: 

110 """ 

111 Write the frame cells using odf 

112 """ 

113 from odf.table import ( 

114 Table, 

115 TableCell, 

116 TableRow, 

117 ) 

118 from odf.text import P 

119 

120 sheet_name = self._get_sheet_name(sheet_name) 

121 assert sheet_name is not None 

122 

123 if sheet_name in self.sheets: 

124 wks = self.sheets[sheet_name] 

125 else: 

126 wks = Table(name=sheet_name) 

127 self.book.spreadsheet.addElement(wks) 

128 

129 if validate_freeze_panes(freeze_panes): 

130 freeze_panes = cast(Tuple[int, int], freeze_panes) 

131 self._create_freeze_panes(sheet_name, freeze_panes) 

132 

133 for _ in range(startrow): 

134 wks.addElement(TableRow()) 

135 

136 rows: DefaultDict = defaultdict(TableRow) 

137 col_count: DefaultDict = defaultdict(int) 

138 

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

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

141 if not col_count[cell.row]: 

142 for _ in range(startcol): 

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

144 

145 # fill with empty cells if needed 

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

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

148 col_count[cell.row] += 1 

149 

150 pvalue, tc = self._make_table_cell(cell) 

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

152 col_count[cell.row] += 1 

153 p = P(text=pvalue) 

154 tc.addElement(p) 

155 

156 # add all rows to the sheet 

157 if len(rows) > 0: 

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

159 wks.addElement(rows[row_nr]) 

160 

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

162 """Convert cell attributes to OpenDocument attributes 

163 

164 Parameters 

165 ---------- 

166 cell : ExcelCell 

167 Spreadsheet cell data 

168 

169 Returns 

170 ------- 

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

172 Dictionary with attributes and attribute values 

173 """ 

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

175 style_name = self._process_style(cell.style) 

176 if style_name is not None: 

177 attributes["stylename"] = style_name 

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

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

180 attributes["numbercolumnsspanned"] = cell.mergeend 

181 return attributes 

182 

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

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

185 

186 Parameters 

187 ---------- 

188 cell : ExcelCell 

189 Spreadsheet cell data 

190 

191 Returns 

192 ------- 

193 pvalue, cell : Tuple[str, TableCell] 

194 Display value, Cell value 

195 """ 

196 from odf.table import TableCell 

197 

198 attributes = self._make_table_cell_attributes(cell) 

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

200 pvalue = value = val 

201 if isinstance(val, bool): 

202 value = str(val).lower() 

203 pvalue = str(val).upper() 

204 if isinstance(val, datetime.datetime): 

205 # Fast formatting 

206 value = val.isoformat() 

207 # Slow but locale-dependent 

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

209 return ( 

210 pvalue, 

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

212 ) 

213 elif isinstance(val, datetime.date): 

214 # Fast formatting 

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

216 # Slow but locale-dependent 

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

218 return ( 

219 pvalue, 

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

221 ) 

222 else: 

223 class_to_cell_type = { 

224 str: "string", 

225 int: "float", 

226 float: "float", 

227 bool: "boolean", 

228 } 

229 return ( 

230 pvalue, 

231 TableCell( 

232 valuetype=class_to_cell_type[type(val)], 

233 value=value, 

234 attributes=attributes, 

235 ), 

236 ) 

237 

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

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

240 

241 Parameters 

242 ---------- 

243 style : Dict 

244 Style dictionary 

245 

246 Returns 

247 ------- 

248 style_key : str 

249 Unique style key for later reference in sheet 

250 """ 

251 from odf.style import ( 

252 ParagraphProperties, 

253 Style, 

254 TableCellProperties, 

255 TextProperties, 

256 ) 

257 

258 if style is None: 

259 return None 

260 style_key = json.dumps(style) 

261 if style_key in self._style_dict: 

262 return self._style_dict[style_key] 

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

264 self._style_dict[style_key] = name 

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

266 if "font" in style: 

267 font = style["font"] 

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

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

270 if "borders" in style: 

271 borders = style["borders"] 

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

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

274 odf_style.addElement( 

275 TableCellProperties( 

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

277 ) 

278 ) 

279 if "alignment" in style: 

280 alignment = style["alignment"] 

281 horizontal = alignment.get("horizontal") 

282 if horizontal: 

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

284 vertical = alignment.get("vertical") 

285 if vertical: 

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

287 self.book.styles.addElement(odf_style) 

288 return name 

289 

290 def _create_freeze_panes( 

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

292 ) -> None: 

293 """ 

294 Create freeze panes in the sheet. 

295 

296 Parameters 

297 ---------- 

298 sheet_name : str 

299 Name of the spreadsheet 

300 freeze_panes : tuple of (int, int) 

301 Freeze pane location x and y 

302 """ 

303 from odf.config import ( 

304 ConfigItem, 

305 ConfigItemMapEntry, 

306 ConfigItemMapIndexed, 

307 ConfigItemMapNamed, 

308 ConfigItemSet, 

309 ) 

310 

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

312 self.book.settings.addElement(config_item_set) 

313 

314 config_item_map_indexed = ConfigItemMapIndexed(name="Views") 

315 config_item_set.addElement(config_item_map_indexed) 

316 

317 config_item_map_entry = ConfigItemMapEntry() 

318 config_item_map_indexed.addElement(config_item_map_entry) 

319 

320 config_item_map_named = ConfigItemMapNamed(name="Tables") 

321 config_item_map_entry.addElement(config_item_map_named) 

322 

323 config_item_map_entry = ConfigItemMapEntry(name=sheet_name) 

324 config_item_map_named.addElement(config_item_map_entry) 

325 

326 config_item_map_entry.addElement( 

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

328 ) 

329 config_item_map_entry.addElement( 

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

331 ) 

332 config_item_map_entry.addElement( 

333 ConfigItem( 

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

335 ) 

336 ) 

337 config_item_map_entry.addElement( 

338 ConfigItem( 

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

340 ) 

341 ) 

342 config_item_map_entry.addElement( 

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

344 ) 

345 config_item_map_entry.addElement( 

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

347 )