From 739740b5a44c5b246fea0219a929914b055b40d9 Mon Sep 17 00:00:00 2001 From: vignesh14052002 Date: Sat, 27 Jul 2024 15:20:49 +0530 Subject: [PATCH 1/4] Expose controls on which block, method, relation can be included in diagram --- py2puml/export/puml.py | 21 ++++++++++++++++++++- py2puml/py2puml.py | 4 ++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/py2puml/export/puml.py b/py2puml/export/puml.py index 638cbd5..97ed6b7 100644 --- a/py2puml/export/puml.py +++ b/py2puml/export/puml.py @@ -26,11 +26,15 @@ FEATURE_INSTANCE = '' -def to_puml_content(diagram_name: str, uml_items: List[UmlItem], uml_relations: List[UmlRelation]) -> Iterable[str]: +def to_puml_content( + diagram_name: str, uml_items: List[UmlItem], uml_relations: List[UmlRelation], **kwargs +) -> Iterable[str]: yield PUML_FILE_START.format(diagram_name=diagram_name) # exports the domain classes and enums for uml_item in uml_items: + if 'is_block_valid' in kwargs and callable(kwargs['is_block_valid']) and not kwargs['is_block_valid'](uml_item): + continue if isinstance(uml_item, UmlEnum): uml_enum: UmlEnum = uml_item yield PUML_ITEM_START_TPL.format(item_type='enum', item_fqn=uml_enum.fqn) @@ -48,12 +52,27 @@ def to_puml_content(diagram_name: str, uml_items: List[UmlItem], uml_relations: attr_type=uml_attr.type, staticity=FEATURE_STATIC if uml_attr.static else FEATURE_INSTANCE, ) + for uml_method in uml_class.methods: + if ( + 'is_method_valid' in kwargs + and callable(kwargs['is_method_valid']) + and not kwargs['is_method_valid'](uml_method) + ): + continue + + yield f' {uml_method.represent_as_puml()}\n' yield PUML_ITEM_END else: raise TypeError(f'cannot process uml_item of type {uml_item.__class__}') # exports the domain relationships between classes and enums for uml_relation in uml_relations: + if ( + 'is_relation_valid' in kwargs + and callable(kwargs['is_relation_valid']) + and not kwargs['is_relation_valid'](uml_relation) + ): + continue yield PUML_RELATION_TPL.format( source_fqn=uml_relation.source_fqn, rel_type=uml_relation.type.value, target_fqn=uml_relation.target_fqn ) diff --git a/py2puml/py2puml.py b/py2puml/py2puml.py index 8300beb..29a584c 100644 --- a/py2puml/py2puml.py +++ b/py2puml/py2puml.py @@ -6,9 +6,9 @@ from py2puml.inspection.inspectpackage import inspect_package -def py2puml(domain_path: str, domain_module: str) -> Iterable[str]: +def py2puml(domain_path: str, domain_module: str, **kwargs) -> Iterable[str]: domain_items_by_fqn: Dict[str, UmlItem] = {} domain_relations: List[UmlRelation] = [] inspect_package(domain_path, domain_module, domain_items_by_fqn, domain_relations) - return to_puml_content(domain_module, domain_items_by_fqn.values(), domain_relations) + return to_puml_content(domain_module, domain_items_by_fqn.values(), domain_relations, **kwargs) From 938632441467145e2d96baa5c207f83d6ef98157 Mon Sep 17 00:00:00 2001 From: vignesh-arivazhagan Date: Sun, 28 Jul 2024 22:15:11 +0530 Subject: [PATCH 2/4] use Filters instead of kwargs --- py2puml/export/__init__.py | 0 py2puml/export/puml.py | 48 ++++++++++++++++++++++++-------------- py2puml/py2puml.py | 8 +++---- tests/modules/__init__.py | 0 4 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 py2puml/export/__init__.py create mode 100644 tests/modules/__init__.py diff --git a/py2puml/export/__init__.py b/py2puml/export/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/py2puml/export/puml.py b/py2puml/export/puml.py index 97ed6b7..f2567f7 100644 --- a/py2puml/export/puml.py +++ b/py2puml/export/puml.py @@ -1,4 +1,5 @@ -from typing import Iterable, List +from dataclasses import dataclass +from typing import Callable, Iterable, List, Optional from py2puml.domain.umlclass import UmlClass from py2puml.domain.umlenum import UmlEnum @@ -26,14 +27,39 @@ FEATURE_INSTANCE = '' +@dataclass +class Filters: + skip_block: Optional[Callable[[UmlItem], bool]] = None + skip_relation: Optional[Callable[[UmlRelation], bool]] = None + + +def should_skip(filter: Callable | None, item: UmlItem | UmlRelation) -> bool: + if filter is None: + return False + + if not callable(filter): + raise ValueError('Filter must be a callable') + + try: + _should_skip = filter(item) + if not isinstance(_should_skip, bool): + raise ValueError('Filter must return a boolean value') + return _should_skip + except Exception as e: + raise ValueError('Error while applying filter') from e + + def to_puml_content( - diagram_name: str, uml_items: List[UmlItem], uml_relations: List[UmlRelation], **kwargs + diagram_name: str, uml_items: List[UmlItem], uml_relations: List[UmlRelation], filters: Optional[Filters] = None ) -> Iterable[str]: + if filters is None: + filters = Filters() + yield PUML_FILE_START.format(diagram_name=diagram_name) # exports the domain classes and enums for uml_item in uml_items: - if 'is_block_valid' in kwargs and callable(kwargs['is_block_valid']) and not kwargs['is_block_valid'](uml_item): + if should_skip(filters.skip_block, uml_item): continue if isinstance(uml_item, UmlEnum): uml_enum: UmlEnum = uml_item @@ -52,26 +78,14 @@ def to_puml_content( attr_type=uml_attr.type, staticity=FEATURE_STATIC if uml_attr.static else FEATURE_INSTANCE, ) - for uml_method in uml_class.methods: - if ( - 'is_method_valid' in kwargs - and callable(kwargs['is_method_valid']) - and not kwargs['is_method_valid'](uml_method) - ): - continue - - yield f' {uml_method.represent_as_puml()}\n' + # TODO: Add skip_method filter here once PR #43 is merged yield PUML_ITEM_END else: raise TypeError(f'cannot process uml_item of type {uml_item.__class__}') # exports the domain relationships between classes and enums for uml_relation in uml_relations: - if ( - 'is_relation_valid' in kwargs - and callable(kwargs['is_relation_valid']) - and not kwargs['is_relation_valid'](uml_relation) - ): + if should_skip(filters.skip_relation, uml_relation): continue yield PUML_RELATION_TPL.format( source_fqn=uml_relation.source_fqn, rel_type=uml_relation.type.value, target_fqn=uml_relation.target_fqn diff --git a/py2puml/py2puml.py b/py2puml/py2puml.py index 29a584c..b5518c1 100644 --- a/py2puml/py2puml.py +++ b/py2puml/py2puml.py @@ -1,14 +1,14 @@ -from typing import Dict, Iterable, List +from typing import Dict, Iterable, List, Optional from py2puml.domain.umlitem import UmlItem from py2puml.domain.umlrelation import UmlRelation -from py2puml.export.puml import to_puml_content +from py2puml.export.puml import Filters, to_puml_content from py2puml.inspection.inspectpackage import inspect_package -def py2puml(domain_path: str, domain_module: str, **kwargs) -> Iterable[str]: +def py2puml(domain_path: str, domain_module: str, filters: Optional[Filters] = None) -> Iterable[str]: domain_items_by_fqn: Dict[str, UmlItem] = {} domain_relations: List[UmlRelation] = [] inspect_package(domain_path, domain_module, domain_items_by_fqn, domain_relations) - return to_puml_content(domain_module, domain_items_by_fqn.values(), domain_relations, **kwargs) + return to_puml_content(domain_module, domain_items_by_fqn.values(), domain_relations, filters) diff --git a/tests/modules/__init__.py b/tests/modules/__init__.py new file mode 100644 index 0000000..e69de29 From dbb6142a9353be0d8175bba5002aae2a9073d23a Mon Sep 17 00:00:00 2001 From: vignesh-arivazhagan Date: Sun, 28 Jul 2024 22:24:07 +0530 Subject: [PATCH 3/4] Add tests --- tests/py2puml/export/test_filters.py | 147 +++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tests/py2puml/export/test_filters.py diff --git a/tests/py2puml/export/test_filters.py b/tests/py2puml/export/test_filters.py new file mode 100644 index 0000000..39d34f4 --- /dev/null +++ b/tests/py2puml/export/test_filters.py @@ -0,0 +1,147 @@ +import pytest + +from py2puml.domain.umlitem import UmlItem +from py2puml.domain.umlrelation import UmlRelation +from py2puml.export.puml import Filters +from py2puml.py2puml import py2puml + +un_modified_puml = [ + '@startuml tests.modules.withinheritedconstructor\n!pragma useIntermediatePackages false\n\n', + 'class tests.modules.withinheritedconstructor.metricorigin.MetricOrigin {\n', + ' unit: str {static}\n', + '}\n', + 'class tests.modules.withinheritedconstructor.point.Origin {\n', + ' is_origin: bool {static}\n', + '}\n', + 'class tests.modules.withinheritedconstructor.point.Point {\n', + ' x: float\n', + ' y: float\n', + '}\n', + 'tests.modules.withinheritedconstructor.point.Origin <|-- tests.modules.withinheritedconstructor.metricorigin.MetricOrigin\n', + 'tests.modules.withinheritedconstructor.point.Point <|-- tests.modules.withinheritedconstructor.point.Origin\n', + 'footer Generated by //py2puml//\n', + '@enduml\n', +] + +puml_with_origin_class_skipped = [ + '@startuml tests.modules.withinheritedconstructor\n!pragma useIntermediatePackages false\n\n', + 'class tests.modules.withinheritedconstructor.metricorigin.MetricOrigin {\n', + ' unit: str {static}\n', + '}\n', + 'class tests.modules.withinheritedconstructor.point.Point {\n', + ' x: float\n', + ' y: float\n', + '}\n', + 'tests.modules.withinheritedconstructor.point.Origin <|-- tests.modules.withinheritedconstructor.metricorigin.MetricOrigin\n', + 'tests.modules.withinheritedconstructor.point.Point <|-- tests.modules.withinheritedconstructor.point.Origin\n', + 'footer Generated by //py2puml//\n', + '@enduml\n', +] + +puml_with_point_origin_relation_skipped = [ + '@startuml tests.modules.withinheritedconstructor\n!pragma useIntermediatePackages false\n\n', + 'class tests.modules.withinheritedconstructor.metricorigin.MetricOrigin {\n', + ' unit: str {static}\n', + '}\n', + 'class tests.modules.withinheritedconstructor.point.Origin {\n', + ' is_origin: bool {static}\n', + '}\n', + 'class tests.modules.withinheritedconstructor.point.Point {\n', + ' x: float\n', + ' y: float\n', + '}\n', + 'tests.modules.withinheritedconstructor.point.Origin <|-- tests.modules.withinheritedconstructor.metricorigin.MetricOrigin\n', + 'footer Generated by //py2puml//\n', + '@enduml\n', +] + +puml_with_point_class_and_point_origin_relation_skipped = [ + '@startuml tests.modules.withinheritedconstructor\n!pragma useIntermediatePackages false\n\n', + 'class tests.modules.withinheritedconstructor.metricorigin.MetricOrigin {\n', + ' unit: str {static}\n', + '}\n', + 'class tests.modules.withinheritedconstructor.point.Point {\n', + ' x: float\n', + ' y: float\n', + '}\n', + 'tests.modules.withinheritedconstructor.point.Origin <|-- tests.modules.withinheritedconstructor.metricorigin.MetricOrigin\n', + 'footer Generated by //py2puml//\n', + '@enduml\n', +] + + +def skip_origin_block(item: UmlItem) -> bool: + return item.fqn.endswith('.Origin') + + +def skip_point_origin_relation(relation: UmlRelation) -> bool: + return relation.source_fqn.endswith('.Point') and relation.target_fqn.endswith('.Origin') + + +def get_puml_content(filters: Filters) -> list[str]: + return list(py2puml('tests/modules/withinheritedconstructor', 'tests.modules.withinheritedconstructor', filters)) + + +def invalid_filter_without_filter_argument(): + return True + + +def invalid_filter_with_wrong_return_type(item: UmlItem) -> str: + return 'True' + + +def invalid_filter_with_exception(item: UmlItem) -> bool: + raise Exception('An error occurred') + + +non_callable_filter = 'not a function' + + +def test_without_giving_filters(): + generated_puml = list(py2puml('tests/modules/withinheritedconstructor', 'tests.modules.withinheritedconstructor')) + assert generated_puml == un_modified_puml + + +def test_default_filters(): + filters = Filters() + generated_puml = get_puml_content(filters) + assert generated_puml == un_modified_puml + + +def test_skip_origin_class(): + filters = Filters(skip_block=skip_origin_block) + generated_puml = get_puml_content(filters) + assert generated_puml == puml_with_origin_class_skipped + + +def test_skip_point_origin_relation(): + filters = Filters(skip_relation=skip_point_origin_relation) + generated_puml = get_puml_content(filters) + assert generated_puml == puml_with_point_origin_relation_skipped + + +def test_skip_point_class_and_point_origin_relation(): + filters = Filters(skip_block=skip_origin_block, skip_relation=skip_point_origin_relation) + generated_puml = get_puml_content(filters) + print(''.join(generated_puml)) + print(len(generated_puml), len(puml_with_point_class_and_point_origin_relation_skipped)) + assert generated_puml == puml_with_point_class_and_point_origin_relation_skipped + + +@pytest.mark.parametrize( + 'invalid_filter', + [ + invalid_filter_without_filter_argument, + invalid_filter_with_wrong_return_type, + invalid_filter_with_exception, + non_callable_filter, + ], +) +def test_invalid_filters(invalid_filter): + with pytest.raises(ValueError): + filters = Filters(skip_block=invalid_filter) + get_puml_content(filters) + + with pytest.raises(ValueError): + filters = Filters(skip_relation=invalid_filter) + get_puml_content(filters) From 786d8acced0286d63ab11f3f08b331bf622f7dd5 Mon Sep 17 00:00:00 2001 From: vignesh-arivazhagan Date: Sun, 28 Jul 2024 22:24:47 +0530 Subject: [PATCH 4/4] Add filter in readme --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 48966b3..09157b2 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,30 @@ if __name__ == '__main__': * running it outputs the previous PlantUML diagram in the terminal and writes it in a file. +### Additionally you can also pass filters to skip specific blocks and relations +```python +from py2puml.domain.umlrelation import UmlRelation +from py2puml.domain.umlclass import UmlMethod +from py2puml.domain.umlitem import UmlItem +from py2puml.export.puml import Filters +from py2puml.py2puml import py2puml + +def skip_block(item: UmlItem) -> bool: + return item.fqn.endswith('') + +def skip_relation(relation: UmlRelation) -> bool: + return relation.source_fqn.endswith('') and relation.target_fqn.endswith('') + +filters = Filters(skip_block, skip_relation) + +puml_content = "".join( + py2puml( + 'py2puml/domain', + 'py2puml.domain', + filters + ) + ) +``` # Tests