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 ): 

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 ): 

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): 

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): 

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(self, comments_data: Optional[List[CommentType]] = None): 

230 # Assemble and write the XML file. 

231 

232 if comments_data is None: 

233 comments_data = [] 

234 

235 # Write the XML declaration. 

236 self._xml_declaration() 

237 

238 # Write the comments element. 

239 self._write_comments() 

240 

241 # Write the authors element. 

242 self._write_authors(comments_data) 

243 

244 # Write the commentList element. 

245 self._write_comment_list(comments_data) 

246 

247 self._xml_end_tag("comments") 

248 

249 # Close the file. 

250 self._xml_close() 

251 

252 ########################################################################### 

253 # 

254 # XML methods. 

255 # 

256 ########################################################################### 

257 

258 def _write_comments(self): 

259 # Write the <comments> element. 

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

261 

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

263 

264 self._xml_start_tag("comments", attributes) 

265 

266 def _write_authors(self, comment_data: List[CommentType]): 

267 # Write the <authors> element. 

268 author_count = 0 

269 

270 self._xml_start_tag("authors") 

271 

272 for comment in comment_data: 

273 author = comment.author 

274 

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

276 # Store the author id. 

277 self.author_ids[author] = author_count 

278 author_count += 1 

279 

280 # Write the author element. 

281 self._write_author(author) 

282 

283 self._xml_end_tag("authors") 

284 

285 def _write_author(self, data: str): 

286 # Write the <author> element. 

287 self._xml_data_element("author", data) 

288 

289 def _write_comment_list(self, comment_data: List[CommentType]): 

290 # Write the <commentList> element. 

291 self._xml_start_tag("commentList") 

292 

293 for comment in comment_data: 

294 # Look up the author id. 

295 author_id = -1 

296 if comment.author is not None: 

297 author_id = self.author_ids[comment.author] 

298 

299 # Write the comment element. 

300 self._write_comment(comment, author_id) 

301 

302 self._xml_end_tag("commentList") 

303 

304 def _write_comment(self, comment: CommentType, author_id: int): 

305 # Write the <comment> element. 

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

307 

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

309 

310 if author_id != -1: 

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

312 

313 self._xml_start_tag("comment", attributes) 

314 

315 # Write the text element. 

316 self._write_text(comment) 

317 

318 self._xml_end_tag("comment") 

319 

320 def _write_text(self, comment: CommentType): 

321 # Write the <text> element. 

322 self._xml_start_tag("text") 

323 

324 # Write the text r element. 

325 self._write_text_r(comment) 

326 

327 self._xml_end_tag("text") 

328 

329 def _write_text_r(self, comment: CommentType): 

330 # Write the <r> element. 

331 self._xml_start_tag("r") 

332 

333 # Write the rPr element. 

334 self._write_r_pr(comment) 

335 

336 # Write the text r element. 

337 self._write_text_t(comment.text) 

338 

339 self._xml_end_tag("r") 

340 

341 def _write_text_t(self, text: str): 

342 # Write the text <t> element. 

343 attributes = [] 

344 

345 if _preserve_whitespace(text): 

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

347 

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

349 

350 def _write_r_pr(self, comment: CommentType): 

351 # Write the <rPr> element. 

352 self._xml_start_tag("rPr") 

353 

354 # Write the sz element. 

355 self._write_sz(comment.font_size) 

356 

357 # Write the color element. 

358 self._write_color() 

359 

360 # Write the rFont element. 

361 self._write_r_font(comment.font_name) 

362 

363 # Write the family element. 

364 self._write_family(comment.font_family) 

365 

366 self._xml_end_tag("rPr") 

367 

368 def _write_sz(self, font_size: float): 

369 # Write the <sz> element. 

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

371 

372 self._xml_empty_tag("sz", attributes) 

373 

374 def _write_color(self): 

375 # Write the <color> element. 

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

377 

378 self._xml_empty_tag("color", attributes) 

379 

380 def _write_r_font(self, font_name: str): 

381 # Write the <rFont> element. 

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

383 

384 self._xml_empty_tag("rFont", attributes) 

385 

386 def _write_family(self, font_family: int): 

387 # Write the <family> element. 

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

389 

390 self._xml_empty_tag("family", attributes)