vcg_printer.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
  4. """Functions to generate files readable with George Sander's vcg
  5. (Visualization of Compiler Graphs).
  6. You can download vcg at https://rw4.cs.uni-sb.de/~sander/html/gshome.html
  7. Note that vcg exists as a debian package.
  8. See vcg's documentation for explanation about the different values that
  9. maybe used for the functions parameters.
  10. """
  11. from __future__ import annotations
  12. from collections.abc import Mapping
  13. from typing import Any
  14. from pylint.pyreverse.printer import EdgeType, Layout, NodeProperties, NodeType, Printer
  15. ATTRS_VAL = {
  16. "algos": (
  17. "dfs",
  18. "tree",
  19. "minbackward",
  20. "left_to_right",
  21. "right_to_left",
  22. "top_to_bottom",
  23. "bottom_to_top",
  24. "maxdepth",
  25. "maxdepthslow",
  26. "mindepth",
  27. "mindepthslow",
  28. "mindegree",
  29. "minindegree",
  30. "minoutdegree",
  31. "maxdegree",
  32. "maxindegree",
  33. "maxoutdegree",
  34. ),
  35. "booleans": ("yes", "no"),
  36. "colors": (
  37. "black",
  38. "white",
  39. "blue",
  40. "red",
  41. "green",
  42. "yellow",
  43. "magenta",
  44. "lightgrey",
  45. "cyan",
  46. "darkgrey",
  47. "darkblue",
  48. "darkred",
  49. "darkgreen",
  50. "darkyellow",
  51. "darkmagenta",
  52. "darkcyan",
  53. "gold",
  54. "lightblue",
  55. "lightred",
  56. "lightgreen",
  57. "lightyellow",
  58. "lightmagenta",
  59. "lightcyan",
  60. "lilac",
  61. "turquoise",
  62. "aquamarine",
  63. "khaki",
  64. "purple",
  65. "yellowgreen",
  66. "pink",
  67. "orange",
  68. "orchid",
  69. ),
  70. "shapes": ("box", "ellipse", "rhomb", "triangle"),
  71. "textmodes": ("center", "left_justify", "right_justify"),
  72. "arrowstyles": ("solid", "line", "none"),
  73. "linestyles": ("continuous", "dashed", "dotted", "invisible"),
  74. }
  75. # meaning of possible values:
  76. # O -> string
  77. # 1 -> int
  78. # list -> value in list
  79. GRAPH_ATTRS = {
  80. "title": 0,
  81. "label": 0,
  82. "color": ATTRS_VAL["colors"],
  83. "textcolor": ATTRS_VAL["colors"],
  84. "bordercolor": ATTRS_VAL["colors"],
  85. "width": 1,
  86. "height": 1,
  87. "borderwidth": 1,
  88. "textmode": ATTRS_VAL["textmodes"],
  89. "shape": ATTRS_VAL["shapes"],
  90. "shrink": 1,
  91. "stretch": 1,
  92. "orientation": ATTRS_VAL["algos"],
  93. "vertical_order": 1,
  94. "horizontal_order": 1,
  95. "xspace": 1,
  96. "yspace": 1,
  97. "layoutalgorithm": ATTRS_VAL["algos"],
  98. "late_edge_labels": ATTRS_VAL["booleans"],
  99. "display_edge_labels": ATTRS_VAL["booleans"],
  100. "dirty_edge_labels": ATTRS_VAL["booleans"],
  101. "finetuning": ATTRS_VAL["booleans"],
  102. "manhattan_edges": ATTRS_VAL["booleans"],
  103. "smanhattan_edges": ATTRS_VAL["booleans"],
  104. "port_sharing": ATTRS_VAL["booleans"],
  105. "edges": ATTRS_VAL["booleans"],
  106. "nodes": ATTRS_VAL["booleans"],
  107. "splines": ATTRS_VAL["booleans"],
  108. }
  109. NODE_ATTRS = {
  110. "title": 0,
  111. "label": 0,
  112. "color": ATTRS_VAL["colors"],
  113. "textcolor": ATTRS_VAL["colors"],
  114. "bordercolor": ATTRS_VAL["colors"],
  115. "width": 1,
  116. "height": 1,
  117. "borderwidth": 1,
  118. "textmode": ATTRS_VAL["textmodes"],
  119. "shape": ATTRS_VAL["shapes"],
  120. "shrink": 1,
  121. "stretch": 1,
  122. "vertical_order": 1,
  123. "horizontal_order": 1,
  124. }
  125. EDGE_ATTRS = {
  126. "sourcename": 0,
  127. "targetname": 0,
  128. "label": 0,
  129. "linestyle": ATTRS_VAL["linestyles"],
  130. "class": 1,
  131. "thickness": 0,
  132. "color": ATTRS_VAL["colors"],
  133. "textcolor": ATTRS_VAL["colors"],
  134. "arrowcolor": ATTRS_VAL["colors"],
  135. "backarrowcolor": ATTRS_VAL["colors"],
  136. "arrowsize": 1,
  137. "backarrowsize": 1,
  138. "arrowstyle": ATTRS_VAL["arrowstyles"],
  139. "backarrowstyle": ATTRS_VAL["arrowstyles"],
  140. "textmode": ATTRS_VAL["textmodes"],
  141. "priority": 1,
  142. "anchor": 1,
  143. "horizontal_order": 1,
  144. }
  145. SHAPES: dict[NodeType, str] = {
  146. NodeType.PACKAGE: "box",
  147. NodeType.CLASS: "box",
  148. NodeType.INTERFACE: "ellipse",
  149. }
  150. # pylint: disable-next=consider-using-namedtuple-or-dataclass
  151. ARROWS: dict[EdgeType, dict[str, str | int]] = {
  152. EdgeType.USES: {
  153. "arrowstyle": "solid",
  154. "backarrowstyle": "none",
  155. "backarrowsize": 0,
  156. },
  157. EdgeType.INHERITS: {
  158. "arrowstyle": "solid",
  159. "backarrowstyle": "none",
  160. "backarrowsize": 10,
  161. },
  162. EdgeType.IMPLEMENTS: {
  163. "arrowstyle": "solid",
  164. "backarrowstyle": "none",
  165. "linestyle": "dotted",
  166. "backarrowsize": 10,
  167. },
  168. EdgeType.ASSOCIATION: {
  169. "arrowstyle": "solid",
  170. "backarrowstyle": "none",
  171. "textcolor": "green",
  172. },
  173. EdgeType.AGGREGATION: {
  174. "arrowstyle": "solid",
  175. "backarrowstyle": "none",
  176. "textcolor": "green",
  177. },
  178. }
  179. ORIENTATION: dict[Layout, str] = {
  180. Layout.LEFT_TO_RIGHT: "left_to_right",
  181. Layout.RIGHT_TO_LEFT: "right_to_left",
  182. Layout.TOP_TO_BOTTOM: "top_to_bottom",
  183. Layout.BOTTOM_TO_TOP: "bottom_to_top",
  184. }
  185. # Misc utilities ###############################################################
  186. class VCGPrinter(Printer):
  187. def _open_graph(self) -> None:
  188. """Emit the header lines."""
  189. self.emit("graph:{\n")
  190. self._inc_indent()
  191. self._write_attributes(
  192. GRAPH_ATTRS,
  193. title=self.title,
  194. layoutalgorithm="dfs",
  195. late_edge_labels="yes",
  196. port_sharing="no",
  197. manhattan_edges="yes",
  198. )
  199. if self.layout:
  200. self._write_attributes(GRAPH_ATTRS, orientation=ORIENTATION[self.layout])
  201. def _close_graph(self) -> None:
  202. """Emit the lines needed to properly close the graph."""
  203. self._dec_indent()
  204. self.emit("}")
  205. def emit_node(
  206. self,
  207. name: str,
  208. type_: NodeType,
  209. properties: NodeProperties | None = None,
  210. ) -> None:
  211. """Create a new node.
  212. Nodes can be classes, packages, participants etc.
  213. """
  214. if properties is None:
  215. properties = NodeProperties(label=name)
  216. elif properties.label is None:
  217. properties.label = name
  218. self.emit(f'node: {{title:"{name}"', force_newline=False)
  219. self._write_attributes(
  220. NODE_ATTRS,
  221. label=self._build_label_for_node(properties),
  222. shape=SHAPES[type_],
  223. )
  224. self.emit("}")
  225. @staticmethod
  226. def _build_label_for_node(properties: NodeProperties) -> str:
  227. fontcolor = "\f09" if properties.fontcolor == "red" else ""
  228. label = rf"\fb{fontcolor}{properties.label}\fn"
  229. if properties.attrs is None and properties.methods is None:
  230. # return a compact form which only displays the classname in a box
  231. return label
  232. attrs = properties.attrs or []
  233. methods = properties.methods or []
  234. method_names = [func.name for func in methods]
  235. # box width for UML like diagram
  236. maxlen = max(len(name) for name in [properties.label] + method_names + attrs)
  237. line = "_" * (maxlen + 2)
  238. label = rf"{label}\n\f{line}"
  239. for attr in attrs:
  240. label = rf"{label}\n\f08{attr}"
  241. if attrs:
  242. label = rf"{label}\n\f{line}"
  243. for func in method_names:
  244. label = rf"{label}\n\f10{func}()"
  245. return label
  246. def emit_edge(
  247. self,
  248. from_node: str,
  249. to_node: str,
  250. type_: EdgeType,
  251. label: str | None = None,
  252. ) -> None:
  253. """Create an edge from one node to another to display relationships."""
  254. self.emit(
  255. f'edge: {{sourcename:"{from_node}" targetname:"{to_node}"',
  256. force_newline=False,
  257. )
  258. attributes = ARROWS[type_]
  259. if label:
  260. attributes["label"] = label
  261. self._write_attributes(
  262. EDGE_ATTRS,
  263. **attributes,
  264. )
  265. self.emit("}")
  266. def _write_attributes(
  267. self, attributes_dict: Mapping[str, Any], **args: Any
  268. ) -> None:
  269. """Write graph, node or edge attributes."""
  270. for key, value in args.items():
  271. try:
  272. _type = attributes_dict[key]
  273. except KeyError as e:
  274. raise AttributeError(
  275. f"no such attribute {key}\npossible attributes are {attributes_dict.keys()}"
  276. ) from e
  277. if not _type:
  278. self.emit(f'{key}:"{value}"\n')
  279. elif _type == 1:
  280. self.emit(f"{key}:{int(value)}\n")
  281. elif value in _type:
  282. self.emit(f"{key}:{value}\n")
  283. else:
  284. raise ValueError(
  285. f"value {value} isn't correct for attribute {key} correct values are {type}"
  286. )