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.color import Color 

13 

14from . import xmlwriter 

15from .utility import _preserve_whitespace, xl_cell_to_rowcol, xl_rowcol_to_cell 

16 

17 

18########################################################################### 

19# 

20# A comment type class. 

21# 

22########################################################################### 

23class CommentType: 

24 """ 

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

26 

27 """ 

28 

29 def __init__( 

30 self, 

31 row: int, 

32 col: int, 

33 text: str, 

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

35 ) -> None: 

36 """ 

37 Initialize a Comment instance. 

38 

39 Args: 

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

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

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

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

44 """ 

45 self.row: int = row 

46 self.col: int = col 

47 self.text: str = text 

48 

49 self.author: Optional[str] = None 

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

51 

52 self.start_row: int = 0 

53 self.start_col: int = 0 

54 

55 self.is_visible: Optional[bool] = None 

56 

57 self.width: float = 128 

58 self.height: float = 74 

59 

60 self.x_scale: float = 1 

61 self.y_scale: float = 1 

62 self.x_offset: int = 0 

63 self.y_offset: int = 0 

64 

65 self.font_size: float = 8 

66 self.font_name: str = "Tahoma" 

67 self.font_family: int = 2 

68 

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

70 

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

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

73 

74 # Set any user supplied options. 

75 self._set_user_options(options) 

76 

77 def _set_user_options( 

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

79 ) -> None: 

80 """ 

81 This method handles the additional optional parameters to 

82 ``write_comment()``. 

83 """ 

84 if options is None: 

85 return 

86 

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

88 # misspelled parameters are silently ignored. 

89 width = options.get("width") 

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

91 self.width = width 

92 

93 height = options.get("height") 

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

95 self.height = height 

96 

97 x_offset = options.get("x_offset") 

98 if x_offset and isinstance(x_offset, int): 

99 self.x_offset = x_offset 

100 

101 y_offset = options.get("y_offset") 

102 if y_offset and isinstance(y_offset, int): 

103 self.y_offset = y_offset 

104 

105 start_col = options.get("start_col") 

106 if start_col and isinstance(start_col, int): 

107 self.start_col = start_col 

108 

109 start_row = options.get("start_row") 

110 if start_row and isinstance(start_row, int): 

111 self.start_row = start_row 

112 

113 font_size = options.get("font_size") 

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

115 self.font_size = font_size 

116 

117 font_name = options.get("font_name") 

118 if font_name and isinstance(font_name, str): 

119 self.font_name = font_name 

120 

121 font_family = options.get("font_family") 

122 if font_family and isinstance(font_family, int): 

123 self.font_family = font_family 

124 

125 author = options.get("author") 

126 if author and isinstance(author, str): 

127 self.author = author 

128 

129 visible = options.get("visible") 

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

131 self.is_visible = visible 

132 

133 if options.get("color"): 

134 # Set the comment background color. 

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

136 

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

138 start_cell = options.get("start_cell") 

139 if start_cell and isinstance(start_cell, str): 

140 (start_row, start_col) = xl_cell_to_rowcol(start_cell) 

141 self.start_row = start_row 

142 self.start_col = start_col 

143 

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

145 x_scale = options.get("x_scale") 

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

147 self.width = self.width * x_scale 

148 

149 y_scale = options.get("y_scale") 

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

151 self.height = self.height * y_scale 

152 

153 # Round the dimensions to the nearest pixel. 

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

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

156 

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

158 """ 

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

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

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

162 """ 

163 row_max = 1048576 

164 col_max = 16384 

165 

166 if self.row == 0: 

167 self.y_offset = 2 

168 self.start_row = 0 

169 elif self.row == row_max - 3: 

170 self.y_offset = 16 

171 self.start_row = row_max - 7 

172 elif self.row == row_max - 2: 

173 self.y_offset = 16 

174 self.start_row = row_max - 6 

175 elif self.row == row_max - 1: 

176 self.y_offset = 14 

177 self.start_row = row_max - 5 

178 else: 

179 self.y_offset = 10 

180 self.start_row = row - 1 

181 

182 if self.col == col_max - 3: 

183 self.x_offset = 49 

184 self.start_col = col_max - 6 

185 elif self.col == col_max - 2: 

186 self.x_offset = 49 

187 self.start_col = col_max - 5 

188 elif self.col == col_max - 1: 

189 self.x_offset = 49 

190 self.start_col = col_max - 4 

