plantuml_printer.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  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. """Class to generate files in dot format and image formats supported by Graphviz."""
  5. from __future__ import annotations
  6. from pylint.pyreverse.printer import EdgeType, Layout, NodeProperties, NodeType, Printer
  7. from pylint.pyreverse.utils import get_annotation_label
  8. class PlantUmlPrinter(Printer):
  9. """Printer for PlantUML diagrams."""
  10. DEFAULT_COLOR = "black"
  11. NODES: dict[NodeType, str] = {
  12. NodeType.CLASS: "class",
  13. NodeType.INTERFACE: "class",
  14. NodeType.PACKAGE: "package",
  15. }
  16. ARROWS: dict[EdgeType, str] = {
  17. EdgeType.INHERITS: "--|>",
  18. EdgeType.IMPLEMENTS: "..|>",
  19. EdgeType.ASSOCIATION: "--*",
  20. EdgeType.AGGREGATION: "--o",
  21. EdgeType.USES: "-->",
  22. }
  23. def _open_graph(self) -> None:
  24. """Emit the header lines."""
  25. self.emit("@startuml " + self.title)
  26. if not self.use_automatic_namespace:
  27. self.emit("set namespaceSeparator none")
  28. if self.layout:
  29. if self.layout is Layout.LEFT_TO_RIGHT:
  30. self.emit("left to right direction")
  31. elif self.layout is Layout.TOP_TO_BOTTOM:
  32. self.emit("top to bottom direction")
  33. else:
  34. raise ValueError(
  35. f"Unsupported layout {self.layout}. PlantUmlPrinter only "
  36. "supports left to right and top to bottom layout."
  37. )
  38. def emit_node(
  39. self,
  40. name: str,
  41. type_: NodeType,
  42. properties: NodeProperties | None = None,
  43. ) -> None:
  44. """Create a new node.
  45. Nodes can be classes, packages, participants etc.
  46. """
  47. if properties is None:
  48. properties = NodeProperties(label=name)
  49. stereotype = " << interface >>" if type_ is NodeType.INTERFACE else ""
  50. nodetype = self.NODES[type_]
  51. if properties.color and properties.color != self.DEFAULT_COLOR:
  52. color = f" #{properties.color}"
  53. else:
  54. color = ""
  55. body = []
  56. if properties.attrs:
  57. body.extend(properties.attrs)
  58. if properties.methods:
  59. for func in properties.methods:
  60. args = self._get_method_arguments(func)
  61. line = "{abstract}" if func.is_abstract() else ""
  62. line += f"{func.name}({', '.join(args)})"
  63. if func.returns:
  64. line += " -> " + get_annotation_label(func.returns)
  65. body.append(line)
  66. label = properties.label if properties.label is not None else name
  67. if properties.fontcolor and properties.fontcolor != self.DEFAULT_COLOR:
  68. label = f"<color:{properties.fontcolor}>{label}</color>"
  69. self.emit(f'{nodetype} "{label}" as {name}{stereotype}{color} {{')
  70. self._inc_indent()
  71. for line in body:
  72. self.emit(line)
  73. self._dec_indent()
  74. self.emit("}")
  75. def emit_edge(
  76. self,
  77. from_node: str,
  78. to_node: str,
  79. type_: EdgeType,
  80. label: str | None = None,
  81. ) -> None:
  82. """Create an edge from one node to another to display relationships."""
  83. edge = f"{from_node} {self.ARROWS[type_]} {to_node}"
  84. if label:
  85. edge += f" : {label}"
  86. self.emit(edge)
  87. def _close_graph(self) -> None:
  88. """Emit the lines needed to properly close the graph."""
  89. self.emit("@enduml")