Skip to content

Commit ed55e8f

Browse files
authored
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
1 parent e3b5edd commit ed55e8f

File tree

13 files changed

+288
-3
lines changed

13 files changed

+288
-3
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ repos:
66
rev: v4.0.1
77
hooks:
88
- id: trailing-whitespace
9-
exclude: "tests/functional/t/trailing_whitespaces.py"
9+
exclude: "tests/functional/t/trailing_whitespaces.py|tests/pyreverse/data/.*.html"
1010
- id: end-of-file-fixer
1111
exclude: "tests/functional/m/missing/missing_final_newline.py|tests/functional/t/trailing_newlines.py"
1212
- repo: https://github.com/myint/autoflake
@@ -94,3 +94,4 @@ repos:
9494
hooks:
9595
- id: prettier
9696
args: [--prose-wrap=always, --print-width=88]
97+
exclude: tests(/.*)*/data

ChangeLog

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ What's New in Pylint 2.13.0?
66
============================
77
Release date: TBA
88

9+
* Pyreverse - add output in mermaidjs format
10+
911
..
1012
Put new features here and also in 'doc/whatsnew/2.13.rst'
1113

doc/additional_commands/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Pyreverse
99
---------
1010

1111
``pyreverse`` analyzes your source code and generates package and class diagrams.
12-
It supports output to ``.dot``/``.gv``, ``.vcg`` and ``.puml``/``.plantuml`` (PlantUML) file formats.
12+
It supports output to ``.dot``/``.gv``, ``.vcg``, ``.puml``/``.plantuml`` (PlantUML) and ``.mmd``/``.html`` (MermaidJS) file formats.
1313
If Graphviz (or the ``dot`` command) is installed, all `output formats supported by Graphviz <https://graphviz.org/docs/outputs/>`_
1414
can be used as well. In this case, ``pyreverse`` first generates a temporary ``.gv`` file, which is then
1515
fed to Graphviz to generate the final image.

doc/whatsnew/2.13.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ Removed checkers
1717
Extensions
1818
==========
1919

20+
* Pyreverse - add output in mermaid-js format and html which is an mermaid js diagram with html boilerplate
21+
2022
Other Changes
2123
=============
2224

pylint/pyreverse/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,14 @@ def __init__(self, args: Iterable[str]):
206206
super().__init__(usage=__doc__)
207207
insert_default_options()
208208
args = self.load_command_line_configuration(args)
209-
if self.config.output_format not in ("dot", "vcg", "puml", "plantuml"):
209+
if self.config.output_format not in (
210+
"dot",
211+
"vcg",
212+
"puml",
213+
"plantuml",
214+
"mmd",
215+
"html",
216+
):
210217
check_graphviz_availability()
211218

212219
sys.exit(self.run(args))

pylint/pyreverse/mermaidjs_printer.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Copyright (c) 2021 Antonio Quarta <[email protected]>
2+
3+
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
4+
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
5+
6+
"""
7+
Class to generate files in mermaidjs format
8+
"""
9+
from typing import Dict, Optional
10+
11+
from pylint.pyreverse.printer import EdgeType, NodeProperties, NodeType, Printer
12+
from pylint.pyreverse.utils import get_annotation_label
13+
14+
15+
class MermaidJSPrinter(Printer):
16+
"""Printer for MermaidJS diagrams"""
17+
18+
DEFAULT_COLOR = "black"
19+
20+
NODES: Dict[NodeType, str] = {
21+
NodeType.CLASS: "class",
22+
NodeType.INTERFACE: "class",
23+
NodeType.PACKAGE: "class",
24+
}
25+
ARROWS: Dict[EdgeType, str] = {
26+
EdgeType.INHERITS: "--|>",
27+
EdgeType.IMPLEMENTS: "..|>",
28+
EdgeType.ASSOCIATION: "--*",
29+
EdgeType.USES: "-->",
30+
}
31+
32+
def _open_graph(self) -> None:
33+
"""Emit the header lines"""
34+
self.emit("classDiagram")
35+
self._inc_indent()
36+
37+
def emit_node(
38+
self,
39+
name: str,
40+
type_: NodeType,
41+
properties: Optional[NodeProperties] = None,
42+
) -> None:
43+
"""Create a new node. Nodes can be classes, packages, participants etc."""
44+
if properties is None:
45+
properties = NodeProperties(label=name)
46+
stereotype = "~~Interface~~" if type_ is NodeType.INTERFACE else ""
47+
nodetype = self.NODES[type_]
48+
body = []
49+
if properties.attrs:
50+
body.extend(properties.attrs)
51+
if properties.methods:
52+
for func in properties.methods:
53+
args = self._get_method_arguments(func)
54+
line = f"{func.name}({', '.join(args)})"
55+
if func.returns:
56+
line += " -> " + get_annotation_label(func.returns)
57+
body.append(line)
58+
name = name.split(".")[-1]
59+
self.emit(f"{nodetype} {name}{stereotype} {{")
60+
self._inc_indent()
61+
for line in body:
62+
self.emit(line)
63+
self._dec_indent()
64+
self.emit("}")
65+
66+
def emit_edge(
67+
self,
68+
from_node: str,
69+
to_node: str,
70+
type_: EdgeType,
71+
label: Optional[str] = None,
72+
) -> None:
73+
"""Create an edge from one node to another to display relationships."""
74+
from_node = from_node.split(".")[-1]
75+
to_node = to_node.split(".")[-1]
76+
edge = f"{from_node} {self.ARROWS[type_]} {to_node}"
77+
if label:
78+
edge += f" : {label}"
79+
self.emit(edge)
80+
81+
def _close_graph(self) -> None:
82+
"""Emit the lines needed to properly close the graph."""
83+
self._dec_indent()
84+
85+
86+
class HTMLMermaidJSPrinter(MermaidJSPrinter):
87+
"""Printer for MermaidJS diagrams wrapped in an html boilerplate"""
88+
89+
HTML_OPEN_BOILERPLATE = """<html>
90+
<body>
91+
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
92+
<div class="mermaid">
93+
"""
94+
HTML_CLOSE_BOILERPLATE = """
95+
</div>
96+
</body>
97+
</html>
98+
"""
99+
GRAPH_INDENT_LEVEL = 4
100+
101+
def _open_graph(self) -> None:
102+
self.emit(self.HTML_OPEN_BOILERPLATE)
103+
for _ in range(self.GRAPH_INDENT_LEVEL):
104+
self._inc_indent()
105+
super()._open_graph()
106+
107+
def _close_graph(self) -> None:
108+
for _ in range(self.GRAPH_INDENT_LEVEL):
109+
self._dec_indent()
110+
self.emit(self.HTML_CLOSE_BOILERPLATE)

