diff options
author | Antonio Quarta <sgheppy88@gmail.com> | 2021-12-03 18:33:04 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-03 18:33:04 +0100 |
commit | ed55e8f66407f1b84a386446372f12feee3230c7 (patch) | |
tree | f45257b4e567ff645acf5625d3379faad4d29a44 | |
parent | e3b5edd37828b4ed6322050e9d562a17b0276102 (diff) | |
download | pylint-git-ed55e8f66407f1b84a386446372f12feee3230c7.tar.gz |
Add mermaidjs as format output for pyreverse (#5272)
add mermaid js printer, fix accepted output format without graphviz
Make an adapter for package graph, use class until mermaid don't
add a package diagram type. Add mmd and html formats to additional commands
-rw-r--r-- | .pre-commit-config.yaml | 3 | ||||
-rw-r--r-- | ChangeLog | 2 | ||||
-rw-r--r-- | doc/additional_commands/index.rst | 2 | ||||
-rw-r--r-- | doc/whatsnew/2.13.rst | 2 | ||||
-rw-r--r-- | pylint/pyreverse/main.py | 9 | ||||
-rw-r--r-- | pylint/pyreverse/mermaidjs_printer.py | 110 | ||||
-rw-r--r-- | pylint/pyreverse/printer_factory.py | 3 | ||||
-rw-r--r-- | tests/pyreverse/conftest.py | 16 | ||||
-rw-r--r-- | tests/pyreverse/data/classes_No_Name.html | 47 | ||||
-rw-r--r-- | tests/pyreverse/data/classes_No_Name.mmd | 38 | ||||
-rw-r--r-- | tests/pyreverse/data/packages_No_Name.html | 19 | ||||
-rw-r--r-- | tests/pyreverse/data/packages_No_Name.mmd | 10 | ||||
-rw-r--r-- | tests/pyreverse/test_writer.py | 30 |
13 files changed, 288 insertions, 3 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e692843b..429c04b33 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: rev: v4.0.1 hooks: - id: trailing-whitespace - exclude: "tests/functional/t/trailing_whitespaces.py" + exclude: "tests/functional/t/trailing_whitespaces.py|tests/pyreverse/data/.*.html" - id: end-of-file-fixer exclude: "tests/functional/m/missing/missing_final_newline.py|tests/functional/t/trailing_newlines.py" - repo: https://github.com/myint/autoflake @@ -94,3 +94,4 @@ repos: hooks: - id: prettier args: [--prose-wrap=always, --print-width=88] + exclude: tests(/.*)*/data @@ -6,6 +6,8 @@ What's New in Pylint 2.13.0? ============================ Release date: TBA +* Pyreverse - add output in mermaidjs format + .. Put new features here and also in 'doc/whatsnew/2.13.rst' diff --git a/doc/additional_commands/index.rst b/doc/additional_commands/index.rst index e4c92cab3..b8ebb15cb 100644 --- a/doc/additional_commands/index.rst +++ b/doc/additional_commands/index.rst @@ -9,7 +9,7 @@ Pyreverse --------- ``pyreverse`` analyzes your source code and generates package and class diagrams. -It supports output to ``.dot``/``.gv``, ``.vcg`` and ``.puml``/``.plantuml`` (PlantUML) file formats. +It supports output to ``.dot``/``.gv``, ``.vcg``, ``.puml``/``.plantuml`` (PlantUML) and ``.mmd``/``.html`` (MermaidJS) file formats. If Graphviz (or the ``dot`` command) is installed, all `output formats supported by Graphviz <https://graphviz.org/docs/outputs/>`_ can be used as well. In this case, ``pyreverse`` first generates a temporary ``.gv`` file, which is then fed to Graphviz to generate the final image. diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index 378b4103b..60099b67e 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -17,6 +17,8 @@ Removed checkers Extensions ========== +* Pyreverse - add output in mermaid-js format and html which is an mermaid js diagram with html boilerplate + Other Changes ============= diff --git a/pylint/pyreverse/main.py b/pylint/pyreverse/main.py index fc302149a..c48b9f3c3 100644 --- a/pylint/pyreverse/main.py +++ b/pylint/pyreverse/main.py @@ -206,7 +206,14 @@ class Run(ConfigurationMixIn): super().__init__(usage=__doc__) insert_default_options() args = self.load_command_line_configuration(args) - if self.config.output_format not in ("dot", "vcg", "puml", "plantuml"): + if self.config.output_format not in ( + "dot", + "vcg", + "puml", + "plantuml", + "mmd", + "html", + ): check_graphviz_availability() sys.exit(self.run(args)) diff --git a/pylint/pyreverse/mermaidjs_printer.py b/pylint/pyreverse/mermaidjs_printer.py new file mode 100644 index 000000000..c8214ab8e --- /dev/null +++ b/pylint/pyreverse/mermaidjs_printer.py @@ -0,0 +1,110 @@ +# Copyright (c) 2021 Antonio Quarta <andi.finkler@gmail.com> + +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE + +""" +Class to generate files in mermaidjs format +""" +from typing import Dict, Optional + +from pylint.pyreverse.printer import EdgeType, NodeProperties, NodeType, Printer +from pylint.pyreverse.utils import get_annotation_label + + +class MermaidJSPrinter(Printer): + """Printer for MermaidJS diagrams""" + + DEFAULT_COLOR = "black" + + NODES: Dict[NodeType, str] = { + NodeType.CLASS: "class", + NodeType.INTERFACE: "class", + NodeType.PACKAGE: "class", + } + ARROWS: Dict[EdgeType, str] = { + EdgeType.INHERITS: "--|>", + EdgeType.IMPLEMENTS: "..|>", + EdgeType.ASSOCIATION: "--*", + EdgeType.USES: "-->", + } + + def _open_graph(self) -> None: + """Emit the header lines""" + self.emit("classDiagram") + self._inc_indent() + + def emit_node( + self, + name: str, + type_: NodeType, + properties: Optional[NodeProperties] = None, + ) -> None: + """Create a new node. Nodes can be classes, packages, participants etc.""" + if properties is None: + properties = NodeProperties(label=name) + stereotype = "~~Interface~~" if type_ is NodeType.INTERFACE else "" + nodetype = self.NODES[type_] + body = [] + if properties.attrs: + body.extend(properties.attrs) + if properties.methods: + for func in properties.methods: + args = self._get_method_arguments(func) + line = f"{func.name}({', '.join(args)})" + if func.returns: + line += " -> " + get_annotation_label(func.returns) + body.append(line) + name = name.split(".")[-1] + self.emit(f"{nodetype} {name}{stereotype} {{") + self._inc_indent() + for line in body: + self.emit(line) + self._dec_indent() + self.emit("}") + + def emit_edge( + self, + from_node: str, + to_node: str, + type_: EdgeType, + label: Optional[str] = None, + ) -> None: + """Create an edge from one node to another to display relationships.""" + from_node = from_node.split(".")[-1] + to_node = to_node.split(".")[-1] + edge = f"{from_node} {self.ARROWS[type_]} {to_node}" + if label: + edge += f" : {label}" + self.emit(edge) + + def _close_graph(self) -> None: + """Emit the lines needed to properly close the graph.""" + self._dec_indent() + + +class HTMLMermaidJSPrinter(MermaidJSPrinter): + """Printer for MermaidJS diagrams wrapped in an html boilerplate""" + + HTML_OPEN_BOILERPLATE = """<html> + <body> + <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script> + <div class="mermaid"> + """ + HTML_CLOSE_BOILERPLATE = """ + </div> + </body> +</html> +""" + GRAPH_INDENT_LEVEL = 4 + + def _open_graph(self) -> None: + self.emit(self.HTML_OPEN_BOILERPLATE) + for _ in range(self.GRAPH_INDENT_LEVEL): + self._inc_indent() + super()._open_graph() + + def _close_graph(self) -> None: + for _ in range(self.GRAPH_INDENT_LEVEL): + self._dec_indent() + self.emit(self.HTML_CLOSE_BOILERPLATE) diff --git a/pylint/pyreverse/printer_factory.py b/pylint/pyreverse/printer_factory.py index 73200c35d..bdcaf0869 100644 --- a/pylint/pyreverse/printer_factory.py +++ b/pylint/pyreverse/printer_factory.py @@ -8,6 +8,7 @@ from typing import Dict, Type from pylint.pyreverse.dot_printer import DotPrinter +from pylint.pyreverse.mermaidjs_printer import HTMLMermaidJSPrinter, MermaidJSPrinter from pylint.pyreverse.plantuml_printer import PlantUmlPrinter from pylint.pyreverse.printer import Printer from pylint.pyreverse.vcg_printer import VCGPrinter @@ -16,6 +17,8 @@ filetype_to_printer: Dict[str, Type[Printer]] = { "vcg": VCGPrinter, "plantuml": PlantUmlPrinter, "puml": PlantUmlPrinter, + "mmd": MermaidJSPrinter, + "html": HTMLMermaidJSPrinter, "dot": DotPrinter, } diff --git a/tests/pyreverse/conftest.py b/tests/pyreverse/conftest.py index 9536fbcb0..c83e74cbd 100644 --- a/tests/pyreverse/conftest.py +++ b/tests/pyreverse/conftest.py @@ -43,6 +43,22 @@ def colorized_puml_config() -> PyreverseConfig: ) +@pytest.fixture() +def mmd_config() -> PyreverseConfig: + return PyreverseConfig( + output_format="mmd", + colorized=False, + ) + + +@pytest.fixture() +def html_config() -> PyreverseConfig: + return PyreverseConfig( + output_format="html", + colorized=False, + ) + + @pytest.fixture(scope="session") def get_project() -> Callable: def _get_project(module: str, name: Optional[str] = "No Name") -> Project: diff --git a/tests/pyreverse/data/classes_No_Name.html b/tests/pyreverse/data/classes_No_Name.html new file mode 100644 index 000000000..3f81c340e --- /dev/null +++ b/tests/pyreverse/data/classes_No_Name.html @@ -0,0 +1,47 @@ +<html> + <body> + <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script> + <div class="mermaid"> + + classDiagram + class Ancestor { + attr : str + cls_member + get_value() + set_value(value) + } + class CustomException { + } + class DoNothing { + } + class DoNothing2 { + } + class DoSomething { + my_int : Optional[int] + my_int_2 : Optional[int] + my_string : str + do_it(new_int: int) -> int + } + class Interface { + get_value() + set_value(value) + } + class PropertyPatterns { + prop1 + prop2 + } + class Specialization { + TYPE : str + relation + relation2 + top : str + } + Specialization --|> Ancestor + Ancestor ..|> Interface + DoNothing --* Ancestor : cls_member + DoNothing --* Specialization : relation + DoNothing2 --* Specialization : relation2 + + </div> + </body> +</html> diff --git a/tests/pyreverse/data/classes_No_Name.mmd b/tests/pyreverse/data/classes_No_Name.mmd new file mode 100644 index 000000000..d2ac9839d --- /dev/null +++ b/tests/pyreverse/data/classes_No_Name.mmd @@ -0,0 +1,38 @@ +classDiagram + class Ancestor { + attr : str + cls_member + get_value() + set_value(value) + } + class CustomException { + } + class DoNothing { + } + class DoNothing2 { + } + class DoSomething { + my_int : Optional[int] + my_int_2 : Optional[int] + my_string : str + do_it(new_int: int) -> int + } + class Interface { + get_value() + set_value(value) + } + class PropertyPatterns { + prop1 + prop2 + } + class Specialization { + TYPE : str + relation + relation2 + top : str + } + Specialization --|> Ancestor + Ancestor ..|> Interface + DoNothing --* Ancestor : cls_member + DoNothing --* Specialization : relation + DoNothing2 --* Specialization : relation2 diff --git a/tests/pyreverse/data/packages_No_Name.html b/tests/pyreverse/data/packages_No_Name.html new file mode 100644 index 000000000..128f8d1a4 --- /dev/null +++ b/tests/pyreverse/data/packages_No_Name.html @@ -0,0 +1,19 @@ +<html> + <body> + <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script> + <div class="mermaid"> + + classDiagram + class data { + } + class clientmodule_test { + } + class property_pattern { + } + class suppliermodule_test { + } + clientmodule_test --> suppliermodule_test + + </div> + </body> +</html> diff --git a/tests/pyreverse/data/packages_No_Name.mmd b/tests/pyreverse/data/packages_No_Name.mmd new file mode 100644 index 000000000..e8b02d070 --- /dev/null +++ b/tests/pyreverse/data/packages_No_Name.mmd @@ -0,0 +1,10 @@ +classDiagram + class data { + } + class clientmodule_test { + } + class property_pattern { + } + class suppliermodule_test { + } + clientmodule_test --> suppliermodule_test diff --git a/tests/pyreverse/test_writer.py b/tests/pyreverse/test_writer.py index 142ba04a0..bbfd36411 100644 --- a/tests/pyreverse/test_writer.py +++ b/tests/pyreverse/test_writer.py @@ -58,6 +58,8 @@ COLORIZED_DOT_FILES = ["packages_colorized.dot", "classes_colorized.dot"] VCG_FILES = ["packages_No_Name.vcg", "classes_No_Name.vcg"] PUML_FILES = ["packages_No_Name.puml", "classes_No_Name.puml"] COLORIZED_PUML_FILES = ["packages_colorized.puml", "classes_colorized.puml"] +MMD_FILES = ["packages_No_Name.mmd", "classes_No_Name.mmd"] +HTML_FILES = ["packages_No_Name.html", "classes_No_Name.html"] class Config: @@ -121,6 +123,22 @@ def setup_colorized_puml( yield from _setup(project, colorized_puml_config, writer) +@pytest.fixture() +def setup_mmd(mmd_config: PyreverseConfig, get_project: Callable) -> Iterator: + writer = DiagramWriter(mmd_config) + + project = get_project(TEST_DATA_DIR) + yield from _setup(project, mmd_config, writer) + + +@pytest.fixture() +def setup_html(html_config: PyreverseConfig, get_project: Callable) -> Iterator: + writer = DiagramWriter(html_config) + + project = get_project(TEST_DATA_DIR) + yield from _setup(project, html_config, writer) + + def _setup( project: Project, config: PyreverseConfig, writer: DiagramWriter ) -> Iterator: @@ -164,6 +182,18 @@ def test_puml_files(generated_file: str) -> None: _assert_files_are_equal(generated_file) +@pytest.mark.usefixtures("setup_mmd") +@pytest.mark.parametrize("generated_file", MMD_FILES) +def test_mmd_files(generated_file: str) -> None: + _assert_files_are_equal(generated_file) + + +@pytest.mark.usefixtures("setup_html") +@pytest.mark.parametrize("generated_file", HTML_FILES) +def test_html_files(generated_file: str) -> None: + _assert_files_are_equal(generated_file) + + @pytest.mark.usefixtures("setup_colorized_puml") @pytest.mark.parametrize("generated_file", COLORIZED_PUML_FILES) def test_colorized_puml_files(generated_file: str) -> None: |