summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntonio Quarta <sgheppy88@gmail.com>2021-12-03 18:33:04 +0100
committerGitHub <noreply@github.com>2021-12-03 18:33:04 +0100
commited55e8f66407f1b84a386446372f12feee3230c7 (patch)
treef45257b4e567ff645acf5625d3379faad4d29a44
parente3b5edd37828b4ed6322050e9d562a17b0276102 (diff)
downloadpylint-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.yaml3
-rw-r--r--ChangeLog2
-rw-r--r--doc/additional_commands/index.rst2
-rw-r--r--doc/whatsnew/2.13.rst2
-rw-r--r--pylint/pyreverse/main.py9
-rw-r--r--pylint/pyreverse/mermaidjs_printer.py110
-rw-r--r--pylint/pyreverse/printer_factory.py3
-rw-r--r--tests/pyreverse/conftest.py16
-rw-r--r--tests/pyreverse/data/classes_No_Name.html47
-rw-r--r--tests/pyreverse/data/classes_No_Name.mmd38
-rw-r--r--tests/pyreverse/data/packages_No_Name.html19
-rw-r--r--tests/pyreverse/data/packages_No_Name.mmd10
-rw-r--r--tests/pyreverse/test_writer.py30
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
diff --git a/ChangeLog b/ChangeLog
index 4dd010abb..c55294e16 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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: