graph.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. # Copyright (c) 2015-2018, 2020 Claudiu Popa <pcmanticore@gmail.com>
  2. # Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
  3. # Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
  4. # Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
  5. # Copyright (c) 2019, 2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
  6. # Copyright (c) 2019 Nick Drozd <nicholasdrozd@gmail.com>
  7. # Copyright (c) 2020 hippo91 <guillaume.peillex@gmail.com>
  8. # Copyright (c) 2020 Damien Baty <damien.baty@polyconseil.fr>
  9. # Copyright (c) 2020 谭九鼎 <109224573@qq.com>
  10. # Copyright (c) 2020 Benjamin Graham <benwilliamgraham@gmail.com>
  11. # Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
  12. # Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
  13. # Copyright (c) 2021 Andreas Finkler <andi.finkler@gmail.com>
  14. # Copyright (c) 2021 Andrew Howe <howeaj@users.noreply.github.com>
  15. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  16. # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
  17. """Graph manipulation utilities.
  18. (dot generation adapted from pypy/translator/tool/make_dot.py)
  19. """
  20. import codecs
  21. import os
  22. import shutil
  23. import subprocess
  24. import sys
  25. import tempfile
  26. from typing import Optional
  27. def target_info_from_filename(filename):
  28. """Transforms /some/path/foo.png into ('/some/path', 'foo.png', 'png')."""
  29. basename = os.path.basename(filename)
  30. storedir = os.path.dirname(os.path.abspath(filename))
  31. target = os.path.splitext(filename)[-1][1:]
  32. return storedir, basename, target
  33. class DotBackend:
  34. """Dot File backend."""
  35. def __init__(
  36. self,
  37. graphname,
  38. rankdir=None,
  39. size=None,
  40. ratio=None,
  41. charset="utf-8",
  42. renderer="dot",
  43. additional_param=None,
  44. ):
  45. if additional_param is None:
  46. additional_param = {}
  47. self.graphname = graphname
  48. self.renderer = renderer
  49. self.lines = []
  50. self._source = None
  51. self.emit(f"digraph {normalize_node_id(graphname)} {{")
  52. if rankdir:
  53. self.emit(f"rankdir={rankdir}")
  54. if ratio:
  55. self.emit(f"ratio={ratio}")
  56. if size:
  57. self.emit(f'size="{size}"')
  58. if charset:
  59. assert charset.lower() in {
  60. "utf-8",
  61. "iso-8859-1",
  62. "latin1",
  63. }, f"unsupported charset {charset}"
  64. self.emit(f'charset="{charset}"')
  65. for param in additional_param.items():
  66. self.emit("=".join(param))
  67. def get_source(self):
  68. """returns self._source"""
  69. if self._source is None:
  70. self.emit("}\n")
  71. self._source = "\n".join(self.lines)
  72. del self.lines
  73. return self._source
  74. source = property(get_source)
  75. def generate(
  76. self, outputfile: Optional[str] = None, mapfile: Optional[str] = None
  77. ) -> str:
  78. """Generates a graph file.
  79. :param str outputfile: filename and path [defaults to graphname.png]
  80. :param str mapfile: filename and path
  81. :rtype: str
  82. :return: a path to the generated file
  83. :raises RuntimeError: if the executable for rendering was not found
  84. """
  85. graphviz_extensions = ("dot", "gv")
  86. name = self.graphname
  87. if outputfile is None:
  88. target = "png"
  89. pdot, dot_sourcepath = tempfile.mkstemp(".gv", name)
  90. ppng, outputfile = tempfile.mkstemp(".png", name)
  91. os.close(pdot)
  92. os.close(ppng)
  93. else:
  94. _, _, target = target_info_from_filename(outputfile)
  95. if not target:
  96. target = "png"
  97. outputfile = outputfile + "." + target
  98. if target not in graphviz_extensions:
  99. pdot, dot_sourcepath = tempfile.mkstemp(".gv", name)
  100. os.close(pdot)
  101. else:
  102. dot_sourcepath = outputfile
  103. with codecs.open(dot_sourcepath, "w", encoding="utf8") as file:
  104. file.write(self.source)
  105. if target not in graphviz_extensions:
  106. if shutil.which(self.renderer) is None:
  107. raise RuntimeError(
  108. f"Cannot generate `{outputfile}` because '{self.renderer}' "
  109. "executable not found. Install graphviz, or specify a `.gv` "
  110. "outputfile to produce the DOT source code."
  111. )
  112. use_shell = sys.platform == "win32"
  113. if mapfile:
  114. subprocess.call(
  115. [
  116. self.renderer,
  117. "-Tcmapx",
  118. "-o",
  119. mapfile,
  120. "-T",
  121. target,
  122. dot_sourcepath,
  123. "-o",
  124. outputfile,
  125. ],
  126. shell=use_shell,
  127. )
  128. else:
  129. subprocess.call(
  130. [self.renderer, "-T", target, dot_sourcepath, "-o", outputfile],
  131. shell=use_shell,
  132. )
  133. os.unlink(dot_sourcepath)
  134. return outputfile
  135. def emit(self, line):
  136. """Adds <line> to final output."""
  137. self.lines.append(line)
  138. def emit_edge(self, name1, name2, **props):
  139. """emit an edge from <name1> to <name2>.
  140. edge properties: see https://www.graphviz.org/doc/info/attrs.html
  141. """
  142. attrs = [f'{prop}="{value}"' for prop, value in props.items()]
  143. n_from, n_to = normalize_node_id(name1), normalize_node_id(name2)
  144. self.emit(f"{n_from} -> {n_to} [{', '.join(sorted(attrs))}];")
  145. def emit_node(self, name, **props):
  146. """emit a node with given properties.
  147. node properties: see https://www.graphviz.org/doc/info/attrs.html
  148. """
  149. attrs = [f'{prop}="{value}"' for prop, value in props.items()]
  150. self.emit(f"{normalize_node_id(name)} [{', '.join(sorted(attrs))}];")
  151. def normalize_node_id(nid):
  152. """Returns a suitable DOT node id for `nid`."""
  153. return f'"{nid}"'
  154. def get_cycles(graph_dict, vertices=None):
  155. """given a dictionary representing an ordered graph (i.e. key are vertices
  156. and values is a list of destination vertices representing edges), return a
  157. list of detected cycles
  158. """
  159. if not graph_dict:
  160. return ()
  161. result = []
  162. if vertices is None:
  163. vertices = graph_dict.keys()
  164. for vertice in vertices:
  165. _get_cycles(graph_dict, [], set(), result, vertice)
  166. return result
  167. def _get_cycles(graph_dict, path, visited, result, vertice):
  168. """recursive function doing the real work for get_cycles"""
  169. if vertice in path:
  170. cycle = [vertice]
  171. for node in path[::-1]:
  172. if node == vertice:
  173. break
  174. cycle.insert(0, node)
  175. # make a canonical representation
  176. start_from = min(cycle)
  177. index = cycle.index(start_from)
  178. cycle = cycle[index:] + cycle[0:index]
  179. # append it to result if not already in
  180. if cycle not in result:
  181. result.append(cycle)
  182. return
  183. path.append(vertice)
  184. try:
  185. for node in graph_dict[vertice]:
  186. # don't check already visited nodes again
  187. if node not in visited:
  188. _get_cycles(graph_dict, path, visited, result, node)
  189. visited.add(node)
  190. except KeyError:
  191. pass
  192. path.pop()