From 8de14a9caf90dff2229a2d82ee72e012c69615bb Mon Sep 17 00:00:00 2001 From: Luc Sorel-Giffo Date: Tue, 9 Jun 2020 23:53:51 +0200 Subject: [PATCH] :bookmark: 0.3.0 handle classes derived from namedtuples --- README.md | 15 +++++++++------ py2puml/parser.py | 30 ++++++++++++++++++++++-------- py2puml/utils.py | 16 +++++++++++----- pyproject.toml | 2 +- tests/modules/withnamedtuple.py | 3 +++ tests/py2puml/test_parser.py | 21 +++++++++++++++++++++ tests/py2puml/test_py2puml.py | 2 +- 7 files changed, 68 insertions(+), 21 deletions(-) create mode 100644 tests/modules/withnamedtuple.py diff --git a/README.md b/README.md index 0cf722a..2e61723 100644 --- a/README.md +++ b/README.md @@ -13,28 +13,30 @@ Generate Plantuml diagrams to document your python code # How it works +## Features + From a given path corresponding to a folder containing python code, `py2puml` loads each file as a module and generate a class diagram with the [PlantUML](https://plantuml.com/en/class-diagram) using: * inspection to detect the classes to document (see the [inspect](https://docs.python.org/3/library/inspect.html) module) * annotations (the python type hinting syntax) to detect the attributes and their types (see the [typing](https://docs.python.org/3/library/typing.html) module) +* fields for classes derived from namedtuples +* composition and inheritance relationships are drawn only between the domain classes (this is designed on purpose, for documentation sake) -Current limitations: +## Current limitations -* type hinting is optional when writing the code and discarded when it is executed, as mentionned in the official documentation. The quality of the diagram output by `py2puml` depends on the reliability with which the annotations were written +* type hinting is optional when writing Python code and discarded when it is executed, as mentionned in the [typing official documentation](https://docs.python.org/3/library/typing.html). The quality of the diagram output by `py2puml` depends on the reliability with which the type annotations were written > The Python runtime does not enforce function and variable type annotations. They can be used by third party tools such as type checkers, IDEs, linters, etc. * complex type hints with more than one level of genericity are not properly handled for the moment: `List[MyClass]` or `Dict[str, MyClass]` are handled properly, `Dict[str, List[MyClass]]` is not. If your domain classes (also called business objects or DTOs) have attributes with complex type hints, it may be a code smell indicating that you should write a class which would better represent the business logic. But I may improve this part of the library as well 😀 -* composition relationships are detected and drawn. Inheritance relationships are not handled for now - * `py2puml` does not inspect sub-folders recursively, but it is planned * `py2puml` outputs diagrams in PlantUML syntax, which can be saved in text files along your python code and versioned with them. To generate image files, use the PlantUML runtime or a docker image (see [think/plantuml](https://hub.docker.com/r/think/plantuml)) * `py2puml` uses features of python 3 (generators for example) and thus won't work with python 2 runtimes. It relies on native python modules and uses no 3rd-party library, except [pytest](https://docs.pytest.org/en/latest/) as a development dependency for running the unit-tests -You may also be interested in this [lucsorel/plantuml-file-loader](https://github.com/lucsorel/plantuml-file-loader) project: A webpack loader which converts PlantUML files into images during the webpack processing (useful to [include PlantUML diagrams in your slides](https://github.com/lucsorel/markdown-image-loader/blob/master/README.md#web-based-slideshows) with RevealJS or RemarkJS). +If you like tools around PlantUML, you may also be interested in this [lucsorel/plantuml-file-loader](https://github.com/lucsorel/plantuml-file-loader) project: A webpack loader which converts PlantUML files into images during the webpack processing (useful to [include PlantUML diagrams in your slides](https://github.com/lucsorel/markdown-image-loader/blob/master/README.md#web-based-slideshows) with RevealJS or RemarkJS). # Install @@ -126,6 +128,7 @@ python3 -m pytest -v # Changelog +* `0.3.0`: handle classes derived from namedtuples (attribute types are `any`) * `0.2.0`: handle inheritance relationships and enums. Unit tested * `0.1.3`: first release, handle all module of a folder and compositions of domain classes @@ -142,7 +145,7 @@ Pull-requests are welcome and will be processed on a best-effort basis. # Alternatives -If `py2puml` does not meet your needs (suggestions and pull-requests are welcome), you can have a look at these projects which follow other approaches (AST, linting, modeling): +If `py2puml` does not meet your needs (suggestions and pull-requests are **welcome**), you can have a look at these projects which follow other approaches (AST, linting, modeling): * [cb109/pyplantuml](https://github.com/cb109/pyplantuml) * [deadbok/py-puml-tools](https://github.com/deadbok/py-puml-tools) diff --git a/py2puml/parser.py b/py2puml/parser.py index b631cf5..dfa4c95 100644 --- a/py2puml/parser.py +++ b/py2puml/parser.py @@ -34,17 +34,29 @@ def parse_enum_type( enum_type_fqdn: str, domain_items_by_fqdn: Dict[str, UmlItem] ): - enum_members = [ - Member(name=enum_member.name, value=enum_member.value) - for enum_member in enum_type.__members__.values() - ] - - enum_class = UmlEnum( + domain_items_by_fqdn[enum_type_fqdn] = UmlEnum( name=enum_type.__name__, fqdn=enum_type_fqdn, - members=enum_members + members=[ + Member(name=enum_member.name, value=enum_member.value) + for enum_member in enum_type.__members__.values() + ] + ) + +def parse_namedtupled_class( + namedtupled_type: Type, + namedtupled_type_fqdn: str, + domain_items_by_fqdn: Dict[str, UmlItem] +): + domain_items_by_fqdn[namedtupled_type_fqdn] = UmlClass( + name=namedtupled_type.__name__, + fqdn=namedtupled_type_fqdn, + attributes=[ + UmlAttribute(tuple_field, 'any') + for tuple_field in namedtupled_type._fields + ] ) - domain_items_by_fqdn[enum_type_fqdn] = enum_class + def handle_inheritance_relation( class_type: Type, @@ -122,6 +134,8 @@ def parse_type( if definition_type_fqdn not in domain_items_by_fqdn: if issubclass(definition_type, Enum): parse_enum_type(definition_type, definition_type_fqdn, domain_items_by_fqdn) + elif getattr(definition_type, '_fields', None) is not None: + parse_namedtupled_class(definition_type, definition_type_fqdn, domain_items_by_fqdn) else: parse_class_type(definition_type, definition_type_fqdn, root_module_name, domain_items_by_fqdn, domain_relations) diff --git a/py2puml/utils.py b/py2puml/utils.py index 245e3b0..a04f682 100644 --- a/py2puml/utils.py +++ b/py2puml/utils.py @@ -1,18 +1,24 @@ from typing import Type -def inspect_type(type: Type): +def inspect_type(type_to_inspect: Type): ''' Utilitary function which inspects the annotations of the given type ''' - type_annotations = getattr(type, '__annotations__', None) + type_annotations = getattr(type_to_inspect, '__annotations__', None) if type_annotations is None: - print(f'class {type.__module__}.{type.__name__} has no annotation') + # print(f'class {type_to_inspect.__module__}.{type_to_inspect.__name__} of type {type(type_to_inspect)} has no annotation') + for attr_class_key in dir(type_to_inspect): + if attr_class_key != '__doc__': + print( + f'{type_to_inspect.__name__}.{attr_class_key}:', + getattr(type_to_inspect, attr_class_key) + ) else: - # print(type.__annotations__) + # print(type_to_inspect.__annotations__) for attr_name, attr_class in type_annotations.items(): for attr_class_key in dir(attr_class): if attr_class_key != '__doc__': print( - f'{type.__name__}.{attr_name}:', + f'{type_to_inspect.__name__}.{attr_name}:', attr_class_key, getattr(attr_class, attr_class_key) ) diff --git a/pyproject.toml b/pyproject.toml index fc8277d..b9cc946 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "py2puml" -version = "0.2.0" +version = "0.3.0" description = "Generate Plantuml diagrams to document your python code " keywords = ["class diagram", "PlantUML", "documentation"] readme = "README.md" diff --git a/tests/modules/withnamedtuple.py b/tests/modules/withnamedtuple.py new file mode 100644 index 0000000..817878d --- /dev/null +++ b/tests/modules/withnamedtuple.py @@ -0,0 +1,3 @@ +from collections import namedtuple + +Circle = namedtuple('Circle', ['x', 'y', 'radius'], defaults=[1]) \ No newline at end of file diff --git a/tests/py2puml/test_parser.py b/tests/py2puml/test_parser.py index aece7ff..e0e08f5 100644 --- a/tests/py2puml/test_parser.py +++ b/tests/py2puml/test_parser.py @@ -10,6 +10,7 @@ from tests.modules.withcomposition import Worker from tests.modules.withenum import TimeUnit from tests.modules.withinheritancewithinmodule import GlowingFish +from tests.modules.withnamedtuple import Circle def assert_attribute(attribute: UmlAttribute, expected_name: str, expected_type: str): assert attribute.name == expected_name @@ -95,3 +96,23 @@ def test_parse_inheritance_within_module(): assert inheritance.type == RelType.INHERITANCE assert inheritance.source_fqdn == 'tests.modules.withinheritancewithinmodule.Light', 'parent class' assert inheritance.target_fqdn == 'tests.modules.withinheritancewithinmodule.GlowingFish', 'child class' + +def test_parse_namedtupled_class(): + domain_items_by_fqdn: Dict[str, UmlItem] = {} + domain_relations: List[UmlRelation] = [] + parse_type(Circle, 'tests.modules.withnamedtuple', domain_items_by_fqdn, domain_relations) + + umlitems_by_fqdn = list(domain_items_by_fqdn.items()) + assert len(umlitems_by_fqdn) == 1, 'one namedtupled class has been parsed' + namedtupled_class: UmlClass + fqdn, namedtupled_class = umlitems_by_fqdn[0] + assert fqdn == 'tests.modules.withnamedtuple.Circle' + assert namedtupled_class.fqdn == fqdn + assert namedtupled_class.name == 'Circle' + attributes = namedtupled_class.attributes + assert len(attributes) == 3, 'namedtupled class has 3 attributes' + assert_attribute(attributes[0], 'x', 'any') + assert_attribute(attributes[1], 'y', 'any') + assert_attribute(attributes[2], 'radius', 'any') + + assert len(domain_relations) == 0, 'parsing enum adds no relation' diff --git a/tests/py2puml/test_py2puml.py b/tests/py2puml/test_py2puml.py index e5e5bef..825927d 100644 --- a/tests/py2puml/test_py2puml.py +++ b/tests/py2puml/test_py2puml.py @@ -2,4 +2,4 @@ def test_version(): - assert __version__ == '0.2.0' + assert __version__ == '0.3.0'