Skip to content

Commit e2270c6

Browse files
authored
🔖 0.3.0 handle classes derived from namedtuples (#6)
1 parent a6fa72d commit e2270c6

File tree

7 files changed

+68
-21
lines changed

7 files changed

+68
-21
lines changed

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,30 @@ Generate Plantuml diagrams to document your python code
1313

1414
# How it works
1515

16+
## Features
17+
1618
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:
1719

1820
* inspection to detect the classes to document (see the [inspect](https://docs.python.org/3/library/inspect.html) module)
1921
* 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)
22+
* fields for classes derived from namedtuples
23+
* composition and inheritance relationships are drawn only between the domain classes (this is designed on purpose, for documentation sake)
2024

21-
Current limitations:
25+
## Current limitations
2226

23-
* 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
27+
* 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
2428

2529
> 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.
2630
2731
* 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 😀
2832

29-
* composition relationships are detected and drawn. Inheritance relationships are not handled for now
30-
3133
* `py2puml` does not inspect sub-folders recursively, but it is planned
3234

3335
* `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))
3436

3537
* `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
3638

37-
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).
39+
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).
3840

3941
# Install
4042

@@ -126,6 +128,7 @@ python3 -m pytest -v
126128

127129
# Changelog
128130

131+
* `0.3.0`: handle classes derived from namedtuples (attribute types are `any`)
129132
* `0.2.0`: handle inheritance relationships and enums. Unit tested
130133
* `0.1.3`: first release, handle all module of a folder and compositions of domain classes
131134

@@ -142,7 +145,7 @@ Pull-requests are welcome and will be processed on a best-effort basis.
142145

143146
# Alternatives
144147

145-
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):
148+
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):
146149

