Coverage for pymend\types.py: 94%

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

2

3 import ast

4 import re

5 import sys

6 from collections.abc import Iterable, Iterator

7 from dataclasses import dataclass, field

8 from typing import Optional, Union

9

10 from typing_extensions import TypeAlias, override

11

12 import pymend.docstring_parser as dsp

13

14 from .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)

24 class 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

43 class 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

232 class ModuleDocstring(DocstringInfo):

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

234

235

236@dataclass

237 class 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

276 class 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

382 class ReturnValue:

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

384

385 type_name: Optional[str] = None

386

387

388@dataclass

389 class FunctionSignature:

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

391

392 params: list[Parameter]

393 returns: ReturnValue

394

395

396@dataclass

397 class 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

408 class 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

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

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

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

725# pylint: disable=no-member

726# Match and try star supported

727 if 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

746 elif 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

764 else:

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 ]