Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/xlsxwriter/comments.py: 13%

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

184 statements  

1############################################################################### 

2# 

3# Comments - A class for writing the Excel XLSX Worksheet file. 

4# 

5# SPDX-License-Identifier: BSD-2-Clause 

6# 

7# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org 

8# 

9 

10from typing import Dict, List, Optional, Union 

11 

12from xlsxwriter import xmlwriter 

13from xlsxwriter.color import Color 

14from xlsxwriter.utility import ( 

15 _preserve_whitespace, 

16 xl_cell_to_rowcol, 

17 xl_rowcol_to_cell, 

18) 

19 

20 

21########################################################################### 

22# 

23# A comment type class. 

24# 

25########################################################################### 

26class CommentType: 

27 """ 

28 A class to represent a comment in an Excel worksheet. 

29 

30 """ 

31 

32 def __init__( 

33 self, 

34 row: int, 

35 col: int, 

36 text: str, 

37 options: Optional[Dict[str, Union[str, int, float]]] = None, 

38 ) -> None: 

39 """ 

40 Initialize a Comment instance. 

41 

42 Args: 

43 row (int): The row number of the comment. 

44 col (int): The column number of the comment. 

45 text (str): The text of the comment. 

46 options (dict): Additional options for the comment. 

47 """ 

48 self.row: int = row 

49 self.col: int = col 

50 self.text: str = text 

51 

52 self.author: Optional[str] = None 

53 self.color: Color = Color("#ffffe1") 

54 

55 self.start_row: int = 0 

56 self.start_col: int = 0 

57 

58 self.is_visible: Optional[bool] = None 

59 

60 self.width: float = 128 

61 self.height: float = 74 

62 

63 self.x_scale: float = 1 

64 self.y_scale: float = 1 

65 self.x_offset: int = 0 

66 self.y_offset: int = 0 

67 

68 self.font_size: float = 8 

69 self.font_name: str = "Tahoma" 

70 self.font_family: int = 2 

71 

72 self.vertices: List[Union[int, float]] = [] 

73 

74 # Set the default start cell and offsets for the comment. 

75 self.set_offsets(self.row, self.col) 

76 

77 # Set any user supplied options. 

78 self._set_user_options(options) 

79 

80 def _set_user_options( 

81 self, options: Optional[Dict[str, Union[str, int, float]]] = None 

82 ) -> None: 

83 """ 

84 This method handles the additional optional parameters to 

85 ``write_comment()``. 

86 """ 

87 if options is None: 

88 return 

89 

90 # Overwrite the defaults with any user supplied values. Incorrect or 

91 # misspelled parameters are silently ignored. 

92 width = options.get("width") 

93 if width and isinstance(width, (int, float)): 

94 self.width = width 

95 

96 height = options.get("height") 

97 if height and isinstance(height, (int, float)): 

98 self.height = height 

99 

100 x_offset = options.get("x_offset") 

101 if x_offset and isinstance(x_offset, int): 

102 self.x_offset = x_offset 

103 

104 y_offset = options.get("y_offset") 

105 if y_offset and isinstance(y_offset, int): 

106 self.y_offset = y_offset 

107 

108 start_col = options.get("start_col") 

109 if start_col and isinstance(start_col, int): 

110 self.start_col = start_col 

111 

112 start_row = options.get("start_row") 

113 if start_row and isinstance(start_row, int): 

114 self.start_row = start_row 

115 

116 font_size = options.get("font_size") 

117 if font_size and isinstance(font_size, (int, float)): 

118 self.font_size = font_size 

119 

120 font_name = options.get("font_name") 

121 if font_name and isinstance(font_name, str): 

122 self.font_name = font_name 

123 

124 font_family = options.get("font_family") 

125 if font_family and isinstance(font_family, int): 

126 self.font_family = font_family 

127 

128 author = options.get("author") 

129 if author and isinstance(author, str): 

130 self.author = author 

131 

132 visible = options.get("visible") 

