Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# Copyright (c) 2010-2020 openpyxl 

2 

3import atexit 

4from collections import defaultdict 

5from io import BytesIO 

6import os 

7from tempfile import NamedTemporaryFile 

8from warnings import warn 

9 

10from openpyxl.xml.functions import xmlfile 

11from openpyxl.xml.constants import SHEET_MAIN_NS 

12 

13from openpyxl.comments.comment_sheet import CommentRecord 

14from openpyxl.packaging.relationship import Relationship, RelationshipList 

15from openpyxl.styles.differential import DifferentialStyle 

16 

17from .dimensions import SheetDimension 

18from .hyperlink import HyperlinkList 

19from .merge import MergeCell, MergeCells 

20from .related import Related 

21from .table import TablePartList 

22 

23from openpyxl.cell._writer import write_cell 

24 

25 

26ALL_TEMP_FILES = [] 

27 

28@atexit.register 

29def _openpyxl_shutdown(): 

30 for path in ALL_TEMP_FILES: 

31 if os.path.exists(path): 

32 os.remove(path) 

33 

34 

35def create_temporary_file(suffix=''): 

36 fobj = NamedTemporaryFile(mode='w+', suffix=suffix, 

37 prefix='openpyxl.', delete=False) 

38 filename = fobj.name 

39 fobj.close() 

40 ALL_TEMP_FILES.append(filename) 

41 return filename 

42 

43 

44class WorksheetWriter: 

45 

46 

47 def __init__(self, ws, out=None): 

48 self.ws = ws 

49 self.ws._comments = [] 

50 if out is None: 

51 out = create_temporary_file() 

52 self.out = out 

53 self._rels = RelationshipList() 

54 self.xf = self.get_stream() 

55 next(self.xf) # start generator 

56 

57 

58 def write_properties(self): 

59 props = self.ws.sheet_properties 

60 self.xf.send(props.to_tree()) 

61 

62 

63 def write_dimensions(self): 

64 """ 

65 Write worksheet size if known 

66 """ 

67 ref = getattr(self.ws, 'calculate_dimension', None) 

68 if ref: 

69 dim = SheetDimension(ref()) 

70 self.xf.send(dim.to_tree()) 

71 

72 

73 def write_format(self): 

74 self.ws.sheet_format.outlineLevelCol = self.ws.column_dimensions.max_outline 

75 fmt = self.ws.sheet_format 

76 self.xf.send(fmt.to_tree()) 

77 

78 

79 def write_views(self): 

80 views = self.ws.views 

81 self.xf.send(views.to_tree()) 

82 

83 

84 def write_cols(self): 

85 cols = self.ws.column_dimensions 

86 self.xf.send(cols.to_tree()) 

87 

88 

89 def write_top(self): 

90 """ 

91 Write all elements up to rows: 

92 properties 

93 dimensions 

94 views 

95 format 

96 cols 

97 """ 

98 self.write_properties() 

99 self.write_dimensions() 

100 self.write_views() 

101 self.write_format() 

102 self.write_cols() 

103 

104 

105 def rows(self): 

106 """Return all rows, and any cells that they contain""" 

107 # order cells by row 

108 rows = defaultdict(list) 

109 for (row, col), cell in sorted(self.ws._cells.items()): 

110 rows[row].append(cell) 

111 

112 # add empty rows if styling has been applied 

113 for row in self.ws.row_dimensions.keys() - rows.keys(): 

114 rows[row] = [] 

115 

116 return sorted(rows.items()) 

117 

118 

119 def write_rows(self): 

120 xf = self.xf.send(True) 

121 

122 with xf.element("sheetData"): 

123 for row_idx, row in self.rows(): 

124 self.write_row(xf, row, row_idx) 

125 

126 self.xf.send(None) # return control to generator 

127 

128 

129 def write_row(self, xf, row, row_idx): 

130 attrs = {'r': f"{row_idx}"} 

131 dims = self.ws.row_dimensions 

132 attrs.update(dims.get(row_idx, {})) 

133 

134 with xf.element("row", attrs): 

135 

136 for cell in row: 

137 if cell._comment is not None: 

138 comment = CommentRecord.from_cell(cell) 

139 self.ws._comments.append(comment) 

140 if ( 

141 cell._value is None 

142 and not cell.has_style 

143 and not cell._comment 

144 ): 

145 continue 

146 write_cell(xf, self.ws, cell, cell.has_style) 

147 

148 

149 def write_protection(self): 

150 prot = self.ws.protection 

151 if prot: 

152 self.xf.send(prot.to_tree()) 

153 

154 

155 def write_scenarios(self): 

156 scenarios = self.ws.scenarios 

157 if scenarios: 

158 self.xf.send(scenarios.to_tree()) 

159 

160 

161 def write_filter(self): 

162 flt = self.ws.auto_filter 

163 if flt: 

164 self.xf.send(flt.to_tree()) 

165 

166 

167 def write_sort(self): 

168 """ 

169 As per discusion with the OOXML Working Group global sort state is not required. 

170 openpyxl never reads it from existing files 

171 """ 

172 pass 

173 

174 

175 def write_merged_cells(self): 

176 merged = self.ws.merged_cells 

177 if merged: 

178 cells = [MergeCell(str(ref)) for ref in self.ws.merged_cells] 

179 self.xf.send(MergeCells(mergeCell=cells).to_tree()) 

180 

181 

182 def write_formatting(self): 

183 df = DifferentialStyle() 

184 wb = self.ws.parent 

185 for cf in self.ws.conditional_formatting: 

186 for rule in cf.rules: 

