Coverage for pymend\types.py: 94%

256 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-04-20 21:33 +0200

1"""Module for defining commonly used types.""" 

2 

3import ast 

4import re 

5import sys 

6from collections.abc import Iterable, Iterator 

7from dataclasses import dataclass, field 

8from typing import Optional, Union 

9 

10from typing_extensions import TypeAlias, override 

11 

12import pymend.docstring_parser as dsp 

13 

14from .const import DEFAULT_DESCRIPTION, DEFAULT_EXCEPTION, DEFAULT_SUMMARY, DEFAULT_TYPE 

15 

16__author__ = "J-E. Nitschke" 

17__copyright__ = "Copyright 2023-2024" 

18__licence__ = "GPL3" 

19__version__ = "1.0.0" 

20__maintainer__ = "J-E. Nitschke" 

21 

22 

23@dataclass(frozen=True) 

24class FixerSettings: 

25 """Settings to influence which sections are required and when.""" 

26 

27 force_params: bool = True 

28 force_return: bool = True 

29 force_raises: bool = True 

30 force_methods: bool = False 

31 force_attributes: bool = False 

32 force_params_min_n_params: int = 0 

33 force_meta_min_func_length: int = 0 

34 ignore_privates: bool = True 

35 ignore_unused_arguments: bool = True 

36 ignored_decorators: list[str] = field(default_factory=lambda: ["overload"]) 

37 ignored_functions: list[str] = field(default_factory=lambda: ["main"]) 

38 ignored_classes: list[str] = field(default_factory=list) 

39 force_defaults: bool = True 

40 

41 

42@dataclass 

43class DocstringInfo: 

44 """Wrapper around raw docstring.""" 

45 

46 name: str 

47 docstring: str 

48 lines: tuple[int, Optional[int]] 

49 modifier: str 

50 issues: list[str] 

51 

52 def output_docstring( 

53 self, 

54 *, 

55 settings: FixerSettings, 

56 output_style: dsp.DocstringStyle = dsp.DocstringStyle.NUMPYDOC, 

57 input_style: dsp.DocstringStyle = dsp.DocstringStyle.AUTO, 

58 ) -> str: 

59 """Parse and fix input docstrings, then compose output docstring. 

60 

61 Parameters 

62 ---------- 

63 settings : FixerSettings 

64 Settings for what to fix and when. 

65 output_style : dsp.DocstringStyle 

66 Output style to use for the docstring. 

67 (Default value = dsp.DocstringStyle.NUMPYDOC) 

68 input_style : dsp.DocstringStyle 

69 Input style to assume for the docstring. 

70 (Default value = dsp.DocstringStyle.AUTO) 

71 

72 Returns 

73 ------- 

74 str 

75 String representing the updated docstring. 

76 

77 Raises 

78 ------ 

79 AssertionError 

80 If the docstring could not be parsed. 

81 """ 

82 self._escape_triple_quotes() 

83 try: 

84 parsed = dsp.parse(self.docstring, style=input_style) 

85 except Exception as e: # noqa: BLE001 

86 msg = f"Failed to parse docstring for `{self.name}` with error: `{e}`" 

87 raise AssertionError(msg) from e 

88 self._fix_docstring(parsed, settings) 

89 self._fix_blank_lines(parsed) 

90 return dsp.compose(parsed, style=output_style) 

91 

92 def report_issues(self) -> tuple[int, str]: 

93 """Report all issues that were found in this docstring. 

94 

95 Returns 

96 ------- 

97 tuple[int, str] 

98 The number of issues found and a string representing a summary 

99 of those. 

100 """ 

101 if not self.issues: 

102 return 0, "" 

103 return len(self.issues), f"{'-'*50}\n{self.name}:\n" + "\n".join(self.issues) 

104 

105 def _escape_triple_quotes(self) -> None: 

106 r"""Escape \"\"\" in the docstring.""" 

107 if '"""' in self.docstring: 

108 self.issues.append("Unescaped triple quotes found.") 

109 self.docstring = self.docstring.replace('"""', r"\"\"\"") 

110 

