# Copyright (c) 2015-2018, 2020 Claudiu Popa # Copyright (c) 2015 Florian Bruhin # Copyright (c) 2018 ssolanki # Copyright (c) 2020-2021 Pierre Sassoulas # Copyright (c) 2020 hippo91 # Copyright (c) 2020 Ram Rachum # Copyright (c) 2020 谭九鼎 <109224573@qq.com> # Copyright (c) 2020 Anthony Sottile # Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com> # Copyright (c) 2021 Andreas Finkler # 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 = fr"\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}" )