147150
* [cb109/pyplantuml](https://github.com/cb109/pyplantuml)
148151
* [deadbok/py-puml-tools](https://github.com/deadbok/py-puml-tools)

py2puml/parser.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,29 @@ def parse_enum_type(
3434
enum_type_fqdn: str,
3535
domain_items_by_fqdn: Dict[str, UmlItem]
3636
):
37-
enum_members = [
38-
Member(name=enum_member.name, value=enum_member.value)
39-
for enum_member in enum_type.__members__.values()
40-
]
41-
42-
enum_class = UmlEnum(
37+
domain_items_by_fqdn[enum_type_fqdn] = UmlEnum(
4338
name=enum_type.__name__,
4439
fqdn=enum_type_fqdn,
45-
members=enum_members
40+
members=[
41+
Member(name=enum_member.name, value=enum_member.value)
42+
for enum_member in enum_type.__members__.values()
43+
]
44+
)
45+
46+
def parse_namedtupled_class(
47+
namedtupled_type: Type,
48+
namedtupled_type_fqdn: str,
49+
domain_items_by_fqdn: Dict[str, UmlItem]
50+
):
51+
domain_items_by_fqdn[namedtupled_type_fqdn] = UmlClass(
52+
name=namedtupled_type.__name__,
53+
fqdn=namedtupled_type_fqdn,
54+
attributes=[
55+
UmlAttribute(tuple_field, 'any')
56+
for tuple_field in namedtupled_type._fields
57+
]
4658
)
47-
domain_items_by_fqdn[enum_type_fqdn] = enum_class
59+
4860

4961
def handle_inheritance_relation(
5062
class_type: Type,
@@ -122,6 +134,8 @@ def parse_type(
122134
if definition_type_fqdn not in domain_items_by_fqdn:
123135
if issubclass(definition_type, Enum):
124136
parse_enum_type(definition_type, definition_type_fqdn, domain_items_by_fqdn)
137+
elif getattr(definition_type, '_fields', None) is not None:
138+
parse_namedtupled_class(definition_type, definition_type_fqdn, domain_items_by_fqdn)
125139
else:
126140
parse_class_type(definition_type, definition_type_fqdn, root_module_name, domain_items_by_fqdn, domain_relations)
127141

py2puml/utils.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
from typing import Type
22

3-
def inspect_type(type: Type):
3+
def inspect_type(type_to_inspect: Type):
44
'''
55
Utilitary function which inspects the annotations of the given type
66
'''
7-
type_annotations = getattr(type, '__annotations__', None)
7+
type_annotations = getattr(type_to_inspect, '__annotations__', None)
88
if type_annotations is None:
9-
print(f'class {type.__module__}.{type.__name__} has no annotation')
9+
# print(f'class {type_to_inspect.__module__}.{type_to_inspect.__name__} of type {type(type_to_inspect)} has no annotation')
10+
for attr_class_key in dir(type_to_inspect):
11+
if attr_class_key != '__doc__':
12+
print(
13+
f'{type_to_inspect.__name__}.{attr_class_key}:',
14+
getattr(type_to_inspect, attr_class_key)
15+
)
1016
else:
11-
# print(type.__annotations__)
17+
# print(type_to_inspect.__annotations__)
1218
for attr_name, attr_class in type_annotations.items():
1319
for attr_class_key in dir(attr_class):
1420
if attr_class_key != '__doc__':
1521
print(
16-
f'{type.__name__}.{attr_name}:',
22+
f'{type_to_inspect.__name__}.{attr_name}:',
1723
attr_class_key, getattr(attr_class, attr_class_key)
1824
)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "py2puml"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
description = "Generate Plantuml diagrams to document your python code "
55
keywords = ["class diagram", "PlantUML", "documentation"]
66
readme = "README.md"

tests/modules/withnamedtuple.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from collections import namedtuple
2+
3+
Circle = namedtuple('Circle', ['x', 'y', 'radius'], defaults=[1])

tests/py2puml/test_parser.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from tests.modules.withcomposition import Worker
1111
from tests.modules.withenum import TimeUnit
1212
from tests.modules.withinheritancewithinmodule import GlowingFish
13+
from tests.modules.withnamedtuple import Circle
1314

1415
def assert_attribute(attribute: UmlAttribute, expected_name: str, expected_type: str):
1516
assert attribute.name == expected_name
@@ -95,3 +96,23 @@ def test_parse_inheritance_within_module():
9596
assert inheritance.type == RelType.INHERITANCE
9697
assert inheritance.source_fqdn == 'tests.modules.withinheritancewithinmodule.Light', 'parent class'
9798
assert inheritance.target_fqdn == 'tests.modules.withinheritancewithinmodule.GlowingFish', 'child class'
99+
100+
def test_parse_namedtupled_class():
101+
domain_items_by_fqdn: Dict[str, UmlItem] = {}
102+
domain_relations: List[UmlRelation] = []
103+
parse_type(Circle, 'tests.modules.withnamedtuple', domain_items_by_fqdn, domain_relations)
104+
105+
umlitems_by_fqdn = list(domain_items_by_fqdn.items())
106+
assert len(umlitems_by_fqdn) == 1, 'one namedtupled class has been parsed'
107+
namedtupled_class: UmlClass
108+
fqdn, namedtupled_class = umlitems_by_fqdn[0]
109+
assert fqdn == 'tests.modules.withnamedtuple.Circle'
110+
assert namedtupled_class.fqdn == fqdn
111+
assert namedtupled_class.name == 'Circle'
112+
attributes = namedtupled_class.attributes
113+
assert len(attributes) == 3, 'namedtupled class has 3 attributes'
114+
assert_attribute(attributes[0], 'x', 'any')
115+
assert_attribute(attributes[1], 'y', 'any')
116+
assert_attribute(attributes[2], 'radius', 'any')
117+
118+
assert len(domain_relations) == 0, 'parsing enum adds no relation'

tests/py2puml/test_py2puml.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33

44
def test_version():
5-
assert __version__ == '0.2.0'
5+
assert __version__ == '0.3.0'

0 commit comments

Comments
 (0)