pylint/pyreverse/printer_factory.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Dict, Type
99

1010
from pylint.pyreverse.dot_printer import DotPrinter
11+
from pylint.pyreverse.mermaidjs_printer import HTMLMermaidJSPrinter, MermaidJSPrinter
1112
from pylint.pyreverse.plantuml_printer import PlantUmlPrinter
1213
from pylint.pyreverse.printer import Printer
1314
from pylint.pyreverse.vcg_printer import VCGPrinter
@@ -16,6 +17,8 @@
1617
"vcg": VCGPrinter,
1718
"plantuml": PlantUmlPrinter,
1819
"puml": PlantUmlPrinter,
20+
"mmd": MermaidJSPrinter,
21+
"html": HTMLMermaidJSPrinter,
1922
"dot": DotPrinter,
2023
}
2124

tests/pyreverse/conftest.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,22 @@ def colorized_puml_config() -> PyreverseConfig:
4343
)
4444

4545

46+
@pytest.fixture()
47+
def mmd_config() -> PyreverseConfig:
48+
return PyreverseConfig(
49+
output_format="mmd",
50+
colorized=False,
51+
)
52+
53+
54+
@pytest.fixture()
55+
def html_config() -> PyreverseConfig:
56+
return PyreverseConfig(
57+
output_format="html",
58+
colorized=False,
59+
)
60+
61+
4662
@pytest.fixture(scope="session")
4763
def get_project() -> Callable:
4864
def _get_project(module: str, name: Optional[str] = "No Name") -> Project:
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<html>
2+
<body>
3+
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
4+
<div class="mermaid">
5+
6+
classDiagram
7+
class Ancestor {
8+
attr : str
9+
cls_member
10+
get_value()
11+
set_value(value)
12+
}
13+
class CustomException {
14+
}
15+
class DoNothing {
16+
}
17+
class DoNothing2 {
18+
}
19+
class DoSomething {
20+
my_int : Optional[int]
21+
my_int_2 : Optional[int]
22+
my_string : str
23+
do_it(new_int: int) -> int
24+
}
25+
class Interface {
26+
get_value()
27+
set_value(value)
28+
}
29+
class PropertyPatterns {
30+
prop1
31+
prop2
32+
}
33+
class Specialization {
34+
TYPE : str
35+
relation
36+
relation2
37+
top : str
38+
}
39+
Specialization --|> Ancestor
40+
Ancestor ..|> Interface
41+
DoNothing --* Ancestor : cls_member
42+
DoNothing --* Specialization : relation
43+
DoNothing2 --* Specialization : relation2
44+
45+
</div>
46+
</body>
47+
</html>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
classDiagram
2+
class Ancestor {
3+
attr : str
4+
cls_member
5+
get_value()
6+
set_value(value)
7+
}
8+
class CustomException {
9+
}
10+
class DoNothing {
11+
}
12+
class DoNothing2 {
13+
}
14+
class DoSomething {
15+
my_int : Optional[int]
16+
my_int_2 : Optional[int]
17+
my_string : str
18+
do_it(new_int: int) -> int
19+
}
20+
class Interface {
21+
get_value()
22+
set_value(value)
23+
}
24+
class PropertyPatterns {
25+
prop1
26+
prop2
27+
}
28+
class Specialization {
29+
TYPE : str
30+
relation
31+
relation2
32+
top : str
33+
}
34+
Specialization --|> Ancestor
35+
Ancestor ..|> Interface
36+
DoNothing --* Ancestor : cls_member
37+
DoNothing --* Specialization : relation
38+
DoNothing2 --* Specialization : relation2

0 commit comments

Comments
 (0)