111 def _fix_docstring( 

112 self, docstring: dsp.Docstring, _settings: FixerSettings 

113 ) -> None: 

114 """Fix docstrings. 

115 

116 Default are to add missing dots, blank lines and give defaults for 

117 descriptions and types. 

118 

119 Parameters 

120 ---------- 

121 docstring : dsp.Docstring 

122 Docstring to fix. 

123 settings : FixerSettings 

124 Settings for what to fix and when. 

125 """ 

126 self._fix_backslashes() 

127 self._fix_short_description(docstring) 

128 self._fix_descriptions(docstring) 

129 self._fix_types(docstring) 

130 

131 def _fix_backslashes(self) -> None: 

132 """If there is any backslash in the docstring set it as raw.""" 

133 if "\\" in self.docstring and "r" not in self.modifier: 

134 self.issues.append("Missing 'r' modifier.") 

135 self.modifier = "r" + self.modifier 

136 

137 def _fix_short_description(self, docstring: dsp.Docstring) -> None: 

138 """Set default summary. 

139 

140 Parameters 

141 ---------- 

142 docstring : dsp.Docstring 

143 Docstring to set the default summary for. 

144 """ 

145 cleaned_short_description = ( 

146 docstring.short_description.strip() if docstring.short_description else "" 

147 ) 

148 if ( 

149 not cleaned_short_description 

150 or cleaned_short_description == DEFAULT_SUMMARY 

151 ): 

152 self.issues.append("Missing short description.") 

153 docstring.short_description = cleaned_short_description or DEFAULT_SUMMARY 

154 if not docstring.short_description.endswith("."): 

155 self.issues.append("Short description missing '.' at the end.") 

156 docstring.short_description = f"{docstring.short_description.rstrip()}." 

157 

158 def _fix_blank_lines(self, docstring: dsp.Docstring) -> None: 

159 """Set blank lines after short and long description. 

160 

161 Parameters 

162 ---------- 

163 docstring : dsp.Docstring 

164 Docstring to fix the blank lines for. 

165 """ 

166 # For parsing a blank line is associated with the description. 

167 if ( 

168 docstring.blank_after_short_description 

169 != bool(docstring.long_description or docstring.meta) 

170 and self.docstring 

171 ): 

172 self.issues.append("Incorrect blank line after short description.") 

173 docstring.blank_after_short_description = bool( 

174 docstring.long_description or docstring.meta 

175 ) 

176 if docstring.long_description: 

177 if ( 

178 docstring.blank_after_long_description != bool(docstring.meta) 

179 and self.docstring 

180 ): 

181 self.issues.append("Incorrect blank line after long description.") 

182 docstring.blank_after_long_description = bool(docstring.meta) 

183 else: 

184 if docstring.blank_after_long_description and self.docstring: 184 ↛ 185line 184 didn't jump to line 185, because the condition on line 184 was never true

185 self.issues.append("Incorrect blank line after long description.") 

186 docstring.blank_after_long_description = False 

187 

188 def _fix_descriptions(self, docstring: dsp.Docstring) -> None: 

189 """Everything should have a description. 

190 

191 Parameters 

192 ---------- 

193 docstring : dsp.Docstring 

194 Docstring whose descriptions need fixing. 

195 """ 

196 for ele in docstring.meta: 

197 # Description works a bit different for examples. 

198 if isinstance(ele, dsp.DocstringExample): 198 ↛ 199line 198 didn't jump to line 199, because the condition on line 198 was never true

199 continue 

200 if not ele.description or ele.description == DEFAULT_DESCRIPTION: 

201 self.issues.append( 

202 f"{ele.args}: Missing or default description `{ele.description}`." 

203 ) 

204 ele.description = ele.description or DEFAULT_DESCRIPTION 

205 

206 def _fix_types(self, docstring: dsp.Docstring) -> None: 

207 """Set empty types for parameters and returns. 

208 

209 Parameters 

210 ---------- 

211 docstring : dsp.Docstring 

212 Docstring whose type information needs fixing 

213 """ 

214 for param in docstring.params: 

215 if param.args[0] == "method": 

216 continue 

