_visualize.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. from __future__ import annotations
  2. import argparse
  3. import sys
  4. from functools import wraps
  5. from typing import Callable, Iterator
  6. import graphviz
  7. from ._core import Automaton, Input, Output, State
  8. from ._discover import findMachines
  9. from ._methodical import MethodicalMachine
  10. from ._typed import TypeMachine, InputProtocol, Core
  11. def _gvquote(s: str) -> str:
  12. return '"{}"'.format(s.replace('"', r"\""))
  13. def _gvhtml(s: str) -> str:
  14. return "<{}>".format(s)
  15. def elementMaker(name: str, *children: str, **attrs: str) -> str:
  16. """
  17. Construct a string from the HTML element description.
  18. """
  19. formattedAttrs = " ".join(
  20. "{}={}".format(key, _gvquote(str(value)))
  21. for key, value in sorted(attrs.items())
  22. )
  23. formattedChildren = "".join(children)
  24. return "<{name} {attrs}>{children}</{name}>".format(
  25. name=name, attrs=formattedAttrs, children=formattedChildren
  26. )
  27. def tableMaker(
  28. inputLabel: str,
  29. outputLabels: list[str],
  30. port: str,
  31. _E: Callable[..., str] = elementMaker,
  32. ) -> str:
  33. """
  34. Construct an HTML table to label a state transition.
  35. """
  36. colspan = {}
  37. if outputLabels:
  38. colspan["colspan"] = str(len(outputLabels))
  39. inputLabelCell = _E(
  40. "td",
  41. _E("font", inputLabel, face="menlo-italic"),
  42. color="purple",
  43. port=port,
  44. **colspan,
  45. )
  46. pointSize = {"point-size": "9"}
  47. outputLabelCells = [
  48. _E("td", _E("font", outputLabel, **pointSize), color="pink")
  49. for outputLabel in outputLabels
  50. ]
  51. rows = [_E("tr", inputLabelCell)]
  52. if outputLabels:
  53. rows.append(_E("tr", *outputLabelCells))
  54. return _E("table", *rows)
  55. def escapify(x: Callable[[State], str]) -> Callable[[State], str]:
  56. @wraps(x)
  57. def impl(t: State) -> str:
  58. return x(t).replace("<", "&lt;").replace(">", "&gt;")
  59. return impl
  60. def makeDigraph(
  61. automaton: Automaton[State, Input, Output],
  62. inputAsString: Callable[[Input], str] = repr,
  63. outputAsString: Callable[[Output], str] = repr,
  64. stateAsString: Callable[[State], str] = repr,
  65. ) -> graphviz.Digraph:
  66. """
  67. Produce a L{graphviz.Digraph} object from an automaton.
  68. """
  69. inputAsString = escapify(inputAsString)
  70. outputAsString = escapify(outputAsString)
  71. stateAsString = escapify(stateAsString)
  72. digraph = graphviz.Digraph(
  73. graph_attr={"pack": "true", "dpi": "100"},
  74. node_attr={"fontname": "Menlo"},
  75. edge_attr={"fontname": "Menlo"},
  76. )
  77. for state in automaton.states():
  78. if state is automaton.initialState:
  79. stateShape = "bold"
  80. fontName = "Menlo-Bold"
  81. else:
  82. stateShape = ""
  83. fontName = "Menlo"
  84. digraph.node(
  85. stateAsString(state),
  86. fontame=fontName,
  87. shape="ellipse",
  88. style=stateShape,
  89. color="blue",
  90. )
  91. for n, eachTransition in enumerate(automaton.allTransitions()):
  92. inState, inputSymbol, outState, outputSymbols = eachTransition
  93. thisTransition = "t{}".format(n)
  94. inputLabel = inputAsString(inputSymbol)
  95. port = "tableport"
  96. table = tableMaker(
  97. inputLabel,
  98. [outputAsString(outputSymbol) for outputSymbol in outputSymbols],
  99. port=port,
  100. )
  101. digraph.node(thisTransition, label=_gvhtml(table), margin="0.2", shape="none")
  102. digraph.edge(
  103. stateAsString(inState),
  104. "{}:{}:w".format(thisTransition, port),
  105. arrowhead="none",
  106. )
  107. digraph.edge("{}:{}:e".format(thisTransition, port), stateAsString(outState))
  108. return digraph
  109. def tool(
  110. _progname: str = sys.argv[0],
  111. _argv: list[str] = sys.argv[1:],
  112. _syspath: list[str] = sys.path,
  113. _findMachines: Callable[
  114. [str],
  115. Iterator[tuple[str, MethodicalMachine | TypeMachine[InputProtocol, Core]]],
  116. ] = findMachines,
  117. _print: Callable[..., None] = print,
  118. ) -> None:
  119. """
  120. Entry point for command line utility.
  121. """
  122. DESCRIPTION = """
  123. Visualize automat.MethodicalMachines as graphviz graphs.
  124. """
  125. EPILOG = """
  126. You must have the graphviz tool suite installed. Please visit
  127. http://www.graphviz.org for more information.
  128. """
  129. if _syspath[0]:
  130. _syspath.insert(0, "")
  131. argumentParser = argparse.ArgumentParser(
  132. prog=_progname, description=DESCRIPTION, epilog=EPILOG
  133. )
  134. argumentParser.add_argument(
  135. "fqpn",
  136. help="A Fully Qualified Path name" " representing where to find machines.",
  137. )
  138. argumentParser.add_argument(
  139. "--quiet", "-q", help="suppress output", default=False, action="store_true"
  140. )
  141. argumentParser.add_argument(
  142. "--dot-directory",
  143. "-d",
  144. help="Where to write out .dot files.",
  145. default=".automat_visualize",
  146. )
  147. argumentParser.add_argument(
  148. "--image-directory",
  149. "-i",
  150. help="Where to write out image files.",
  151. default=".automat_visualize",
  152. )
  153. argumentParser.add_argument(
  154. "--image-type",
  155. "-t",
  156. help="The image format.",
  157. choices=graphviz.FORMATS,
  158. default="png",
  159. )
  160. argumentParser.add_argument(
  161. "--view",
  162. "-v",
  163. help="View rendered graphs with" " default image viewer",
  164. default=False,
  165. action="store_true",
  166. )
  167. args = argumentParser.parse_args(_argv)
  168. explicitlySaveDot = args.dot_directory and (
  169. not args.image_directory or args.image_directory != args.dot_directory
  170. )
  171. if args.quiet:
  172. def _print(*args):
  173. pass
  174. for fqpn, machine in _findMachines(args.fqpn):
  175. _print(fqpn, "...discovered")
  176. digraph = machine.asDigraph()
  177. if explicitlySaveDot:
  178. digraph.save(filename="{}.dot".format(fqpn), directory=args.dot_directory)
  179. _print(fqpn, "...wrote dot into", args.dot_directory)
  180. if args.image_directory:
  181. deleteDot = not args.dot_directory or explicitlySaveDot
  182. digraph.format = args.image_type
  183. digraph.render(
  184. filename="{}.dot".format(fqpn),
  185. directory=args.image_directory,
  186. view=args.view,
  187. cleanup=deleteDot,
  188. )
  189. if deleteDot:
  190. msg = "...wrote image into"
  191. else:
  192. msg = "...wrote image and dot into"
  193. _print(fqpn, msg, args.image_directory)