187 if rule.dxf and rule.dxf != df: 

188 rule.dxfId = wb._differential_styles.add(rule.dxf) 

189 self.xf.send(cf.to_tree()) 

190 

191 

192 def write_validations(self): 

193 dv = self.ws.data_validations 

194 if dv: 

195 self.xf.send(dv.to_tree()) 

196 

197 

198 def write_hyperlinks(self): 

199 links = HyperlinkList() 

200 

201 for link in self.ws._hyperlinks: 

202 if link.target: 

203 rel = Relationship(type="hyperlink", TargetMode="External", Target=link.target) 

204 self._rels.append(rel) 

205 link.id = rel.id 

206 links.hyperlink.append(link) 

207 

208 if links: 

209 self.xf.send(links.to_tree()) 

210 

211 

212 def write_print(self): 

213 print_options = self.ws.print_options 

214 if print_options: 

215 self.xf.send(print_options.to_tree()) 

216 

217 

218 def write_margins(self): 

219 margins = self.ws.page_margins 

220 if margins: 

221 self.xf.send(margins.to_tree()) 

222 

223 

224 def write_page(self): 

225 setup = self.ws.page_setup 

226 if setup: 

227 self.xf.send(setup.to_tree()) 

228 

229 

230 def write_header(self): 

231 hf = self.ws.HeaderFooter 

232 if hf: 

233 self.xf.send(hf.to_tree()) 

234 

235 

236 def write_breaks(self): 

237 brks = (self.ws.row_breaks, self.ws.col_breaks) 

238 for brk in brks: 

239 if brk: 

240 self.xf.send(brk.to_tree()) 

241 

242 

243 def write_drawings(self): 

244 if self.ws._charts or self.ws._images: 

245 rel = Relationship(type="drawing", Target="") 

246 self._rels.append(rel) 

247 drawing = Related() 

248 drawing.id = rel.id 

249 self.xf.send(drawing.to_tree("drawing")) 

250 

251 

252 def write_legacy(self): 

253 """ 

254 Comments & VBA controls use VML and require an additional element 

255 that is no longer in the specification. 

256 """ 

257 if (self.ws.legacy_drawing is not None or self.ws._comments): 

258 legacy = Related(id="anysvml") 

259 self.xf.send(legacy.to_tree("legacyDrawing")) 

260 

261 

262 def write_tables(self): 

263 tables = TablePartList() 

264 

265 for table in self.ws._tables.values(): 

266 if not table.tableColumns: 

267 table._initialise_columns() 

268 if table.headerRowCount: 

269 try: 

270 row = self.ws[table.ref][0] 

271 for cell, col in zip(row, table.tableColumns): 

272 if cell.data_type != "s": 

273 warn("File may not be readable: column headings must be strings.") 

274 col.name = str(cell.value) 

275 except TypeError: 

276 warn("Column headings are missing, file may not be readable") 

277 rel = Relationship(Type=table._rel_type, Target="") 

278 self._rels.append(rel) 

279 table._rel_id = rel.Id 

280 tables.append(Related(id=rel.Id)) 

281 

282 if tables: 

283 self.xf.send(tables.to_tree()) 

284 

285 

286 def get_stream(self): 

287 with xmlfile(self.out) as xf: 

288 with xf.element("worksheet", xmlns=SHEET_MAIN_NS): 

289 try: 

290 while True: 

291 el = (yield) 

292 if el is True: 

293 yield xf 

294 elif el is None: # et_xmlfile chokes 

295 continue 

296 else: 

297 xf.write(el) 

298 except GeneratorExit: 

299 pass 

300 

301 

302 def write_tail(self): 

303 """ 

304 Write all elements after the rows 

305 calc properties 

306 protection 

307 protected ranges # 

308 scenarios 

309 filters 

310 sorts # always ignored 

311 data consolidation # 

312 custom views # 

313 merged cells 

314 phonetic properties # 

315 conditional formatting 

316 data validation 

317 hyperlinks 

318 print options 

319 page margins 

320 page setup 

321 header 

322 row breaks 

323 col breaks 

324 custom properties # 

325 cell watches # 

326 ignored errors # 

327 smart tags # 

328 drawing 

329 drawingHF # 

330 background # 

331 OLE objects # 

332 controls # 

333 web publishing # 

334 tables 

335 """ 

336 self.write_protection() 

337 self.write_scenarios() 

338 self.write_filter() 

339 self.write_merged_cells() 

340 self.write_formatting() 

341 self.write_validations() 

342 self.write_hyperlinks() 

343 self.write_print() 

344 self.write_margins() 

345 self.write_page() 

346 self.write_header() 

347 self.write_breaks() 

348 self.write_drawings() 

349 self.write_legacy() 

350 self.write_tables() 

351 

352 

353 def write(self): 

354 """ 

355 High level 

356 """ 

357 self.write_top() 

358 self.write_rows() 

359 self.write_tail() 

360 self.close() 

361 

362 

363 def close(self): 

364 """ 

365 Close the context manager 

366 """ 

367 if self.xf: 

368 self.xf.close() 

369 

370 

371 def read(self): 

372 """ 

373 Close the context manager and return serialised XML 

374 """ 

375 self.close() 

376 if isinstance(self.out, BytesIO): 

377 return self.out.getvalue() 

378 with open(self.out, "rb") as src: 

379 out = src.read() 

380 

381 return out 

382 

383 

384 def cleanup(self): 

385 """ 

386 Remove tempfile 

387 """ 

388 os.remove(self.out) 

389 ALL_TEMP_FILES.remove(self.out)