11
2- from typing import Dict , List , Tuple
3-
2+ from typing import Dict , List , Tuple , Type
3+ from inspect import getsource , unwrap
44from ast import (
55 NodeVisitor , arg , expr ,
66 FunctionDef , Assign , AnnAssign ,
77 Attribute , Name , Subscript , get_source_segment
88)
99from collections import namedtuple
10+ from textwrap import dedent
11+ from importlib import import_module
1012
11- from py2puml .domain .umlclass import UmlAttribute
13+ from py2puml .domain .umlclass import UmlAttribute , UmlMethod
1214from py2puml .domain .umlrelation import UmlRelation , RelType
1315from py2puml .parsing .compoundtypesplitter import CompoundTypeSplitter , SPLITTING_CHARACTERS
1416from py2puml .parsing .moduleresolver import ModuleResolver , NamespacedType
1517
16-
1718Variable = namedtuple ('Variable' , ['id' , 'type_expr' ])
1819
20+
1921class SignatureVariablesCollector (NodeVisitor ):
20- '''
21- Collects the variables and their type annotations from the signature of a constructor method
22- '''
23- def __init__ (self , constructor_source : str , * args , ** kwargs ):
22+ """
23+ Collects the variables and their type annotations from the signature of a method
24+ """
25+ def __init__ (self , skip_self = False , * args , ** kwargs ):
2426 super ().__init__ (* args , ** kwargs )
25- self .constructor_source = constructor_source
27+ self .skip_self = skip_self
2628 self .class_self_id : str = None
2729 self .variables : List [Variable ] = []
2830
2931 def visit_arg (self , node : arg ):
3032 variable = Variable (node .arg , node .annotation )
3133
3234 # first constructor variable is the name for the 'self' reference
33- if self .class_self_id is None :
35+ if self .class_self_id is None and not self . skip_self :
3436 self .class_self_id = variable .id
3537 # other arguments are constructor parameters
3638 else :
@@ -66,6 +68,57 @@ def visit_Subscript(self, node: Subscript):
6668 pass
6769
6870
71+ class ClassVisitor (NodeVisitor ):
72+
73+ def __init__ (self , class_type : Type , root_fqn : str , * args , ** kwargs ):
74+ super ().__init__ (* args , ** kwargs )
75+ self .uml_methods : List [UmlMethod ] = []
76+
77+ def visit_FunctionDef (self , node : FunctionDef ):
78+ method_visitor = MethodVisitor ()
79+ method_visitor .visit (node )
80+ self .uml_methods .append (method_visitor .uml_method )
81+
82+
83+ class MethodVisitor (NodeVisitor ):
84+ """
85+ Node visitor subclass used to walk the abstract syntax tree of a method class and identify method arguments.
86+
87+ If the method is the class constructor, instance attributes (and their type) are also identified by looking both at the constructor signature and constructor's body. When searching in the constructor's body, the visitor looks for relevant assignments (with and without type annotation).
88+ """
89+
90+ def __init__ (self , * args , ** kwargs ):
91+ super ().__init__ (* args , ** kwargs )
92+ self .variables_namespace : List [Variable ] = []
93+ self .class_self_id : str
94+ self .uml_method : UmlMethod
95+
96+ def generic_visit (self , node ):
97+ NodeVisitor .generic_visit (self , node )
98+
99+ def visit_FunctionDef (self , node : FunctionDef ):
100+ decorators = [decorator .id for decorator in node .decorator_list ]
101+ is_static = 'staticmethod' in decorators
102+ is_class = 'classmethod' in decorators
103+ variables_collector = SignatureVariablesCollector (skip_self = is_static )
104+ variables_collector .visit (node )
105+ self .variables_namespace = variables_collector .variables
106+
107+ if node .name == '__init__' :
108+ self .class_self_id : str = variables_collector .class_self_id
109+ self .generic_visit (node ) #Only visit child nodes for constructor
110+
111+ self .uml_method = UmlMethod (name = node .name , is_static = is_static , is_class = is_class )
112+ for argument in variables_collector .variables :
113+ if argument .type_expr :
114+ if hasattr (argument .type_expr , 'id' ):
115+ self .uml_method .arguments [argument .id ] = argument .type_expr .id
116+ else :
117+ self .uml_method .arguments [argument .id ] = f'SUBscript { argument .type_expr .value .id } '
118+ else :
119+ self .uml_method .arguments [argument .id ] = None
120+
121+
69122class ConstructorVisitor (NodeVisitor ):
70123 '''
71124 Identifies the attributes (and infer their type) assigned to self in the body of a constructor method
@@ -105,7 +158,7 @@ def generic_visit(self, node):
105158 def visit_FunctionDef (self , node : FunctionDef ):
106159 # retrieves constructor arguments ('self' reference and typed arguments)
107160 if node .name == '__init__' :
108- variables_collector = SignatureVariablesCollector (self . constructor_source )
161+ variables_collector = SignatureVariablesCollector ()
109162 variables_collector .visit (node )
110163 self .class_self_id : str = variables_collector .class_self_id
111164 self .variables_namespace = variables_collector .variables
@@ -158,7 +211,6 @@ def visit_Assign(self, node: Assign):
158211 # other assignments were done in new variables that can shadow existing ones
159212 self .variables_namespace .extend (variables_collector .variables )
160213
161-
162214 def derive_type_annotation_details (self , annotation : expr ) -> Tuple [str , List [str ]]:
163215 '''
164216 From a type annotation, derives:
@@ -182,13 +234,12 @@ def derive_type_annotation_details(self, annotation: expr) -> Tuple[str, List[st
182234 return short_type , [full_namespaced_type ]
183235 # compound type (List[...], Tuple[Dict[str, float], module.DomainType], etc.)
184236 elif isinstance (annotation , Subscript ):
185- return shorten_compound_type_annotation (
186- get_source_segment (self .constructor_source , annotation ),
187- self .module_resolver
188- )
189-
237+ source_segment = get_source_segment (self .constructor_source , annotation )
238+ short_type , associated_types = shorten_compound_type_annotation (source_segment , self .module_resolver )
239+ return short_type , associated_types
190240 return None , []
191241
242+
192243def shorten_compound_type_annotation (type_annotation : str , module_resolver : ModuleResolver ) -> Tuple [str , List [str ]]:
193244 '''
194245 In the string representation of a compound type annotation, the elementary types can be prefixed by their packages or sub-packages.
0 commit comments