217 if not param.type_name or param.type_name == DEFAULT_TYPE: 

218 self.issues.append(f"{param.arg_name}: Missing or default type name.") 

219 param.type_name = param.type_name or DEFAULT_TYPE 

220 for returned in docstring.many_returns: 

221 if not returned.type_name or returned.type_name == DEFAULT_TYPE: 

222 self.issues.append( 

223 "Missing or default type name for return value: " 

224 f" `{returned.return_name} |" 

225 f" {returned.type_name} |" 

226 f" {returned.description}`." 

227 ) 

228 returned.type_name = returned.type_name or DEFAULT_TYPE 

229 

230 

231@dataclass 

232class ModuleDocstring(DocstringInfo): 

233 """Information about a module.""" 

234 

235 

236@dataclass 

237class Parameter: 

238 """Info for parameter from signature.""" 

239 

240 arg_name: str 

241 type_name: Optional[str] = None 

242 default: Optional[str] = None 

243 

244 def custom_hash(self) -> int: 

245 """Implement custom has function for uniquefying. 

246 

247 Returns 

248 ------- 

249 int 

250 Hash value of the instance. 

251 """ 

252 return hash((self.arg_name, self.type_name, self.default)) 

253 

254 @staticmethod 

255 def uniquefy(lst: Iterable["Parameter"]) -> Iterator["Parameter"]: 

256 """Remove duplicates while keeping order. 

257 

258 Parameters 

259 ---------- 

260 lst : Iterable['Parameter'] 

261 Iterable of parameters that should be uniqueified. 

262 

263 Yields 

264 ------ 

265 'Parameter' 

266 Uniqueified parameters. 

267 """ 

268 seen: set[int] = set() 

269 for item in lst: 

270 if (itemhash := item.custom_hash()) not in seen: 

271 seen.add(itemhash) 

272 yield item 

273 

274 

275@dataclass 

276class ClassDocstring(DocstringInfo): 

277 """Information about a module.""" 

278 

279 attributes: list[Parameter] 

280 methods: list[str] 

281 

282 @override 

283 def _fix_docstring(self, docstring: dsp.Docstring, settings: FixerSettings) -> None: 

284 """Fix docstrings. 

285 

286 Additionally adjust attributes and methods from body. 

287 

288 Parameters 

289 ---------- 

290 docstring : dsp.Docstring 

291 Docstring to fix. 

292 settings : FixerSettings 

293 Settings for what to fix and when. 

294 """ 

295 super()._fix_docstring(docstring, settings) 

296 self._adjust_attributes(docstring, settings) 

297 self._adjust_methods(docstring, settings) 

298 

299 def _adjust_attributes( 

300 self, docstring: dsp.Docstring, settings: FixerSettings 

301 ) -> None: 

302 """Overwrite or create attribute docstring entries based on body. 

303 

304 Create the full list if there was no original docstring. 

305 

306 Do not add additional attributes and do not create the section 

307 if it did not exist. 

308 

309 Parameters 

310 ---------- 

311 docstring : dsp.Docstring 

312 Docstring to adjust parameters for. 

313 settings : FixerSettings 

314 Settings for what to fix and when. 

315 """ 

316 # If a docstring or the section already exists we are done. 

317 # We already fixed empty types and descriptions in the super call. 

318 if self.docstring and not settings.force_attributes: 

319 return 

320 # Build dicts for faster lookup 

321 atts_from_doc = { 

322 att.arg_name: att for att in docstring.params if att.args[0] == "attribute" 

323 } 

324 atts_from_sig = {att.arg_name: att for att in self.attributes} 

325 for name, att_sig in atts_from_sig.items(): 

326 # We already updated types and descriptions in the super call. 

327 if name in atts_from_doc: 327 ↛ 328line 327 didn't jump to line 328, because the condition on line 327 was never true

328 continue 

329 self.issues.append(f"Missing attribute `{att_sig.arg_name}`.") 