191 else: 

192 self.x_offset = 15 

193 self.start_col = col + 1 

194 

195 

196########################################################################### 

197# 

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

199# 

200########################################################################### 

201class Comments(xmlwriter.XMLwriter): 

202 """ 

203 A class for writing the Excel XLSX Comments file. 

204 

205 

206 """ 

207 

208 ########################################################################### 

209 # 

210 # Public API. 

211 # 

212 ########################################################################### 

213 

214 def __init__(self) -> None: 

215 """ 

216 Constructor. 

217 

218 """ 

219 

220 super().__init__() 

221 self.author_ids = {} 

222 

223 ########################################################################### 

224 # 

225 # Private API. 

226 # 

227 ########################################################################### 

228 

229 def _assemble_xml_file( 

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

231 ) -> None: 

232 # Assemble and write the XML file. 

233 

234 if comments_data is None: 

235 comments_data = [] 

236 

237 # Write the XML declaration. 

238 self._xml_declaration() 

239 

240 # Write the comments element. 

241 self._write_comments() 

242 

243 # Write the authors element. 

244 self._write_authors(comments_data) 

245 

246 # Write the commentList element. 

247 self._write_comment_list(comments_data) 

248 

249 self._xml_end_tag("comments") 

250 

251 # Close the file. 

252 self._xml_close() 

253 

254 ########################################################################### 

255 # 

256 # XML methods. 

257 # 

258 ########################################################################### 

259 

260 def _write_comments(self) -> None: 

261 # Write the <comments> element. 

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

263 

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

265 

266 self._xml_start_tag("comments", attributes) 

267 

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

269 # Write the <authors> element. 

270 author_count = 0 

271 

272 self._xml_start_tag("authors") 

273 

274 for comment in comment_data: 

275 author = comment.author 

276 

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

278 # Store the author id. 

279 self.author_ids[author] = author_count 

280 author_count += 1 

281 

282 # Write the author element. 

283 self._write_author(author) 

284 

285 self._xml_end_tag("authors") 

286 

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

288 # Write the <author> element. 

289 self._xml_data_element("author", data) 

290 

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

292 # Write the <commentList> element. 

293 self._xml_start_tag("commentList") 

294 

295 for comment in comment_data: 

296 # Look up the author id. 

297 author_id = -1 

298 if comment.author is not None: 

299 author_id = self.author_ids[comment.author] 

300 

301 # Write the comment element. 

302 self._write_comment(comment, author_id) 

303 

304 self._xml_end_tag("commentList") 

305 

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

307 # Write the <comment> element. 

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

309 

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

311 

312 if author_id != -1: 

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

314 

315 self._xml_start_tag("comment", attributes) 

316 

317 # Write the text element. 

318 self._write_text(comment) 

319 

320 self._xml_end_tag("comment") 

321 

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

323 # Write the <text> element. 

324 self._xml_start_tag("text") 

325 

326 # Write the text r element. 

327 self._write_text_r(comment) 

328 

329 self._xml_end_tag("text") 

330 

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

332 # Write the <r> element. 

333 self._xml_start_tag("r") 

334 

335 # Write the rPr element. 

336 self._write_r_pr(comment) 

337 

338 # Write the text r element. 

339 self._write_text_t(comment.text) 

340 

341 self._xml_end_tag("r") 

342 

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

344 # Write the text <t> element. 

345 attributes = [] 

346 

347 if _preserve_whitespace(text): 

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

349 

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

351 

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

353 # Write the <rPr> element. 

354 self._xml_start_tag("rPr") 

355 

356 # Write the sz element. 

357 self._write_sz(comment.font_size) 

358 

359 # Write the color element. 

360 self._write_color() 

361 

362 # Write the rFont element. 

363 self._write_r_font(comment.font_name) 

364 

365 # Write the family element. 

366 self._write_family(comment.font_family) 

367 

368 self._xml_end_tag("rPr") 

369 

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

371 # Write the <sz> element. 

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

373 

374 self._xml_empty_tag("sz", attributes) 

375 

376 def _write_color(self) -> None: 

377 # Write the <color> element. 

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

379 

380 self._xml_empty_tag("color", attributes) 

381 

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

383 # Write the <rFont> element. 

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

385 

386 self._xml_empty_tag("rFont", attributes) 

387 

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

389 # Write the <family> element. 

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

391 

392 self._xml_empty_tag("family", attributes)