-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
pyreverse
: add PlantUML output
#4846
Changes from 11 commits
c89fcb5
e913c4c
c2f5a18
f42135b
659470d
09808ba
cebb458
b0044e1
f2cf518
f86a28b
9426ba5
af26527
c1c0b61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,10 +27,8 @@ | |
from pylint.config import ConfigurationMixIn | ||
from pylint.pyreverse import writer | ||
from pylint.pyreverse.diadefslib import DiadefsHandler | ||
from pylint.pyreverse.dot_printer import DotPrinter | ||
from pylint.pyreverse.inspector import Linker, project_from_files | ||
from pylint.pyreverse.utils import check_graphviz_availability, insert_default_options | ||
from pylint.pyreverse.vcg_printer import VCGPrinter | ||
|
||
OPTIONS = ( | ||
( | ||
|
@@ -209,8 +207,7 @@ def run(self, args): | |
diadefs = handler.get_diadefs(project, linker) | ||
finally: | ||
sys.path.pop(0) | ||
printer_class = VCGPrinter if self.config.output_format == "vcg" else DotPrinter | ||
writer.DiagramWriter(self.config, printer_class).write(diadefs) | ||
writer.DiagramWriter(self.config).write(diadefs) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I introduced a factory function to get the printer class for a given filetype. This supports the Open-Closed-Principle (only factory function needs to be updated if a new printer is added). Also, the |
||
return 0 | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
# Copyright (c) 2021 Andreas Finkler <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 dot format and image formats supported by Graphviz. | ||
""" | ||
from typing import Dict, Optional | ||
|
||
from pylint.pyreverse.printer import EdgeType, Layout, NodeProperties, NodeType, Printer | ||
from pylint.pyreverse.utils import get_annotation_label | ||
|
||
|
||
class PlantUmlPrinter(Printer): | ||
"""Printer for PlantUML diagrams""" | ||
|
||
DEFAULT_COLOR = "black" | ||
|
||
NODES: Dict[NodeType, str] = { | ||
NodeType.CLASS: "class", | ||
NodeType.INTERFACE: "class", | ||
NodeType.PACKAGE: "package", | ||
} | ||
ARROWS: Dict[EdgeType, str] = { | ||
EdgeType.INHERITS: "--|>", | ||
EdgeType.IMPLEMENTS: "..|>", | ||
EdgeType.ASSOCIATION: "--*", | ||
EdgeType.USES: "-->", | ||
} | ||
|
||
def _open_graph(self) -> None: | ||
"""Emit the header lines""" | ||
self.emit("@startuml " + self.title) | ||
if not self.use_automatic_namespace: | ||
self.emit("set namespaceSeparator none") | ||
if self.layout: | ||
if self.layout is Layout.LEFT_TO_RIGHT: | ||
self.emit("left to right direction") | ||
elif self.layout is Layout.TOP_TO_BOTTOM: | ||
self.emit("top to bottom direction") | ||
else: | ||
raise ValueError( | ||
f"Unsupported layout {self.layout}. PlantUmlPrinter only supports left to right and top to bottom layout." | ||
) | ||
|
||
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_] | ||
color = ( | ||
f" #{properties.color}" if properties.color != self.DEFAULT_COLOR else "" | ||
) | ||
body = "" | ||
if properties.attrs: | ||
body += "\n".join(properties.attrs) | ||
if properties.methods: | ||
body += "\n" | ||
for func in properties.methods: | ||
args = self._get_method_arguments(func) | ||
body += f"\n{func.name}({', '.join(args)})" | ||
if func.returns: | ||
body += " -> " + get_annotation_label(func.returns) | ||
label = properties.label if properties.label is not None else name | ||
if properties.fontcolor and properties.fontcolor != self.DEFAULT_COLOR: | ||
label = f"<color:{properties.fontcolor}>{label}</color>" | ||
self.emit(f'{nodetype} "{label}" as {name}{stereotype}{color} {{\n{body}\n}}') | ||
|
||
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.""" | ||
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.emit("@enduml") |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,8 @@ | |
|
||
import astroid | ||
|
||
from pylint.pyreverse.utils import get_annotation_label | ||
|
||
|
||
class NodeType(Enum): | ||
CLASS = "class" | ||
|
@@ -84,6 +86,28 @@ def emit_edge( | |
) -> None: | ||
"""Create an edge from one node to another to display relationships.""" | ||
|
||
@staticmethod | ||
def _get_method_arguments(method: astroid.FunctionDef) -> List[str]: | ||
if method.args.args: | ||
arguments: List[astroid.AssignName] = [ | ||
arg for arg in method.args.args if arg.name != "self" | ||
] | ||
else: | ||
arguments = [] | ||
|
||
annotations = dict(zip(arguments, method.args.annotations[1:])) | ||
for arg in arguments: | ||
annotation_label = "" | ||
ann = annotations.get(arg) | ||
if ann: | ||
annotation_label = get_annotation_label(ann) | ||
annotations[arg] = annotation_label | ||
|
||
return [ | ||
f"{arg.name}: {ann}" if ann else f"{arg.name}" | ||
for arg, ann in annotations.items() | ||
] | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is used by both |
||
def generate(self, outputfile: str) -> None: | ||
"""Generate and save the final outputfile.""" | ||
self._close_graph() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# Copyright (c) 2021 Andreas Finkler <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 | ||
|
||
from typing import Type | ||
|
||
from pylint.pyreverse.dot_printer import DotPrinter | ||
from pylint.pyreverse.plantuml_printer import PlantUmlPrinter | ||
from pylint.pyreverse.printer import Printer | ||
from pylint.pyreverse.vcg_printer import VCGPrinter | ||
|
||
filetype_to_printer = {"vcg": VCGPrinter, "puml": PlantUmlPrinter, "dot": DotPrinter} | ||
DudeNr33 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
def get_printer_for_filetype(filetype: str) -> Type[Printer]: | ||
return filetype_to_printer.get(filetype, DotPrinter) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,3 +10,11 @@ def set_value(self, value): | |
class DoNothing: pass | ||
|
||
class DoNothing2: pass | ||
|
||
class DoSomething: | ||
def __init__(self, a_string: str, optional_int: int = None): | ||
self.my_string = a_string | ||
self.my_int = optional_int | ||
|
||
def do_it(self, new_int: int) -> int: | ||
return self.my_int + new_int | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This new class is for checking that #4551 works correctly with PlantUML output too. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
@startuml classes_No_Name | ||
set namespaceSeparator none | ||
class "Ancestor" as data.clientmodule_test.Ancestor { | ||
attr : str | ||
cls_member | ||
|
||
get_value() | ||
set_value(value) | ||
} | ||
class "DoNothing" as data.suppliermodule_test.DoNothing { | ||
|
||
} | ||
class "DoNothing2" as data.suppliermodule_test.DoNothing2 { | ||
|
||
} | ||
class "DoSomething" as data.suppliermodule_test.DoSomething { | ||
my_int : Optional[int] | ||
my_string : str | ||
|
||
do_it(new_int: int) -> int | ||
} | ||
class "Interface" as data.suppliermodule_test.Interface { | ||
|
||
|
||
get_value() | ||
set_value(value) | ||
} | ||
class "Specialization" as data.clientmodule_test.Specialization { | ||
TYPE : str | ||
relation | ||
relation2 | ||
top : str | ||
} | ||
data.clientmodule_test.Specialization --|> data.clientmodule_test.Ancestor | ||
data.clientmodule_test.Ancestor ..|> data.suppliermodule_test.Interface | ||
data.suppliermodule_test.DoNothing --* data.clientmodule_test.Ancestor : cls_member | ||
data.suppliermodule_test.DoNothing --* data.clientmodule_test.Specialization : relation | ||
data.suppliermodule_test.DoNothing2 --* data.clientmodule_test.Specialization : relation2 | ||
@enduml |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
@startuml packages_No_Name | ||
set namespaceSeparator none | ||
package "data" as data { | ||
|
||
} | ||
package "data.clientmodule_test" as data.clientmodule_test { | ||
|
||
} | ||
package "data.suppliermodule_test" as data.suppliermodule_test { | ||
|
||
} | ||
data.clientmodule_test --> data.suppliermodule_test | ||
@enduml |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same logic is used by
PlantUmlPrinter
, so I extracted a method and put it inside the basePrinter
class.