330 docstring.meta.append( 

331 dsp.DocstringParam( 

332 args=["attribute", att_sig.arg_name], 

333 description=DEFAULT_DESCRIPTION, 

334 arg_name=att_sig.arg_name, 

335 type_name=DEFAULT_TYPE, 

336 is_optional=False, 

337 default=None, 

338 ) 

339 ) 

340 

341 def _adjust_methods( 

342 self, docstring: dsp.Docstring, settings: FixerSettings 

343 ) -> None: 

344 """If a new docstring is generated add a methods section. 

345 

346 Create the full list if there was no original docstring. 

347 

348 Do not add additional methods and do not create the section 

349 if it did not exist. 

350 

351 Parameters 

352 ---------- 

353 docstring : dsp.Docstring 

354 Docstring to adjust methods for. 

355 settings : FixerSettings 

356 Settings for what to fix and when. 

357 """ 

358 if self.docstring and not settings.force_methods: 

359 return 

360 # Build dicts for faster lookup 

361 meth_from_doc = { 

362 meth.arg_name: meth for meth in docstring.params if meth.args[0] == "method" 

363 } 

364 for method in self.methods: 

365 # We already descriptions in the super call. 

366 if method in meth_from_doc: 366 ↛ 367line 366 didn't jump to line 367, because the condition on line 366 was never true

367 continue 

368 self.issues.append(f"Missing method `{method}`.") 

369 docstring.meta.append( 

370 dsp.DocstringParam( 

371 args=["method", method], 

372 description=DEFAULT_DESCRIPTION, 

373 arg_name=method, 

374 type_name=None, 

375 is_optional=False, 

376 default=None, 

377 ) 

378 ) 

379 

380 

381@dataclass 

382class ReturnValue: 

383 """Info about return value from signature.""" 

384 

385 type_name: Optional[str] = None 

386 

387 

388@dataclass 

389class FunctionSignature: 

390 """Information about a function signature.""" 

391 

392 params: list[Parameter] 

393 returns: ReturnValue 

394 

395 

396@dataclass 

397class FunctionBody: 

398 """Information about a function from its body.""" 

399 

400 raises: list[str] 

401 returns: set[tuple[str, ...]] 

402 returns_value: bool 

403 yields: set[tuple[str, ...]] 

404 yields_value: bool 

405 

406 

407@dataclass 

408class FunctionDocstring(DocstringInfo): 

409 """Information about a function from docstring.""" 

410 

411 signature: FunctionSignature 

412 body: FunctionBody 

413 length: int 

414 

415 @override 

416 def _fix_docstring(self, docstring: dsp.Docstring, settings: FixerSettings) -> None: 

417 """Fix docstrings. 

418 

419 Additionally adjust: 

420 parameters from function signature. 

421 return and yield from signature and body. 

422 raises from body. 

423 

424 Parameters 

425 ---------- 

426 docstring : dsp.Docstring 

427 Docstring to fix. 

428 settings : FixerSettings 

429 Settings for what to fix and when. 

430 """ 

431 super()._fix_docstring(docstring, settings) 

432 self._adjust_parameters(docstring, settings) 

433 self._adjust_returns(docstring, settings) 

434 self._adjust_yields(docstring, settings) 

435 self._adjust_raises(docstring, settings) 

436 

437 def _escape_default_value(self, default_value: str) -> str: 

438 r"""Escape the default value so that the docstring remains fully valid. 

439 

440 Currently only escapes triple quotes '\"\"\"'. 

441 

442 Parameters 

443 ---------- 

444 default_value : str 

445 Value to escape. 

446 

447 Returns 

448 ------- 

449 str 

450 Optionally escaped value. 

451 """ 

452 if '"""' in default_value: 

453 if "r" not in self.modifier: 453 ↛ 455line 453 didn't jump to line 455, because the condition on line 453 was never false

454 self.modifier = "r" + self.modifier 

455 return default_value.replace('"""', r"\"\"\"") 

456 return default_value 

457 

458 def _adjust_parameters( 

459 self, docstring: dsp.Docstring, settings: FixerSettings 

460 ) -> None: 

