Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add methods support. #30

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions py2puml/domain/umlclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions py2puml/exportpuml.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from inspect import signature
from typing import List, Iterable

from py2puml.domain.umlitem import UmlItem
Expand All @@ -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'

Expand All @@ -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__}')
Expand Down
80 changes: 51 additions & 29 deletions py2puml/inspection/inspectclass.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@

from inspect import isabstract
from inspect import isabstract, signature
import inspect
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guidelines favor named imports instead of module imports: named imports gives meaning to what is happening in the module, module imports clutters the module's memory footprint

from typing import Type, List, Dict

from re import compile
from dataclasses import dataclass
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dataclass seems not necessary anymore after your fix, is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check.


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]+)'>$")

Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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)
])
Expand All @@ -83,7 +75,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(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function needs unit-testing, once we agree on the implementation details

definition_methods, class_type,
):
no_dunder = lambda x: not (x[0].startswith('__') or x[0].endswith('__'))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just excluding magic methods

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please, make no_dunder a proper function that must be unit-tested. And please, give more meaningful names to variables (x -> member, for instance)

Copy link
Contributor Author

@jonykalavera jonykalavera Apr 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😰 ups. lol I was really hacking it together. sure thing. also, I tested this with some other projects and it totally shows more than what we want to see. need to look a lot further into determining declared methods only.

methods = filter(no_dunder, inspect.getmembers(class_type, callable))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from inspect import getmembers

for name, method in methods:
signature = inspect.signature(method)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from inspect import signature

uml_method = UmlMethod(
name=name,
signature=str(signature),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is a convenient way to get the whole signature, but:

  • it does not output the signature in the PlantUML expected syntax see comments 2. in extract methods #11 (comment)
  • the way str(signature) behave is likely to change and break the feature

However, I don't like how I implemented the the way attributes detected by inspection (dataclasses, class attributes) are typed: builtins.date would be displayed instead of just date for example. I did a better job when detecting instance attributes in constructors, the whole would need harmonization.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is a convenient way to get the whole signature, but:

* it does not output the signature in the PlantUML expected syntax see comments `2.` in [extract methods #11 (comment)](https://github.com/lucsorel/py2puml/issues/11#issuecomment-1104471337)

it does not guarantee it for sure. I should just iterate the structure we can rely on and build it from ther

* the way `str(signature)` behave is likely to change and break the feature

It might. yeah

However, I don't like how I implemented the the way attributes detected by inspection (dataclasses, class attributes) are typed: builtins.date would be displayed instead of just date for example. I did a better job when detecting instance attributes in constructors, the whole would need harmonization.

I'll make sure to take a closer look there.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for now, let's leave the signature format like that, it is not how UML users expect it, but it is readable to python users

)
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,
Expand All @@ -92,30 +114,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,
Copy link
Contributor Author

@jonykalavera jonykalavera Apr 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dataclass is just a decorator function.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

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)
3 changes: 2 additions & 1 deletion py2puml/inspection/inspectnamedtuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ def inspect_namedtuple_type(
attributes=[
UmlAttribute(tuple_field, 'Any', False)
for tuple_field in namedtuple_type._fields
]
],
methods=[],
Copy link
Contributor Author

@jonykalavera jonykalavera Apr 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically namedtuples can have methods. should we support it?

from typing import NamedTuple


class Employee(NamedTuple):
    """Represents an employee."""
    name: str
    id: int = 3

    def foo(self) -> str:
        return f'<Employee {self.name}, id={self.id}>'

emp = Employee('Hello')
print(emp)
print(emp.foo())
print(emp._fields)

```

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it is easy to distinguish added methods (like the Employee.foo() one) from native methods of named tuples (like _asdict), let's do it. Otherwise, let's wait for the need to come and the feature to be requested 😃

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's see after I figure it out for regular classes and then we get back to this one to do it or split it.

)
6 changes: 6 additions & 0 deletions py2puml/py2puml.domain.puml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down