Coverage for pymend\types.py: 94%
256 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-04-20 21:33 +0200
« prev ^ index » next coverage.py v7.3.2, created at 2024-04-20 21:33 +0200
1"""Module for defining commonly used types."""
3import ast
4import re
5import sys
6from collections.abc import Iterable, Iterator
7from dataclasses import dataclass, field
8from typing import Optional, Union
10from typing_extensions import TypeAlias, override
12import pymend.docstring_parser as dsp
14from .const import DEFAULT_DESCRIPTION, DEFAULT_EXCEPTION, DEFAULT_SUMMARY, DEFAULT_TYPE
16__author__ = "J-E. Nitschke"
17__copyright__ = "Copyright 2023-2024"
18__licence__ = "GPL3"
19__version__ = "1.0.0"
20__maintainer__ = "J-E. Nitschke"
23@dataclass(frozen=True)
24class FixerSettings:
25 """Settings to influence which sections are required and when."""
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
42@dataclass
43class DocstringInfo:
44 """Wrapper around raw docstring."""
46 name: str
47 docstring: str
48 lines: tuple[int, Optional[int]]
49 modifier: str
50 issues: list[str]
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.
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)
72 Returns
73 -------
74 str
75 String representing the updated docstring.
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)
92 def report_issues(self) -> tuple[int, str]:
93 """Report all issues that were found in this docstring.
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)
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"\"\"\"")
111 def _fix_docstring(
112 self, docstring: dsp.Docstring, _settings: FixerSettings
113 ) -> None:
114 """Fix docstrings.
116 Default are to add missing dots, blank lines and give defaults for
117 descriptions and types.
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)
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
137 def _fix_short_description(self, docstring: dsp.Docstring) -> None:
138 """Set default summary.
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()}."
158 def _fix_blank_lines(self, docstring: dsp.Docstring) -> None:
159 """Set blank lines after short and long description.
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
188 def _fix_descriptions(self, docstring: dsp.Docstring) -> None:
189 """Everything should have a description.
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
206 def _fix_types(self, docstring: dsp.Docstring) -> None:
207 """Set empty types for parameters and returns.
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
231@dataclass
232class ModuleDocstring(DocstringInfo):
233 """Information about a module."""
236@dataclass
237class Parameter:
238 """Info for parameter from signature."""
240 arg_name: str
241 type_name: Optional[str] = None
242 default: Optional[str] = None
244 def custom_hash(self) -> int:
245 """Implement custom has function for uniquefying.
247 Returns
248 -------
249 int
250 Hash value of the instance.
251 """
252 return hash((self.arg_name, self.type_name, self.default))
254 @staticmethod
255 def uniquefy(lst: Iterable["Parameter"]) -> Iterator["Parameter"]:
256 """Remove duplicates while keeping order.
258 Parameters
259 ----------
260 lst : Iterable['Parameter']
261 Iterable of parameters that should be uniqueified.
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
275@dataclass
276class ClassDocstring(DocstringInfo):
277 """Information about a module."""
279 attributes: list[Parameter]
280 methods: list[str]
282 @override
283 def _fix_docstring(self, docstring: dsp.Docstring, settings: FixerSettings) -> None:
284 """Fix docstrings.
286 Additionally adjust attributes and methods from body.
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)
299 def _adjust_attributes(
300 self, docstring: dsp.Docstring, settings: FixerSettings
301 ) -> None:
302 """Overwrite or create attribute docstring entries based on body.
304 Create the full list if there was no original docstring.
306 Do not add additional attributes and do not create the section
307 if it did not exist.
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 )
341 def _adjust_methods(
342 self, docstring: dsp.Docstring, settings: FixerSettings
343 ) -> None:
344 """If a new docstring is generated add a methods section.
346 Create the full list if there was no original docstring.
348 Do not add additional methods and do not create the section
349 if it did not exist.
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 )
381@dataclass
382class ReturnValue:
383 """Info about return value from signature."""
385 type_name: Optional[str] = None
388@dataclass
389class FunctionSignature:
390 """Information about a function signature."""
392 params: list[Parameter]
393 returns: ReturnValue
396@dataclass
397class FunctionBody:
398 """Information about a function from its body."""
400 raises: list[str]
401 returns: set[tuple[str, ...]]
402 returns_value: bool
403 yields: set[tuple[str, ...]]
404 yields_value: bool
407@dataclass
408class FunctionDocstring(DocstringInfo):
409 """Information about a function from docstring."""
411 signature: FunctionSignature
412 body: FunctionBody
413 length: int
415 @override
416 def _fix_docstring(self, docstring: dsp.Docstring, settings: FixerSettings) -> None:
417 """Fix docstrings.
419 Additionally adjust:
420 parameters from function signature.
421 return and yield from signature and body.
422 raises from body.
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)
437 def _escape_default_value(self, default_value: str) -> str:
438 r"""Escape the default value so that the docstring remains fully valid.
440 Currently only escapes triple quotes '\"\"\"'.
442 Parameters
443 ----------
444 default_value : str
445 Value to escape.
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
458 def _adjust_parameters(
459 self, docstring: dsp.Docstring, settings: FixerSettings
460 ) -> None:
461 """Overwrite or create param docstring entries based on signature.
463 If an entry already exists update the type description if one exists
464 in the signature. Same for the default value.
466 If no entry exists then create one with name, type and default from the
467 signature and place holder description.
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 )
530 def _adjust_returns(
531 self, docstring: dsp.Docstring, settings: FixerSettings
532 ) -> None:
533 """Overwrite or create return docstring entries based on signature.
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.
540 If one return value is specified overwrite the type with the signature
541 if one was present there.
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]
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 )
615 def _adjust_yields(self, docstring: dsp.Docstring, settings: FixerSettings) -> None:
616 """See _adjust_returns.
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.
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 )
678 def _adjust_raises(self, docstring: dsp.Docstring, settings: FixerSettings) -> None:
679 """Adjust raises section based on parsed body.
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 )
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 ]