461 """Overwrite or create param docstring entries based on signature. 

462 

463 If an entry already exists update the type description if one exists 

464 in the signature. Same for the default value. 

465 

466 If no entry exists then create one with name, type and default from the 

467 signature and place holder description. 

468 

469 Parameters 

470 ---------- 

471 docstring : dsp.Docstring 

472 Docstring to adjust parameters for. 

473 settings : FixerSettings 

474 Settings for what to fix and when. 

475 """ 

476 # Build dicts for faster lookup 

477 params_from_doc = {param.arg_name: param for param in docstring.params} 

478 params_from_sig = {param.arg_name: param for param in self.signature.params} 

479 for name, param_sig in params_from_sig.items(): 

480 if name in params_from_doc: 

481 param_doc = params_from_doc[name] 

482 if param_sig.type_name and param_sig.type_name != param_doc.type_name: 

483 self.issues.append( 

484 f"{name}: Parameter type was" 

485 f" `{param_doc.type_name} `but signature" 

486 f" has type hint `{param_sig.type_name}`." 

487 ) 

488 param_doc.type_name = param_sig.type_name or param_doc.type_name 

489 param_doc.is_optional = False 

490 if param_sig.default: 

491 param_doc.default = param_sig.default 

492 # param_doc.description should never be None at this point 

493 # as it should have already been set by '_fix_descriptions' 

494 if ( 

495 param_doc.description is not None 

496 and "default" not in param_doc.description.lower() 

497 and settings.force_defaults 

498 ): 

499 self.issues.append( 

500 f"{name}: Missing description of default value." 

501 ) 

502 param_doc.description += ( 

503 f" (Default value = " 

504 f"{self._escape_default_value(param_sig.default)})" 

505 ) 

506 elif ( 

507 settings.force_params 

508 and len(params_from_doc) >= settings.force_params_min_n_params 

509 and self.length >= settings.force_meta_min_func_length 

510 or not self.docstring 

511 ): 

512 self.issues.append(f"Missing parameter `{name}`.") 

513 place_holder_description = DEFAULT_DESCRIPTION 

514 if param_sig.default: 

515 place_holder_description += ( 

516 f" (Default value = " 

517 f"{self._escape_default_value(param_sig.default)})" 

518 ) 

519 docstring.meta.append( 

520 dsp.DocstringParam( 

521 args=["param", name], 

522 description=place_holder_description, 

523 arg_name=name, 

524 type_name=param_sig.type_name or DEFAULT_TYPE, 

525 is_optional=False, 

526 default=param_sig.default, 

527 ) 

528 ) 

529 

530 def _adjust_returns( 

531 self, docstring: dsp.Docstring, settings: FixerSettings 

532 ) -> None: 

533 """Overwrite or create return docstring entries based on signature. 

534 

535 If no return value was parsed from the docstring: 

536 Add one based on the signature with a dummy description except 

537 if the return type was not specified or specified to be None AND there 

538 was an existing docstring. 

539 

540 If one return value is specified overwrite the type with the signature 

541 if one was present there. 

542 

543 If multiple were specified then leave them as is. 

544 They might very well be expanding on a return type like: 

545 Tuple[int, str, whatever] 

546 

547 Parameters 

548 ---------- 

549 docstring : dsp.Docstring 

550 Docstring to adjust return values for. 

551 settings : FixerSettings 

552 Settings for what to fix and when. 

553 """ 

554 doc_returns = docstring.many_returns 

555 sig_return = self.signature.returns.type_name 

556 # If the return type is a generator extract the actual return type from that. 

557 if sig_return and ( 

558 matches := (re.match(r"Generator\[(\w+), (\w+), (\w+)\]", sig_return)) 

559 ): 

560 sig_return = matches[3] 

561 # If only one return value is specified take the type from the signature 

562 # as that is more likely to be correct 

563 if ( 

564 not doc_returns 

565 and self.body.returns_value 

566 # If we do not want to force returns then only add new ones if 

567 # there was no docstring at all. 

568 and ( 

569 settings.force_return 

570 and self.length >= settings.force_meta_min_func_length 

571 or not self.docstring 

572 ) 

573 ): 

574 self.issues.append("Missing return value.") 

