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}".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)