133 if visible is not None and isinstance(visible, bool): 

134 self.is_visible = visible 

135 

136 if options.get("color"): 

137 # Set the comment background color. 

138 self.color = Color._from_value(options["color"]) 

139 

140 # Convert a cell reference to a row and column. 

141 start_cell = options.get("start_cell") 

142 if start_cell and isinstance(start_cell, str): 

143 (start_row, start_col) = xl_cell_to_rowcol(start_cell) 

144 self.start_row = start_row 

145 self.start_col = start_col 

146 

147 # Scale the size of the comment box if required. 

148 x_scale = options.get("x_scale") 

149 if x_scale and isinstance(x_scale, (int, float)): 

150 self.width = self.width * x_scale 

151 

152 y_scale = options.get("y_scale") 

153 if y_scale and isinstance(y_scale, (int, float)): 

154 self.height = self.height * y_scale 

155 

156 # Round the dimensions to the nearest pixel. 

157 self.width = int(0.5 + self.width) 

158 self.height = int(0.5 + self.height) 

159 

160 def set_offsets(self, row: int, col: int) -> None: 

161 """ 

162 Set the default start cell and offsets for the comment. These are 

163 generally a fixed offset relative to the parent cell. However there are 

164 some edge cases for cells at the, well, edges. 

165 """ 

166 row_max = 1048576 

167 col_max = 16384 

168 

169 if self.row == 0: 

170 self.y_offset = 2 

171 self.start_row = 0 

172 elif self.row == row_max - 3: 

173 self.y_offset = 16 

174 self.start_row = row_max - 7 

175 elif self.row == row_max - 2: 

176 self.y_offset = 16 

177 self.start_row = row_max - 6 

178 elif self.row == row_max - 1: 

179 self.y_offset = 14 

180 self.start_row = row_max - 5 

181 else: 

182 self.y_offset = 10 

183 self.start_row = row - 1 

184 

185 if self.col == col_max - 3: 

186 self.x_offset = 49 

187 self.start_col = col_max - 6 

188 elif self.col == col_max - 2: 

189 self.x_offset = 49 

190 self.start_col = col_max - 5 

191 elif self.col == col_max - 1: 

192 self.x_offset = 49 

193 self.start_col = col_max - 4 

194 else: 

195 self.x_offset = 15 

196 self.start_col = col + 1 

197 

198 

199########################################################################### 

200# 

201# The file writer class for the Excel XLSX Comments file. 

202# 

203########################################################################### 

204class Comments(xmlwriter.XMLwriter): 

205 """ 

206 A class for writing the Excel XLSX Comments file. 

207 

208 

209 """ 

210 

211 ########################################################################### 

212 # 

213 # Public API. 

214 # 

215 ########################################################################### 

216 

217 def __init__(self) -> None: 

218 """ 

219 Constructor. 

220 

221 """ 

222 

223 super().__init__() 

224 self.author_ids = {} 

225 

226 ########################################################################### 

227 # 

228 # Private API. 

229 # 

230 ########################################################################### 

231 

232 def _assemble_xml_file( 

233 self, comments_data: Optional[List[CommentType]] = None 

234 ) -> None: 

235 # Assemble and write the XML file. 

236 

237 if comments_data is None: 

238 comments_data = [] 

239 

240 # Write the XML declaration. 

241 self._xml_declaration() 

242 

243 # Write the comments element. 

244 self._write_comments() 

245 

246 # Write the authors element. 

247 self._write_authors(comments_data) 

248 

249 # Write the commentList element. 

250 self._write_comment_list(comments_data) 

251 

252 self._xml_end_tag("comments") 

253 

254 # Close the file. 

255 self._xml_close() 

256 

257 ########################################################################### 

258 # 

259 # XML methods. 

260 # 

261 ########################################################################### 

262 

263 def _write_comments(self) -> None: 

264 # Write the <comments> element. 

265 xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" 

266 

267 attributes = [("xmlns", xmlns)] 

268 