575 docstring.meta.append( 

576 dsp.DocstringReturns( 

577 args=["returns"], 

578 description=DEFAULT_DESCRIPTION, 

579 type_name=sig_return or DEFAULT_TYPE, 

580 is_generator=False, 

581 return_name=None, 

582 ) 

583 ) 

584 # If there is only one return value specified and we do not 

585 # yield anything then correct it with the actual return value. 

586 elif len(doc_returns) == 1 and not self.body.yields_value: 

587 doc_return = doc_returns[0] 

588 if sig_return and doc_return.type_name != sig_return: 

589 self.issues.append( 

590 f"Return type was `{doc_return.type_name}` but" 

591 f" signature has type hint `{sig_return}`." 

592 ) 

593 doc_return.type_name = sig_return or doc_return.type_name 

594 # If we have multiple return values specified 

595 # and we have only extracted one set of return values from the body. 

596 # then update the multiple return values with the names from 

597 # the actual return values. 

598 elif len(doc_returns) > 1 and len(self.body.returns) == 1: 

599 doc_names = {returned.return_name for returned in doc_returns} 

600 for body_name in next(iter(self.body.returns)): 

601 if body_name not in doc_names: 

602 self.issues.append( 

603 f"Missing return value in multi return statement `{body_name}`." 

604 ) 

605 docstring.meta.append( 

606 dsp.DocstringReturns( 

607 args=["returns"], 

608 description=DEFAULT_DESCRIPTION, 

609 type_name=DEFAULT_TYPE, 

610 is_generator=False, 

611 return_name=body_name, 

612 ) 

613 ) 

614 

615 def _adjust_yields(self, docstring: dsp.Docstring, settings: FixerSettings) -> None: 

616 """See _adjust_returns. 

617 

618 Only difference is that the signature return type is not added 

619 to the docstring since it is a bit more complicated for generators. 

620 

621 Parameters 

622 ---------- 

623 docstring : dsp.Docstring 

624 Docstring to adjust yields for. 

625 settings : FixerSettings 

626 Settings for what to fix and when. 

627 """ 

628 doc_yields = docstring.many_yields 

629 sig_return = self.signature.returns.type_name 

630 # Extract actual return type from Iterators and Generators. 

631 if sig_return and ( 

632 matches := ( 

633 re.match(r"(?:Iterable|Iterator)\[([^\]]+)\]", sig_return) 

634 or re.match(r"Generator\[(\w+), (\w+), (\w+)\]", sig_return) 

635 ) 

636 ): 

637 sig_return = matches[1] 

638 else: 

639 sig_return = None 

640 # If only one return value is specified take the type from the signature 

641 # as that is more likely to be correct 

642 if not doc_yields and self.body.yields_value and settings.force_return: 

643 self.issues.append("Missing yielded value.") 

644 docstring.meta.append( 

645 dsp.DocstringYields( 

646 args=["yields"], 

647 description=DEFAULT_DESCRIPTION, 

648 type_name=sig_return or DEFAULT_TYPE, 

649 is_generator=True, 

650 yield_name=None, 

651 ) 

652 ) 

653 elif len(doc_yields) == 1: 

654 doc_yields = doc_yields[0] 

655 if sig_return and doc_yields.type_name != sig_return: 

656 self.issues.append( 

657 f"Yield type was `{doc_yields.type_name}` but" 

658 f" signature has type hint `{sig_return}`." 

659 ) 

660 doc_yields.type_name = sig_return or doc_yields.type_name 

661 elif len(doc_yields) > 1 and len(self.body.yields) == 1: 

662 doc_names = {yielded.yield_name for yielded in doc_yields} 

663 for body_name in next(iter(self.body.yields)): 

664 if body_name not in doc_names: 

665 self.issues.append( 

666 f"Missing yielded value in multi yield statement `{body_name}`." 

667 ) 

668 docstring.meta.append( 

669 dsp.DocstringYields( 

670 args=["yields"], 

671 description=DEFAULT_DESCRIPTION, 

672 type_name=DEFAULT_TYPE, 

673 is_generator=True, 

674 yield_name=body_name, 

675 ) 

676 ) 

