third_party.pylibs.pylint.src/pylint/pyreverse/vcg_printer.py
2021-08-20 22:58:03 +02:00

290 lines
8.8 KiB
Python

# Copyright (c) 2015-2018, 2020 Claudiu Popa <pcmanticore@gmail.com>
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
# Copyright (c) 2020-2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
# Copyright (c) 2020 hippo91 <guillaume.peillex@gmail.com>
# Copyright (c) 2020 Ram Rachum <ram@rachum.com>
# Copyright (c) 2020 谭九鼎 <109224573@qq.com>
# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
# Copyright (c) 2021 Andreas Finkler <andi.finkler@gmail.com>
# Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.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
"""Functions to generate files readable with Georg Sander's vcg
(Visualization of Compiler Graphs).
You can download vcg at https://rw4.cs.uni-sb.de/~sander/html/gshome.html
Note that vcg exists as a debian package.
See vcg's documentation for explanation about the different values that
maybe used for the functions parameters.
"""
from typing import Any, Dict, Mapping, Optional
from pylint.pyreverse.printer import EdgeType, Layout, NodeProperties, NodeType, Printer
ATTRS_VAL = {
"algos": (
"dfs",
"tree",
"minbackward",
"left_to_right",
"right_to_left",
"top_to_bottom",
"bottom_to_top",
"maxdepth",
"maxdepthslow",
"mindepth",
"mindepthslow",
"mindegree",
"minindegree",
"minoutdegree",
"maxdegree",
"maxindegree",
"maxoutdegree",
),
"booleans": ("yes", "no"),
"colors": (
"black",
"white",
"blue",
"red",
"green",
"yellow",
"magenta",
"lightgrey",
"cyan",
"darkgrey",
"darkblue",
"darkred",
"darkgreen",
"darkyellow",
"darkmagenta",
"darkcyan",
"gold",
"lightblue",
"lightred",
"lightgreen",
"lightyellow",
"lightmagenta",
"lightcyan",
"lilac",
"turquoise",
"aquamarine",
"khaki",
"purple",
"yellowgreen",
"pink",
"orange",
"orchid",
),
"shapes": ("box", "ellipse", "rhomb", "triangle"),
"textmodes": ("center", "left_justify", "right_justify"),
"arrowstyles": ("solid", "line", "none"),
"linestyles": ("continuous", "dashed", "dotted", "invisible"),
}
# meaning of possible values:
# O -> string
# 1 -> int
# list -> value in list
GRAPH_ATTRS = {
"title": 0,
"label": 0,
"color": ATTRS_VAL["colors"],
"textcolor": ATTRS_VAL["colors"],
"bordercolor": ATTRS_VAL["colors"],
"width": 1,
"height": 1,
"borderwidth": 1,
"textmode": ATTRS_VAL["textmodes"],
"shape": ATTRS_VAL["shapes"],
"shrink": 1,
"stretch": 1,
"orientation": ATTRS_VAL["algos"],
"vertical_order": 1,
"horizontal_order": 1,
"xspace": 1,
"yspace": 1,
"layoutalgorithm": ATTRS_VAL["algos"],
"late_edge_labels": ATTRS_VAL["booleans"],
"display_edge_labels": ATTRS_VAL["booleans"],
"dirty_edge_labels": ATTRS_VAL["booleans"],
"finetuning": ATTRS_VAL["booleans"],
"manhattan_edges": ATTRS_VAL["booleans"],
"smanhattan_edges": ATTRS_VAL["booleans"],
"port_sharing": ATTRS_VAL["booleans"],
"edges": ATTRS_VAL["booleans"],
"nodes": ATTRS_VAL["booleans"],
"splines": ATTRS_VAL["booleans"],
}
NODE_ATTRS = {
"title": 0,
"label": 0,
"color": ATTRS_VAL["colors"],
"textcolor": ATTRS_VAL["colors"],
"bordercolor": ATTRS_VAL["colors"],
"width": 1,
"height": 1,
"borderwidth": 1,
"textmode": ATTRS_VAL["textmodes"],
"shape": ATTRS_VAL["shapes"],
"shrink": 1,
"stretch": 1,
"vertical_order": 1,
"horizontal_order": 1,
}
EDGE_ATTRS = {
"sourcename": 0,
"targetname": 0,
"label": 0,
"linestyle": ATTRS_VAL["linestyles"],
"class": 1,
"thickness": 0,
"color": ATTRS_VAL["colors"],
"textcolor": ATTRS_VAL["colors"],
"arrowcolor": ATTRS_VAL["colors"],
"backarrowcolor": ATTRS_VAL["colors"],
"arrowsize": 1,
"backarrowsize": 1,
"arrowstyle": ATTRS_VAL["arrowstyles"],
"backarrowstyle": ATTRS_VAL["arrowstyles"],
"textmode": ATTRS_VAL["textmodes"],
"priority": 1,
"anchor": 1,
"horizontal_order": 1,
}
SHAPES: Dict[NodeType, str] = {
NodeType.PACKAGE: "box",
NodeType.CLASS: "box",
NodeType.INTERFACE: "ellipse",
}
ARROWS: Dict[EdgeType, Dict] = {
EdgeType.USES: dict(arrowstyle="solid", backarrowstyle="none", backarrowsize=0),
EdgeType.INHERITS: dict(
arrowstyle="solid", backarrowstyle="none", backarrowsize=10
),
EdgeType.IMPLEMENTS: dict(
arrowstyle="solid",
backarrowstyle="none",
linestyle="dotted",
backarrowsize=10,
),
EdgeType.ASSOCIATION: dict(
arrowstyle="solid", backarrowstyle="none", textcolor="green"
),
}
ORIENTATION: Dict[Layout, str] = {
Layout.LEFT_TO_RIGHT: "left_to_right",
Layout.RIGHT_TO_LEFT: "right_to_left",
Layout.TOP_TO_BOTTOM: "top_to_bottom",
Layout.BOTTOM_TO_TOP: "bottom_to_top",
}
# Misc utilities ###############################################################
class VCGPrinter(Printer):
def _open_graph(self) -> None:
"""Emit the header lines"""
self.emit("graph:{\n")
self._inc_indent()
self._write_attributes(
GRAPH_ATTRS,
title=self.title,
layoutalgorithm="dfs",
late_edge_labels="yes",
port_sharing="no",
manhattan_edges="yes",
)
if self.layout:
self._write_attributes(GRAPH_ATTRS, orientation=ORIENTATION[self.layout])
def _close_graph(self) -> None:
"""Emit the lines needed to properly close the graph."""
self._dec_indent()
self.emit("}")
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)
elif properties.label is None:
properties.label = name
self.emit(f'node: {{title:"{name}"', force_newline=False)
self._write_attributes(
NODE_ATTRS,
label=self._build_label_for_node(properties),
shape=SHAPES[type_],
)
self.emit("}")
@staticmethod
def _build_label_for_node(properties: NodeProperties) -> str:
fontcolor = "\f09" if properties.fontcolor == "red" else ""
label = rf"\fb{fontcolor}{properties.label}\fn"
if properties.attrs is None and properties.methods is None:
# return a compact form which only displays the classname in a box
return label
attrs = properties.attrs or []
methods = properties.methods or []
method_names = [func.name for func in methods]
# box width for UML like diagram
maxlen = max(len(name) for name in [properties.label] + method_names + attrs)
line = "_" * (maxlen + 2)
label = fr"{label}\n\f{line}"
for attr in attrs:
label = fr"{label}\n\f08{attr}"
if attrs:
label = fr"{label}\n\f{line}"
for func in method_names:
label = fr"{label}\n\f10{func}()"
return label
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."""
self.emit(
f'edge: {{sourcename:"{from_node}" targetname:"{to_node}"',
force_newline=False,
)
attributes = ARROWS[type_]
if label:
attributes["label"] = label
self._write_attributes(
EDGE_ATTRS,
**attributes,
)
self.emit("}")
def _write_attributes(self, attributes_dict: Mapping[str, Any], **args) -> None:
"""write graph, node or edge attributes"""
for key, value in args.items():
try:
_type = attributes_dict[key]
except KeyError as e:
raise Exception(
f"no such attribute {key}\npossible attributes are {attributes_dict.keys()}"
) from e
if not _type:
self.emit(f'{key}:"{value}"\n')
elif _type == 1:
self.emit(f"{key}:{int(value)}\n")
elif value in _type:
self.emit(f"{key}:{value}\n")
else:
raise Exception(
f"value {value} isn't correct for attribute {key} correct values are {type}"
)