269 self._xml_start_tag("comments", attributes) 

270 

271 def _write_authors(self, comment_data: List[CommentType]) -> None: 

272 # Write the <authors> element. 

273 author_count = 0 

274 

275 self._xml_start_tag("authors") 

276 

277 for comment in comment_data: 

278 author = comment.author 

279 

280 if author is not None and author not in self.author_ids: 

281 # Store the author id. 

282 self.author_ids[author] = author_count 

283 author_count += 1 

284 

285 # Write the author element. 

286 self._write_author(author) 

287 

288 self._xml_end_tag("authors") 

289 

290 def _write_author(self, data: str) -> None: 

291 # Write the <author> element. 

292 self._xml_data_element("author", data) 

293 

294 def _write_comment_list(self, comment_data: List[CommentType]) -> None: 

295 # Write the <commentList> element. 

296 self._xml_start_tag("commentList") 

297 

298 for comment in comment_data: 

299 # Look up the author id. 

300 author_id = -1 

301 if comment.author is not None: 

302 author_id = self.author_ids[comment.author] 

303 

304 # Write the comment element. 

305 self._write_comment(comment, author_id) 

306 

307 self._xml_end_tag("commentList") 

308 

309 def _write_comment(self, comment: CommentType, author_id: int) -> None: 

310 # Write the <comment> element. 

311 ref = xl_rowcol_to_cell(comment.row, comment.col) 

312 

313 attributes = [("ref", ref)] 

314 

315 if author_id != -1: 

316 attributes.append(("authorId", f"{author_id}")) 

317 

318 self._xml_start_tag("comment", attributes) 

319 

320 # Write the text element. 

321 self._write_text(comment) 

322 

323 self._xml_end_tag("comment") 

324 

325 def _write_text(self, comment: CommentType) -> None: 

326 # Write the <text> element. 

327 self._xml_start_tag("text") 

328 

329 # Write the text r element. 

330 self._write_text_r(comment) 

331 

332 self._xml_end_tag("text") 

333 

334 def _write_text_r(self, comment: CommentType) -> None: 

335 # Write the <r> element. 

336 self._xml_start_tag("r") 

337 

338 # Write the rPr element. 

339 self._write_r_pr(comment) 

340 

341 # Write the text r element. 

342 self._write_text_t(comment.text) 

343 

344 self._xml_end_tag("r") 

345 

346 def _write_text_t(self, text: str) -> None: 

347 # Write the text <t> element. 

348 attributes = [] 

349 

350 if _preserve_whitespace(text): 

351 attributes.append(("xml:space", "preserve")) 

352 

353 self._xml_data_element("t", text, attributes) 

354 

355 def _write_r_pr(self, comment: CommentType) -> None: 

356 # Write the <rPr> element. 

357 self._xml_start_tag("rPr") 

358 

359 # Write the sz element. 

360 self._write_sz(comment.font_size) 

361 

362 # Write the color element. 

363 self._write_color() 

364 

365 # Write the rFont element. 

366 self._write_r_font(comment.font_name) 

367 

368 # Write the family element. 

369 self._write_family(comment.font_family) 

370 

371 self._xml_end_tag("rPr") 

372 

373 def _write_sz(self, font_size: float) -> None: 

374 # Write the <sz> element. 

375 attributes = [("val", font_size)] 

376 

377 self._xml_empty_tag("sz", attributes) 

378 

379 def _write_color(self) -> None: 

380 # Write the <color> element. 

381 attributes = [("indexed", 81)] 

382 

383 self._xml_empty_tag("color", attributes) 

384 

385 def _write_r_font(self, font_name: str) -> None: 

386 # Write the <rFont> element. 

387 attributes = [("val", font_name)] 

388 

389 self._xml_empty_tag("rFont", attributes) 

390 

391 def _write_family(self, font_family: int) -> None: 

392 # Write the <family> element. 

393 attributes = [("val", font_family)] 

394 

395 self._xml_empty_tag("family", attributes)