677 

678 def _adjust_raises(self, docstring: dsp.Docstring, settings: FixerSettings) -> None: 

679 """Adjust raises section based on parsed body. 

680 

681 Parameters 

682 ---------- 

683 docstring : dsp.Docstring 

684 Docstring to adjust raises section for. 

685 settings : FixerSettings 

686 Settings for what to fix and when. 

687 """ 

688 if self.docstring and not settings.force_raises: 688 ↛ 689line 688 didn't jump to line 689, because the condition on line 688 was never true

689 return 

690 # Only consider those raises that are not already raised in the body. 

691 # We are potentially raising the same type of exception multiple times. 

692 # Only remove the first of each type per one encountered in the docstring.. 

693 raised_in_body = self.body.raises.copy() 

694 # Sort the raised assertionts so that `DEFAULT_EXCEPTION` are at the beginning. 

695 # This ensures that these are removed first before we start removing 

696 # them through more specific exceptions 

697 for raised in sorted( 

698 docstring.raises, 

699 key=lambda x: x.type_name == DEFAULT_EXCEPTION, 

700 reverse=True, 

701 ): 

702 if raised.type_name in raised_in_body: 

703 raised_in_body.remove(raised.type_name) 

704 # If this specific Error is not in the body but the body contains 

705 # unknown exceptions then remove one of those instead. 

706 # For example when exception stored in variable and raised later. 

707 # We want people to be able to specific them by name and not have 

708 # pymend constantly force unnamed raises on them. 

709 elif DEFAULT_EXCEPTION in raised_in_body: 

710 raised_in_body.remove(DEFAULT_EXCEPTION) 

711 for missing_raise in raised_in_body: 

712 self.issues.append(f"Missing raised exception `{missing_raise}`.") 

713 docstring.meta.append( 

714 dsp.DocstringRaises( 

715 args=["raises", missing_raise], 

716 description=DEFAULT_DESCRIPTION, 

717 type_name=missing_raise, 

718 ) 

719 ) 

720 

721 

722ElementDocstring: TypeAlias = Union[ModuleDocstring, ClassDocstring, FunctionDocstring] 

723DefinitionNodes: TypeAlias = Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef] 

724NodeOfInterest: TypeAlias = Union[DefinitionNodes, ast.Module] 

725# pylint: disable=no-member 

726# Match and try star supported 

727if sys.version_info >= (3, 11): 727 ↛ 746line 727 didn't jump to line 746, because the condition on line 727 was never false

728 BodyTypes: TypeAlias = Union[ 

729 ast.Module, 

730 ast.Interactive, 

731 ast.FunctionDef, 

732 ast.AsyncFunctionDef, 

733 ast.ClassDef, 

734 ast.For, 

735 ast.AsyncFor, 

736 ast.While, 

737 ast.If, 

738 ast.With, 

739 ast.AsyncWith, 

740 ast.Try, 

741 ast.ExceptHandler, 

742 ast.match_case, 

743 ast.TryStar, 

744 ] 

745# Only match, no trystar 

746elif sys.version_info == (3, 10): 

747 BodyTypes: TypeAlias = Union[ 

748 ast.Module, 

749 ast.Interactive, 

750 ast.FunctionDef, 

751 ast.AsyncFunctionDef, 

752 ast.ClassDef, 

753 ast.For, 

754 ast.AsyncFor, 

755 ast.While, 

756 ast.If, 

757 ast.With, 

758 ast.AsyncWith, 

759 ast.Try, 

760 ast.ExceptHandler, 

761 ast.match_case, 

762 ] 

763# Neither match nor trystar 

764else: 

765 BodyTypes: TypeAlias = Union[ 

766 ast.Module, 

767 ast.Interactive, 

768 ast.FunctionDef, 

769 ast.AsyncFunctionDef, 

770 ast.ClassDef, 

771 ast.For, 

772 ast.AsyncFor, 

773 ast.While, 

774 ast.If, 

775 ast.With, 

776 ast.AsyncWith, 

777 ast.Try, 

778 ast.ExceptHandler, 

779 ]