@@ -337,6 +337,22 @@ def current_source(self, value):
337337 if self .types_db is not None :
338338 self .types_db .current_source = value
339339
340+ @property
341+ def is_inside_function_def (self ):
342+ """Check whether the current scope is within a function.
343+
344+ Returns
345+ -------
346+ out : bool
347+ """
348+ inside_function_def = self ._scope_stack [- 1 ].type in (
349+ ScopeType .FUNC ,
350+ ScopeType .METHOD ,
351+ ScopeType .CLASSMETHOD ,
352+ ScopeType .STATICMETHOD ,
353+ )
354+ return inside_function_def
355+
340356 def python_to_stub (self , source , * , module_path = None ):
341357 """Convert Python source code to stub-file ready code.
342358
@@ -386,7 +402,10 @@ def visit_ClassDef(self, node):
386402 return True
387403
388404 def leave_ClassDef (self , original_node , updated_node ):
389- """Drop class scope from the stack.
405+ """Finalize class definition.
406+
407+ If the docstring documents attributes, make sure to insert them as (instance)
408+ attributes for the class. Also drop class scope from the stack.
390409
391410 Parameters
392411 ----------
@@ -397,6 +416,11 @@ def leave_ClassDef(self, original_node, updated_node):
397416 -------
398417 updated_node : cst.ClassDef
399418 """
419+ pytypes = self ._pytypes_stack [- 1 ]
420+ if pytypes and pytypes .attributes :
421+ updated_node = self ._insert_instance_attributes (
422+ updated_node , pytypes .attributes
423+ )
400424 self ._scope_stack .pop ()
401425 self ._pytypes_stack .pop ()
402426 return updated_node
@@ -418,6 +442,32 @@ def visit_FunctionDef(self, node):
418442 self ._pytypes_stack .append (pytypes )
419443 return True
420444
445+ def visit_IndentedBlock (self , node ):
446+ """Skip function body.
447+
448+ Parameters
449+ ----------
450+ node : cst.IndentedBlock
451+
452+ Returns
453+ -------
454+ out : bool
455+ """
456+ return not self .is_inside_function_def
457+
458+ def visit_SimpleStatementSuite (self , node ):
459+ """Skip statement suites inside functions.
460+
461+ Parameters
462+ ----------
463+ node : cst.SimpleStatementSuite
464+
465+ Returns
466+ -------
467+ out : bool
468+ """
469+ return not self .is_inside_function_def
470+
421471 def leave_FunctionDef (self , original_node , updated_node ):
422472 """Add type annotation for return to function.
423473
@@ -671,15 +721,12 @@ def leave_Module(self, original_node, updated_node):
671721 if self .current_source :
672722 current_module = module_name_from_path (self .current_source )
673723 required_imports = [
674- imp
675- for imp in self ._required_imports
676- if imp .import_path != current_module
724+ imp for imp in required_imports if imp .import_path != current_module
677725 ]
678726 import_nodes = self ._parse_imports (
679727 required_imports , current_module = current_module
680728 )
681729 updated_node = updated_node .with_changes (
682- header = [self ._docstub_generated_comment ],
683730 body = import_nodes + updated_node .body ,
684731 )
685732 self ._scope_stack .pop ()
@@ -744,6 +791,7 @@ def _parse_imports(imports, *, current_module=None):
744791 import_nodes : tuple[cst.SimpleStatementLine, ...]
745792 """
746793 lines = {imp .format_import (relative_to = current_module ) for imp in imports }
794+ lines = sorted (lines )
747795 import_nodes = tuple (cst .parse_statement (line ) for line in lines )
748796 return import_nodes
749797
@@ -840,3 +888,39 @@ def _create_annotated_assign(self, *, name, trailing_semicolon=False):
840888 semicolon = semicolon ,
841889 )
842890 return node
891+
892+ def _insert_instance_attributes (self , updated_node , attributes ):
893+ """Insert instance attributes into ClassDef node.
894+
895+ Instance attributes of classes are usually initialized inside the ``__init__``
896+ or other methods, whose body this transformer doesn't visit. Instead, we rely
897+ on the "Attributes" section in the docstring to make those available. If
898+ attributes are found in the docstring, we need to make sure that they are
899+ inserted into the class scope / definition.
900+
901+ Parameters
902+ ----------
903+ updated_node : cst.ClassDef
904+ attributes : dict[str, ~.Annotation]
905+
906+ Returns
907+ -------
908+ updated_node : cst.ClassDef
909+ """
910+ to_insert = []
911+ for name in attributes :
912+ attribute_exists = any (
913+ cstm .findall (updated_node , cstm .AnnAssign (target = cstm .Name (name )))
914+ )
915+ if attribute_exists :
916+ continue
917+
918+ assign = self ._create_annotated_assign (name = name )
919+ stmnt_line = cst .SimpleStatementLine (body = [assign ])
920+ to_insert .append (stmnt_line )
921+
922+ updated_node = updated_node .with_deep_changes (
923+ updated_node .body , body = tuple (to_insert ) + updated_node .body .body
924+ )
925+
926+ return updated_node
0 commit comments