Skip to content

Commit 7762531

Browse files
jtc42vignesh14052002vignesharivazhagan
authored
feat: expose controls on which block, method, relation can be included in uml diagram (#3)
* Expose controls on which block, method, relation can be included in diagram * use Filters instead of kwargs * Add tests * Add filter in readme --------- Co-authored-by: vignesh14052002 <[email protected]> Co-authored-by: vignesh-arivazhagan <[email protected]>
1 parent bf7af0e commit 7762531

File tree

6 files changed

+210
-6
lines changed

6 files changed

+210
-6
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,30 @@ if __name__ == '__main__':
183183

184184
* running it outputs the previous PlantUML diagram in the terminal and writes it in a file.
185185

186+
### Additionally you can also pass filters to skip specific blocks and relations
187+
```python
188+
from py2puml.domain.umlrelation import UmlRelation
189+
from py2puml.domain.umlclass import UmlMethod
190+
from py2puml.domain.umlitem import UmlItem
191+
from py2puml.export.puml import Filters
192+
from py2puml.py2puml import py2puml
193+
194+
def skip_block(item: UmlItem) -> bool:
195+
return item.fqn.endswith('<block-to-ignore>')
196+
197+
def skip_relation(relation: UmlRelation) -> bool:
198+
return relation.source_fqn.endswith('<relation-source>') and relation.target_fqn.endswith('<relation-target>')
199+
200+
filters = Filters(skip_block, skip_relation)
201+
202+
puml_content = "".join(
203+
py2puml(
204+
'py2puml/domain',
205+
'py2puml.domain',
206+
filters
207+
)
208+
)
209+
```
186210

187211
# Tests
188212

py2puml/export/__init__.py

Whitespace-only changes.

py2puml/export/puml.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Iterable, List
1+
from dataclasses import dataclass
2+
from typing import Callable, Iterable, List, Optional
23

34
from py2puml.domain.umlclass import UmlClass
45
from py2puml.domain.umlenum import UmlEnum
@@ -26,11 +27,40 @@
2627
FEATURE_INSTANCE = ''
2728

2829

29-
def to_puml_content(diagram_name: str, uml_items: List[UmlItem], uml_relations: List[UmlRelation]) -> Iterable[str]:
30+
@dataclass
31+
class Filters:
32+
skip_block: Optional[Callable[[UmlItem], bool]] = None
33+
skip_relation: Optional[Callable[[UmlRelation], bool]] = None
34+
35+
36+
def should_skip(filter: Callable | None, item: UmlItem | UmlRelation) -> bool:
37+
if filter is None:
38+
return False
39+
40+
if not callable(filter):
41+
raise ValueError('Filter must be a callable')
42+
43+
try:
44+
_should_skip = filter(item)
45+
if not isinstance(_should_skip, bool):
46+
raise ValueError('Filter must return a boolean value')
47+
return _should_skip
48+
except Exception as e:
49+
raise ValueError('Error while applying filter') from e
50+
51+
52+
def to_puml_content(
53+
diagram_name: str, uml_items: List[UmlItem], uml_relations: List[UmlRelation], filters: Optional[Filters] = None
54+
) -> Iterable[str]:
55+
if filters is None:
56+
filters = Filters()
57+
3058
yield PUML_FILE_START.format(diagram_name=diagram_name)
3159

3260
# exports the domain classes and enums
3361
for uml_item in uml_items:
62+
if should_skip(filters.skip_block, uml_item):
63+
continue
3464
if isinstance(uml_item, UmlEnum):
3565
uml_enum: UmlEnum = uml_item
3666
yield PUML_ITEM_START_TPL.format(item_type='enum', item_fqn=uml_enum.fqn)
@@ -48,12 +78,15 @@ def to_puml_content(diagram_name: str, uml_items: List[UmlItem], uml_relations:
4878
attr_type=uml_attr.type,
4979
staticity=FEATURE_STATIC if uml_attr.static else FEATURE_INSTANCE,
5080
)
81+
# TODO: Add skip_method filter here once PR #43 is merged
5182
yield PUML_ITEM_END
5283
else:
5384
raise TypeError(f'cannot process uml_item of type {uml_item.__class__}')
5485

5586
# exports the domain relationships between classes and enums
5687
for uml_relation in uml_relations:
88+
if should_skip(filters.skip_relation, uml_relation):
89+
continue
5790
yield PUML_RELATION_TPL.format(
5891
source_fqn=uml_relation.source_fqn, rel_type=uml_relation.type.value, target_fqn=uml_relation.target_fqn
5992
)

py2puml/py2puml.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
from typing import Dict, Iterable, List
1+
from typing import Dict, Iterable, List, Optional
22

33
from py2puml.domain.umlitem import UmlItem
44
from py2puml.domain.umlrelation import UmlRelation
5-
from py2puml.export.puml import to_puml_content
5+
from py2puml.export.puml import Filters, to_puml_content
66
from py2puml.inspection.inspectpackage import inspect_package
77

88

9-
def py2puml(domain_path: str, domain_module: str) -> Iterable[str]:
9+
def py2puml(domain_path: str, domain_module: str, filters: Optional[Filters] = None) -> Iterable[str]:
1010
domain_items_by_fqn: Dict[str, UmlItem] = {}
1111
domain_relations: List[UmlRelation] = []
1212
inspect_package(domain_path, domain_module, domain_items_by_fqn, domain_relations)
1313

14-
return to_puml_content(domain_module, domain_items_by_fqn.values(), domain_relations)
14+
return to_puml_content(domain_module, domain_items_by_fqn.values(), domain_relations, filters)

tests/modules/__init__.py

Whitespace-only changes.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import pytest
2+
3+
from py2puml.domain.umlitem import UmlItem
4+
from py2puml.domain.umlrelation import UmlRelation
5+
from py2puml.export.puml import Filters
6+
from py2puml.py2puml import py2puml
7+
8+
un_modified_puml = [
9+
'@startuml tests.modules.withinheritedconstructor\n!pragma useIntermediatePackages false\n\n',
10+
'class tests.modules.withinheritedconstructor.metricorigin.MetricOrigin {\n',
11+
' unit: str {static}\n',
12+
'}\n',
13+
'class tests.modules.withinheritedconstructor.point.Origin {\n',
14+
' is_origin: bool {static}\n',
15+
'}\n',
16+
'class tests.modules.withinheritedconstructor.point.Point {\n',
17+
' x: float\n',
18+
' y: float\n',
19+
'}\n',
20+
'tests.modules.withinheritedconstructor.point.Origin <|-- tests.modules.withinheritedconstructor.metricorigin.MetricOrigin\n',
21+
'tests.modules.withinheritedconstructor.point.Point <|-- tests.modules.withinheritedconstructor.point.Origin\n',
22+
'footer Generated by //py2puml//\n',
23+
'@enduml\n',
24+
]
25+
26+
puml_with_origin_class_skipped = [
27+
'@startuml tests.modules.withinheritedconstructor\n!pragma useIntermediatePackages false\n\n',
28+
'class tests.modules.withinheritedconstructor.metricorigin.MetricOrigin {\n',
29+
' unit: str {static}\n',
30+
'}\n',
31+
'class tests.modules.withinheritedconstructor.point.Point {\n',
32+
' x: float\n',
33+
' y: float\n',
34+
'}\n',
35+
'tests.modules.withinheritedconstructor.point.Origin <|-- tests.modules.withinheritedconstructor.metricorigin.MetricOrigin\n',
36+
'tests.modules.withinheritedconstructor.point.Point <|-- tests.modules.withinheritedconstructor.point.Origin\n',
37+
'footer Generated by //py2puml//\n',
38+
'@enduml\n',
39+
]
40+
41+
puml_with_point_origin_relation_skipped = [
42+
'@startuml tests.modules.withinheritedconstructor\n!pragma useIntermediatePackages false\n\n',
43+
'class tests.modules.withinheritedconstructor.metricorigin.MetricOrigin {\n',
44+
' unit: str {static}\n',
45+
'}\n',
46+
'class tests.modules.withinheritedconstructor.point.Origin {\n',
47+
' is_origin: bool {static}\n',
48+
'}\n',
49+
'class tests.modules.withinheritedconstructor.point.Point {\n',
50+
' x: float\n',
51+
' y: float\n',
52+
'}\n',
53+
'tests.modules.withinheritedconstructor.point.Origin <|-- tests.modules.withinheritedconstructor.metricorigin.MetricOrigin\n',
54+
'footer Generated by //py2puml//\n',
55+
'@enduml\n',
56+
]
57+
58+
puml_with_point_class_and_point_origin_relation_skipped = [
59+
'@startuml tests.modules.withinheritedconstructor\n!pragma useIntermediatePackages false\n\n',
60+
'class tests.modules.withinheritedconstructor.metricorigin.MetricOrigin {\n',
61+
' unit: str {static}\n',
62+
'}\n',
63+
'class tests.modules.withinheritedconstructor.point.Point {\n',
64+
' x: float\n',
65+
' y: float\n',
66+
'}\n',
67+
'tests.modules.withinheritedconstructor.point.Origin <|-- tests.modules.withinheritedconstructor.metricorigin.MetricOrigin\n',
68+
'footer Generated by //py2puml//\n',
69+
'@enduml\n',
70+
]
71+
72+
73+
def skip_origin_block(item: UmlItem) -> bool:
74+
return item.fqn.endswith('.Origin')
75+
76+
77+
def skip_point_origin_relation(relation: UmlRelation) -> bool:
78+
return relation.source_fqn.endswith('.Point') and relation.target_fqn.endswith('.Origin')
79+
80+
81+
def get_puml_content(filters: Filters) -> list[str]:
82+
return list(py2puml('tests/modules/withinheritedconstructor', 'tests.modules.withinheritedconstructor', filters))
83+
84+
85+
def invalid_filter_without_filter_argument():
86+
return True
87+
88+
89+
def invalid_filter_with_wrong_return_type(item: UmlItem) -> str:
90+
return 'True'
91+
92+
93+
def invalid_filter_with_exception(item: UmlItem) -> bool:
94+
raise Exception('An error occurred')
95+
96+
97+
non_callable_filter = 'not a function'
98+
99+
100+
def test_without_giving_filters():
101+
generated_puml = list(py2puml('tests/modules/withinheritedconstructor', 'tests.modules.withinheritedconstructor'))
102+
assert generated_puml == un_modified_puml
103+
104+
105+
def test_default_filters():
106+
filters = Filters()
107+
generated_puml = get_puml_content(filters)
108+
assert generated_puml == un_modified_puml
109+
110+
111+
def test_skip_origin_class():
112+
filters = Filters(skip_block=skip_origin_block)
113+
generated_puml = get_puml_content(filters)
114+
assert generated_puml == puml_with_origin_class_skipped
115+
116+
117+
def test_skip_point_origin_relation():
118+
filters = Filters(skip_relation=skip_point_origin_relation)
119+
generated_puml = get_puml_content(filters)
120+
assert generated_puml == puml_with_point_origin_relation_skipped
121+
122+
123+
def test_skip_point_class_and_point_origin_relation():
124+
filters = Filters(skip_block=skip_origin_block, skip_relation=skip_point_origin_relation)
125+
generated_puml = get_puml_content(filters)
126+
print(''.join(generated_puml))
127+
print(len(generated_puml), len(puml_with_point_class_and_point_origin_relation_skipped))
128+
assert generated_puml == puml_with_point_class_and_point_origin_relation_skipped
129+
130+
131+
@pytest.mark.parametrize(
132+
'invalid_filter',
133+
[
134+
invalid_filter_without_filter_argument,
135+
invalid_filter_with_wrong_return_type,
136+
invalid_filter_with_exception,
137+
non_callable_filter,
138+
],
139+
)
140+
def test_invalid_filters(invalid_filter):
141+
with pytest.raises(ValueError):
142+
filters = Filters(skip_block=invalid_filter)
143+
get_puml_content(filters)
144+
145+
with pytest.raises(ValueError):
146+
filters = Filters(skip_relation=invalid_filter)
147+
get_puml_content(filters)

0 commit comments

Comments
 (0)