123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- from __future__ import annotations
- import argparse
- import sys
- from functools import wraps
- from typing import Callable, Iterator
- import graphviz
- from ._core import Automaton, Input, Output, State
- from ._discover import findMachines
- from ._methodical import MethodicalMachine
- from ._typed import TypeMachine, InputProtocol, Core
- def _gvquote(s: str) -> str:
- return '"{}"'.format(s.replace('"', r"\""))
- def _gvhtml(s: str) -> str:
- return "<{}>".format(s)
- def elementMaker(name: str, *children: str, **attrs: str) -> str:
- """
- Construct a string from the HTML element description.
- """
- formattedAttrs = " ".join(
- "{}={}".format(key, _gvquote(str(value)))
- for key, value in sorted(attrs.items())
- )
- formattedChildren = "".join(children)
- return "<{name} {attrs}>{children}</{name}>".format(
- name=name, attrs=formattedAttrs, children=formattedChildren
- )
- def tableMaker(
- inputLabel: str,
- outputLabels: list[str],
- port: str,
- _E: Callable[..., str] = elementMaker,
- ) -> str:
- """
- Construct an HTML table to label a state transition.
- """
- colspan = {}
- if outputLabels:
- colspan["colspan"] = str(len(outputLabels))
- inputLabelCell = _E(
- "td",
- _E("font", inputLabel, face="menlo-italic"),
- color="purple",
- port=port,
- **colspan,
- )
- pointSize = {"point-size": "9"}
- outputLabelCells = [
- _E("td", _E("font", outputLabel, **pointSize), color="pink")
- for outputLabel in outputLabels
- ]
- rows = [_E("tr", inputLabelCell)]
- if outputLabels:
- rows.append(_E("tr", *outputLabelCells))
- return _E("table", *rows)
- def escapify(x: Callable[[State], str]) -> Callable[[State], str]:
- @wraps(x)
- def impl(t: State) -> str:
- return x(t).replace("<", "<").replace(">", ">")
- return impl
- def makeDigraph(
- automaton: Automaton[State, Input, Output],
- inputAsString: Callable[[Input], str] = repr,
- outputAsString: Callable[[Output], str] = repr,
- stateAsString: Callable[[State], str] = repr,
- ) -> graphviz.Digraph:
- """
- Produce a L{graphviz.Digraph} object from an automaton.
- """
- inputAsString = escapify(inputAsString)
- outputAsString = escapify(outputAsString)
- stateAsString = escapify(stateAsString)
- digraph = graphviz.Digraph(
- graph_attr={"pack": "true", "dpi": "100"},
- node_attr={"fontname": "Menlo"},
- edge_attr={"fontname": "Menlo"},
- )
- for state in automaton.states():
- if state is automaton.initialState:
- stateShape = "bold"
- fontName = "Menlo-Bold"
- else:
- stateShape = ""
- fontName = "Menlo"
- digraph.node(
- stateAsString(state),
- fontame=fontName,
- shape="ellipse",
- style=stateShape,
- color="blue",
- )
- for n, eachTransition in enumerate(automaton.allTransitions()):
- inState, inputSymbol, outState, outputSymbols = eachTransition
- thisTransition = "t{}".format(n)
- inputLabel = inputAsString(inputSymbol)
- port = "tableport"
- table = tableMaker(
- inputLabel,
- [outputAsString(outputSymbol) for outputSymbol in outputSymbols],
- port=port,
- )
- digraph.node(thisTransition, label=_gvhtml(table), margin="0.2", shape="none")
- digraph.edge(
- stateAsString(inState),
- "{}:{}:w".format(thisTransition, port),
- arrowhead="none",
- )
- digraph.edge("{}:{}:e".format(thisTransition, port), stateAsString(outState))
- return digraph
- def tool(
- _progname: str = sys.argv[0],
- _argv: list[str] = sys.argv[1:],
- _syspath: list[str] = sys.path,
- _findMachines: Callable[
- [str],
- Iterator[tuple[str, MethodicalMachine | TypeMachine[InputProtocol, Core]]],
- ] = findMachines,
- _print: Callable[..., None] = print,
- ) -> None:
- """
- Entry point for command line utility.
- """
- DESCRIPTION = """
- Visualize automat.MethodicalMachines as graphviz graphs.
- """
- EPILOG = """
- You must have the graphviz tool suite installed. Please visit
- http://www.graphviz.org for more information.
- """
- if _syspath[0]:
- _syspath.insert(0, "")
- argumentParser = argparse.ArgumentParser(
- prog=_progname, description=DESCRIPTION, epilog=EPILOG
- )
- argumentParser.add_argument(
- "fqpn",
- help="A Fully Qualified Path name" " representing where to find machines.",
- )
- argumentParser.add_argument(
- "--quiet", "-q", help="suppress output", default=False, action="store_true"
- )
- argumentParser.add_argument(
- "--dot-directory",
- "-d",
- help="Where to write out .dot files.",
- default=".automat_visualize",
- )
- argumentParser.add_argument(
- "--image-directory",
- "-i",
- help="Where to write out image files.",
- default=".automat_visualize",
- )
- argumentParser.add_argument(
- "--image-type",
- "-t",
- help="The image format.",
- choices=graphviz.FORMATS,
- default="png",
- )
- argumentParser.add_argument(
- "--view",
- "-v",
- help="View rendered graphs with" " default image viewer",
- default=False,
- action="store_true",
- )
- args = argumentParser.parse_args(_argv)
- explicitlySaveDot = args.dot_directory and (
- not args.image_directory or args.image_directory != args.dot_directory
- )
- if args.quiet:
- def _print(*args):
- pass
- for fqpn, machine in _findMachines(args.fqpn):
- _print(fqpn, "...discovered")
- digraph = machine.asDigraph()
- if explicitlySaveDot:
- digraph.save(filename="{}.dot".format(fqpn), directory=args.dot_directory)
- _print(fqpn, "...wrote dot into", args.dot_directory)
- if args.image_directory:
- deleteDot = not args.dot_directory or explicitlySaveDot
- digraph.format = args.image_type
- digraph.render(
- filename="{}.dot".format(fqpn),
- directory=args.image_directory,
- view=args.view,
- cleanup=deleteDot,
- )
- if deleteDot:
- msg = "...wrote image into"
- else:
- msg = "...wrote image and dot into"
- _print(fqpn, msg, args.image_directory)
|