Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand Down
30 changes: 22 additions & 8 deletions py2puml/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down
16 changes: 11 additions & 5 deletions py2puml/utils.py
Original file line number Diff line number Diff line change
@@ -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)
)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
3 changes: 3 additions & 0 deletions tests/modules/withnamedtuple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from collections import namedtuple

Circle = namedtuple('Circle', ['x', 'y', 'radius'], defaults=[1])
21 changes: 21 additions & 0 deletions tests/py2puml/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
2 changes: 1 addition & 1 deletion tests/py2puml/test_py2puml.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@


def test_version():
assert __version__ == '0.2.0'
assert __version__ == '0.3.0'