diff --git a/py2puml/domain/umlclass.py b/py2puml/domain/umlclass.py index 8052424..364eb39 100644 --- a/py2puml/domain/umlclass.py +++ b/py2puml/domain/umlclass.py @@ -9,7 +9,15 @@ class UmlAttribute(object): type: str static: bool + +@dataclass +class UmlMethod(object): + name: str + signature: str + + @dataclass class UmlClass(UmlItem): attributes: List[UmlAttribute] + methods: List[UmlMethod] is_abstract: bool = False diff --git a/py2puml/exportpuml.py b/py2puml/exportpuml.py index f1cf556..209a762 100644 --- a/py2puml/exportpuml.py +++ b/py2puml/exportpuml.py @@ -1,3 +1,4 @@ +from inspect import signature from typing import List, Iterable from py2puml.domain.umlitem import UmlItem @@ -9,6 +10,7 @@ PUML_FILE_END = '@enduml\n' PUML_ITEM_START_TPL = '{item_type} {item_fqn} {{\n' PUML_ATTR_TPL = ' {attr_name}: {attr_type}{staticity}\n' +PUML_METHOD_TPL = ' {name}{signature}\n' PUML_ITEM_END = '}\n' PUML_COMPOSITION_TPL = '{source_fqn} {rel_type}-- {target_fqn}\n' @@ -31,6 +33,8 @@ def to_puml_content(uml_items: List[UmlItem], uml_relations: List[UmlRelation]) yield PUML_ITEM_START_TPL.format(item_type='abstract class' if uml_item.is_abstract else 'class', item_fqn=uml_class.fqn) for uml_attr in uml_class.attributes: yield PUML_ATTR_TPL.format(attr_name=uml_attr.name, attr_type=uml_attr.type, staticity=FEATURE_STATIC if uml_attr.static else FEATURE_INSTANCE) + for uml_method in uml_class.methods: + yield PUML_METHOD_TPL.format(name=uml_method.name, signature=uml_method.signature) yield PUML_ITEM_END else: raise TypeError(f'cannot process uml_item of type {uml_item.__class__}') diff --git a/py2puml/inspection/inspectclass.py b/py2puml/inspection/inspectclass.py index 93232c5..40759af 100644 --- a/py2puml/inspection/inspectclass.py +++ b/py2puml/inspection/inspectclass.py @@ -1,15 +1,14 @@ -from inspect import isabstract +import inspect from typing import Type, List, Dict from re import compile from dataclasses import dataclass from py2puml.domain.umlitem import UmlItem -from py2puml.domain.umlclass import UmlClass, UmlAttribute +from py2puml.domain.umlclass import UmlClass, UmlAttribute, UmlMethod from py2puml.domain.umlrelation import UmlRelation, RelType from py2puml.parsing.parseclassconstructor import parse_class_constructor -from py2puml.utils import inspect_domain_definition CONCRETE_TYPE_PATTERN = compile("^<(?:class|enum) '([\\.|\\w]+)'>$") @@ -33,20 +32,12 @@ def handle_inheritance_relation( ) def inspect_static_attributes( - class_type: Type, class_type_fqn: str, + definition_attrs: List[UmlAttribute], + class_type: Type, root_module_name: str, - domain_items_by_fqn: Dict[str, UmlItem], domain_relations: List[UmlRelation] -) -> List[UmlAttribute]: - definition_attrs: List[UmlAttribute] = [] - uml_class = UmlClass( - name=class_type.__name__, - fqn=class_type_fqn, - attributes=definition_attrs, - is_abstract=isabstract(class_type) - ) - domain_items_by_fqn[class_type_fqn] = uml_class +): # inspect_domain_definition(class_type) type_annotations = getattr(class_type, '__annotations__', None) if type_annotations is not None: @@ -58,7 +49,7 @@ def inspect_static_attributes( if attr_class.__module__.startswith(root_module_name): attr_type = attr_class.__name__ domain_relations.append( - UmlRelation(uml_class.fqn, f'{attr_class.__module__}.{attr_class.__name__}', RelType.COMPOSITION) + UmlRelation(class_type_fqn, f'{attr_class.__module__}.{attr_class.__name__}', RelType.COMPOSITION) ) else: attr_type = concrete_type @@ -73,7 +64,7 @@ def inspect_static_attributes( if getattr(component_class, '__name__', None) is not None ] domain_relations.extend([ - UmlRelation(uml_class.fqn, f'{component_class.__module__}.{component_class.__name__}', RelType.COMPOSITION) + UmlRelation(class_type_fqn, f'{component_class.__module__}.{component_class.__name__}', RelType.COMPOSITION) for component_class in component_classes if component_class.__module__.startswith(root_module_name) ]) @@ -83,7 +74,37 @@ def inspect_static_attributes( uml_attr = UmlAttribute(attr_name, attr_type, static=True) definition_attrs.append(uml_attr) - return definition_attrs + +def inspect_methods( + definition_methods, class_type, +): + no_dunder = lambda x: not (x[0].startswith('__') or x[0].endswith('__')) + methods = filter(no_dunder, inspect.getmembers(class_type, callable)) + for name, method in methods: + signature = inspect.signature(method) + uml_method = UmlMethod( + name=name, + signature=str(signature), + ) + definition_methods.append(uml_method) + + +def handle_class_type( + class_type: Type, + class_type_fqn: str, + domain_items_by_fqn: Dict[str, UmlItem], +) -> UmlClass: + definition_attrs: List[UmlAttribute] = [] + definition_methods: List[UmlMethod] = [] + uml_class = UmlClass( + name=class_type.__name__, + fqn=class_type_fqn, + attributes=definition_attrs, + methods=definition_methods, + is_abstract=inspect.isabstract(class_type) + ) + domain_items_by_fqn[class_type_fqn] = uml_class + return uml_class def inspect_class_type( class_type: Type, @@ -92,30 +113,30 @@ def inspect_class_type( domain_items_by_fqn: Dict[str, UmlItem], domain_relations: List[UmlRelation] ): - attributes = inspect_static_attributes( - class_type, class_type_fqn, root_module_name, - domain_items_by_fqn, domain_relations + uml_class = handle_class_type(class_type, class_type_fqn, domain_items_by_fqn) + inspect_static_attributes( + class_type_fqn, uml_class.attributes, class_type, root_module_name, domain_relations ) + inspect_methods(uml_class.methods, class_type) instance_attributes, compositions = parse_class_constructor(class_type, class_type_fqn, root_module_name) - attributes.extend(instance_attributes) + uml_class.attributes.extend(instance_attributes) domain_relations.extend(compositions.values()) handle_inheritance_relation(class_type, class_type_fqn, root_module_name, domain_relations) def inspect_dataclass_type( - class_type: Type[dataclass], + class_type: Type, class_type_fqn: str, root_module_name: str, domain_items_by_fqn: Dict[str, UmlItem], domain_relations: List[UmlRelation] ): - for attribute in inspect_static_attributes( - class_type, - class_type_fqn, - root_module_name, - domain_items_by_fqn, - domain_relations - ): + uml_class = handle_class_type(class_type, class_type_fqn, domain_items_by_fqn) + inspect_static_attributes( + class_type_fqn, uml_class.attributes, class_type, root_module_name, domain_relations + ) + inspect_methods(uml_class.methods, class_type) + for attribute in uml_class.attributes: attribute.static = False handle_inheritance_relation(class_type, class_type_fqn, root_module_name, domain_relations) \ No newline at end of file diff --git a/py2puml/inspection/inspectnamedtuple.py b/py2puml/inspection/inspectnamedtuple.py index 0b80ee9..b72eaa1 100644 --- a/py2puml/inspection/inspectnamedtuple.py +++ b/py2puml/inspection/inspectnamedtuple.py @@ -16,5 +16,6 @@ def inspect_namedtuple_type( attributes=[ UmlAttribute(tuple_field, 'Any', False) for tuple_field in namedtuple_type._fields - ] + ], + methods=[], ) diff --git a/py2puml/py2puml.domain.puml b/py2puml/py2puml.domain.puml index 44db8e8..9ec4b96 100644 --- a/py2puml/py2puml.domain.puml +++ b/py2puml/py2puml.domain.puml @@ -6,12 +6,17 @@ class py2puml.domain.umlclass.UmlAttribute { } class py2puml.domain.umlclass.UmlClass { attributes: List[UmlAttribute] + methods: List[UmlMethod] is_abstract: bool } class py2puml.domain.umlitem.UmlItem { name: str fqn: str } +class py2puml.domain.umlclass.UmlMethod { + name: str + signature: str +} class py2puml.domain.umlenum.Member { name: str value: str @@ -29,8 +34,10 @@ class py2puml.domain.umlrelation.UmlRelation { type: RelType } py2puml.domain.umlclass.UmlClass *-- py2puml.domain.umlclass.UmlAttribute +py2puml.domain.umlclass.UmlClass *-- py2puml.domain.umlclass.UmlMethod py2puml.domain.umlitem.UmlItem <|-- py2puml.domain.umlclass.UmlClass py2puml.domain.umlenum.UmlEnum *-- py2puml.domain.umlenum.Member py2puml.domain.umlitem.UmlItem <|-- py2puml.domain.umlenum.UmlEnum py2puml.domain.umlrelation.UmlRelation *-- py2puml.domain.umlrelation.RelType @enduml + diff --git a/py2puml/py2puml.parsing.puml b/py2puml/py2puml.parsing.puml new file mode 100644 index 0000000..808d756 --- /dev/null +++ b/py2puml/py2puml.parsing.puml @@ -0,0 +1,64 @@ +@startuml +class py2puml.parsing.astvisitors.AssignedVariablesCollector { + class_self_id: str + annotation: expr + variables: List[Variable] + self_attributes: List[Variable] + generic_visit(self, node) + visit(self, node) + visit_Attribute(self, node: ast.Attribute) + visit_Constant(self, node) + visit_Name(self, node: ast.Name) + visit_Subscript(self, node: ast.Subscript) +} +class py2puml.parsing.compoundtypesplitter.CompoundTypeSplitter { + compound_type_annotation: str + get_parts(self) -> Tuple[str] +} +class py2puml.parsing.astvisitors.ConstructorVisitor { + constructor_source: str + class_fqn: str + root_fqn: str + module_resolver: ModuleResolver + class_self_id: str + variables_namespace: List[Variable] + uml_attributes: List[UmlAttribute] + uml_relations_by_target_fqn: Dict[str, UmlRelation] + derive_type_annotation_details(self, annotation: ast.expr) -> Tuple[str, List[str]] + extend_relations(self, target_fqns: List[str]) + generic_visit(self, node) + get_from_namespace(self, variable_id: str) -> py2puml.parsing.astvisitors.Variable + visit(self, node) + visit_AnnAssign(self, node: ast.AnnAssign) + visit_Assign(self, node: ast.Assign) + visit_Constant(self, node) + visit_FunctionDef(self, node: ast.FunctionDef) +} +class py2puml.parsing.moduleresolver.ModuleResolver { + module: module + get_module_full_name(self) -> str + resolve_full_namespace_type(self, partial_dotted_path: str) -> Tuple[str, str] +} +class py2puml.parsing.moduleresolver.NamespacedType { + full_namespace: Any + type_name: Any +} +class py2puml.parsing.astvisitors.SignatureVariablesCollector { + constructor_source: str + class_self_id: str + variables: List[Variable] + generic_visit(self, node) + visit(self, node) + visit_Constant(self, node) + visit_arg(self, node: ast.arg) +} +class py2puml.parsing.astvisitors.Variable { + id: Any + type_expr: Any +} +py2puml.parsing.astvisitors.AssignedVariablesCollector *-- py2puml.parsing.astvisitors.Variable +py2puml.parsing.astvisitors.ConstructorVisitor *-- py2puml.parsing.moduleresolver.ModuleResolver +py2puml.parsing.astvisitors.ConstructorVisitor *-- py2puml.parsing.astvisitors.Variable +py2puml.parsing.astvisitors.SignatureVariablesCollector *-- py2puml.parsing.astvisitors.Variable +@enduml +