From 823a7ca26c85847e1871ccaad0d6ab457a5454dd Mon Sep 17 00:00:00 2001 From: Jony Kalavera Date: Fri, 15 Apr 2022 22:02:43 +0200 Subject: [PATCH 1/4] draft implementation of methods support. --- py2puml/domain/umlclass.py | 11 +++- py2puml/exportpuml.py | 4 ++ py2puml/inspection/inspectclass.py | 77 +++++++++++++++++-------- py2puml/inspection/inspectnamedtuple.py | 3 +- py2puml/py2puml.domain.puml | 6 ++ 5 files changed, 74 insertions(+), 27 deletions(-) diff --git a/py2puml/domain/umlclass.py b/py2puml/domain/umlclass.py index 8052424..443b4fc 100644 --- a/py2puml/domain/umlclass.py +++ b/py2puml/domain/umlclass.py @@ -1,5 +1,6 @@ +from inspect import signature from typing import List -from dataclasses import dataclass +from dataclasses import dataclass, field from py2puml.domain.umlitem import UmlItem @@ -9,7 +10,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..e0635ea 100644 --- a/py2puml/inspection/inspectclass.py +++ b/py2puml/inspection/inspectclass.py @@ -1,15 +1,15 @@ -from inspect import isabstract +from inspect import isabstract, signature +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 +33,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 +50,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 +65,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) ]) @@ -85,6 +77,37 @@ def inspect_static_attributes( 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=isabstract(class_type) + ) + domain_items_by_fqn[class_type_fqn] = uml_class + return uml_class + def inspect_class_type( class_type: Type, class_type_fqn: str, @@ -92,9 +115,12 @@ def inspect_class_type( domain_items_by_fqn: Dict[str, UmlItem], domain_relations: List[UmlRelation] ): + uml_class = handle_class_type(class_type, class_type_fqn, domain_items_by_fqn) attributes = inspect_static_attributes( - class_type, class_type_fqn, root_module_name, - domain_items_by_fqn, domain_relations + 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) @@ -103,19 +129,20 @@ def inspect_class_type( 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) + attributes = 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 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..f51a03a 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,6 +34,7 @@ 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 From 1bb6ef3f53e214070eb53e70c97033c582b9ac0b Mon Sep 17 00:00:00 2001 From: Jony Kalavera Date: Fri, 15 Apr 2022 22:29:40 +0200 Subject: [PATCH 2/4] adjustments --- py2puml/domain/umlclass.py | 3 +-- py2puml/inspection/inspectclass.py | 19 +++++++------------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/py2puml/domain/umlclass.py b/py2puml/domain/umlclass.py index 443b4fc..364eb39 100644 --- a/py2puml/domain/umlclass.py +++ b/py2puml/domain/umlclass.py @@ -1,6 +1,5 @@ -from inspect import signature from typing import List -from dataclasses import dataclass, field +from dataclasses import dataclass from py2puml.domain.umlitem import UmlItem diff --git a/py2puml/inspection/inspectclass.py b/py2puml/inspection/inspectclass.py index e0635ea..7a39884 100644 --- a/py2puml/inspection/inspectclass.py +++ b/py2puml/inspection/inspectclass.py @@ -38,7 +38,7 @@ def inspect_static_attributes( class_type: Type, root_module_name: str, domain_relations: List[UmlRelation] -) -> List[UmlAttribute]: +): # inspect_domain_definition(class_type) type_annotations = getattr(class_type, '__annotations__', None) if type_annotations is not None: @@ -75,7 +75,6 @@ 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, @@ -116,14 +115,12 @@ def inspect_class_type( domain_relations: List[UmlRelation] ): uml_class = handle_class_type(class_type, class_type_fqn, domain_items_by_fqn) - attributes = inspect_static_attributes( + inspect_static_attributes( class_type_fqn, uml_class.attributes, class_type, root_module_name, domain_relations ) - inspect_methods( - uml_class.methods, class_type - ) + 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) @@ -136,13 +133,11 @@ def inspect_dataclass_type( domain_relations: List[UmlRelation] ): uml_class = handle_class_type(class_type, class_type_fqn, domain_items_by_fqn) - attributes = inspect_static_attributes( + 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 attributes: + 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 From cd23f0688f5bbefaa9beb760f790e85f8cc526d4 Mon Sep 17 00:00:00 2001 From: Jony Kalavera Date: Fri, 15 Apr 2022 22:55:41 +0200 Subject: [PATCH 3/4] more adjustments --- py2puml/inspection/inspectclass.py | 3 +-- py2puml/py2puml.domain.puml | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/py2puml/inspection/inspectclass.py b/py2puml/inspection/inspectclass.py index 7a39884..40759af 100644 --- a/py2puml/inspection/inspectclass.py +++ b/py2puml/inspection/inspectclass.py @@ -1,5 +1,4 @@ -from inspect import isabstract, signature import inspect from typing import Type, List, Dict @@ -102,7 +101,7 @@ def handle_class_type( fqn=class_type_fqn, attributes=definition_attrs, methods=definition_methods, - is_abstract=isabstract(class_type) + is_abstract=inspect.isabstract(class_type) ) domain_items_by_fqn[class_type_fqn] = uml_class return uml_class diff --git a/py2puml/py2puml.domain.puml b/py2puml/py2puml.domain.puml index f51a03a..9ec4b96 100644 --- a/py2puml/py2puml.domain.puml +++ b/py2puml/py2puml.domain.puml @@ -40,3 +40,4 @@ 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 + From ad7712c52c8210a3510739c066fc9ef9abf289fe Mon Sep 17 00:00:00 2001 From: Jony Kalavera Date: Fri, 15 Apr 2022 23:11:33 +0200 Subject: [PATCH 4/4] example --- py2puml/py2puml.parsing.puml | 64 ++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 py2puml/py2puml.parsing.puml 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 +