@@ -341,13 +341,27 @@ def _parse_python(self, filepath: str, content: str, tree) -> ModuleInfo:
341341 if not func .name .startswith ('_' ):
342342 exports .append (func .name )
343343 elif node_type == 'decorated_definition' :
344+ # Handle decorated functions
344345 inner_func = self ._find_child (child , 'function_definition' )
345346 if inner_func :
346347 func = self ._extract_py_function (inner_func , content , child )
347348 if func :
348349 functions .append (func )
349350 if not func .name .startswith ('_' ):
350351 exports .append (func .name )
352+
353+ # Handle decorated classes (e.g., @dataclass)
354+ inner_class = self ._find_child (child , 'class_definition' )
355+ if inner_class :
356+ cls = self ._extract_py_class (inner_class , content , decorated_node = child )
357+ if cls :
358+ classes .append (cls )
359+ if not cls .name .startswith ('_' ):
360+ exports .append (cls .name )
361+
362+ enum_type = self ._extract_py_enum (inner_class , content )
363+ if enum_type :
364+ types .append (enum_type )
351365
352366 # Constants with enhanced extraction
353367 if node_type == 'expression_statement' :
@@ -652,7 +666,7 @@ def _extract_py_enum(self, node, content: str) -> Optional[TypeInfo]:
652666 except Exception :
653667 return None
654668
655- def _extract_py_class (self , node , content : str ) -> Optional [ClassInfo ]:
669+ def _extract_py_class (self , node , content : str , decorated_node = None ) -> Optional [ClassInfo ]:
656670 """Extract Python class from AST node."""
657671 name_node = self ._find_child (node , 'identifier' )
658672 if not name_node :
@@ -670,10 +684,10 @@ def _extract_py_class(self, node, content: str) -> Optional[ClassInfo]:
670684 # Check for dataclass decorator
671685 decorators = []
672686 is_dataclass = False
673- # Look for parent decorated_definition
674- parent = getattr (node , 'parent' , None )
675- if parent and parent .type == 'decorated_definition' :
676- for c in parent .children :
687+ # Use provided decorated_node or try to find parent
688+ dec_source = decorated_node or getattr (node , 'parent' , None )
689+ if dec_source and dec_source .type == 'decorated_definition' :
690+ for c in dec_source .children :
677691 if c .type == 'decorator' :
678692 dec_text = self ._text (c , content ).lstrip ('@' )
679693 decorators .append (dec_text .split ('(' )[0 ])
@@ -685,6 +699,7 @@ def _extract_py_class(self, node, content: str) -> Optional[ClassInfo]:
685699 methods = []
686700 fields = []
687701 attributes = []
702+ properties = []
688703 body = self ._find_child (node , 'block' )
689704 if body :
690705 for i , child in enumerate (body .children ):
@@ -697,24 +712,29 @@ def _extract_py_class(self, node, content: str) -> Optional[ClassInfo]:
697712 m = self ._extract_py_function (child , content )
698713 if m :
699714 methods .append (m )
715+ # Extract self.x = ... from __init__ method
716+ if m .name == '__init__' and not is_dataclass :
717+ init_attrs = self ._extract_init_attributes (child , content )
718+ attributes .extend (init_attrs )
700719 elif child .type == 'decorated_definition' :
701720 inner = self ._find_child (child , 'function_definition' )
702721 if inner :
703722 m = self ._extract_py_function (inner , content , child )
704723 if m :
705724 methods .append (m )
706725
707- # Extract dataclass fields
726+ # Extract dataclass fields (class-level annotated assignments)
708727 elif is_dataclass and child .type == 'expression_statement' :
709728 field = self ._extract_dataclass_field (child , content )
710729 if field :
711730 fields .append (field )
712731
713- # Extract instance attributes (self.x = ... )
732+ # Extract class-level properties (annotated assignments without dataclass )
714733 elif child .type == 'expression_statement' and not is_dataclass :
715- attr = self ._extract_class_attribute (child , content )
716- if attr :
717- attributes .append (attr )
734+ # Check for annotated assignment like "x: int" or "x: int = 5"
735+ prop = self ._extract_class_property (child , content )
736+ if prop :
737+ properties .append (prop )
718738
719739 return ClassInfo (
720740 name = name ,
@@ -724,6 +744,7 @@ def _extract_py_class(self, node, content: str) -> Optional[ClassInfo]:
724744 is_dataclass = is_dataclass ,
725745 fields = fields ,
726746 attributes = attributes ,
747+ properties = properties ,
727748 methods = methods ,
728749 is_interface = False ,
729750 is_abstract = 'ABC' in bases or 'ABCMeta' in bases ,
@@ -824,6 +845,46 @@ def _extract_class_attribute(self, node, content: str) -> Optional[AttributeInfo
824845 )
825846 return None
826847
848+ def _extract_init_attributes (self , func_node , content : str ) -> List [AttributeInfo ]:
849+ """Extract self.x = ... assignments from __init__ method body."""
850+ attributes = []
851+ seen_names = set ()
852+
853+ def scan_block (block_node ):
854+ """Recursively scan block for self.x assignments."""
855+ if not block_node :
856+ return
857+ for child in block_node .children :
858+ if child .type == 'expression_statement' :
859+ attr = self ._extract_class_attribute (child , content )
860+ if attr and attr .name not in seen_names :
861+ seen_names .add (attr .name )
862+ attributes .append (attr )
863+ # Also scan nested blocks (if/for/while/try)
864+ elif child .type in ('if_statement' , 'for_statement' , 'while_statement' , 'try_statement' ):
865+ for sub in child .children :
866+ if sub .type == 'block' :
867+ scan_block (sub )
868+
869+ body = self ._find_child (func_node , 'block' )
870+ scan_block (body )
871+ return attributes [:15 ] # Limit to 15 attributes
872+
873+ def _extract_class_property (self , node , content : str ) -> Optional [str ]:
874+ """Extract class-level property from annotated assignment."""
875+ try :
876+ stmt_text = self ._text (node , content ).strip ()
877+ except Exception :
878+ return None
879+
880+ # Match annotated assignment: "name: Type" or "name: Type = value"
881+ m = re .match (r'^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([^=]+?)(?:\s*=.*)?$' , stmt_text )
882+ if m :
883+ name = m .group (1 )
884+ type_ann = m .group (2 ).strip ()
885+ return f"{ name } : { type_ann } "
886+ return None
887+
827888 def _extract_py_import (self , node , content : str ) -> List [str ]:
828889 """Extract import statement."""
829890 imports = []
@@ -1575,14 +1636,26 @@ def _extract_ast_class(self, node: ast.ClassDef) -> ClassInfo:
15751636 if isinstance (target , ast .Name ):
15761637 properties .append (target .id )
15771638
1639+ # Extract decorators
1640+ decorators = []
1641+ for dec in node .decorator_list :
1642+ if isinstance (dec , ast .Name ):
1643+ decorators .append (dec .id )
1644+ elif isinstance (dec , ast .Call ) and isinstance (dec .func , ast .Name ):
1645+ decorators .append (dec .func .id )
1646+ elif isinstance (dec , ast .Attribute ):
1647+ decorators .append (dec .attr )
1648+
15781649 return ClassInfo (
15791650 name = node .name ,
15801651 bases = bases ,
1652+ decorators = decorators ,
15811653 docstring = ast .get_docstring (node )[:100 ] if ast .get_docstring (node ) else None ,
1654+ is_dataclass = is_dataclass ,
15821655 methods = methods ,
15831656 properties = properties ,
15841657 is_interface = False ,
1585- is_abstract = 'ABC' in bases or is_dataclass ,
1658+ is_abstract = 'ABC' in bases ,
15861659 generic_params = []
15871660 )
15881661
0 commit comments