From 3898b1d803738aa811d4c88add2c75cec3dad00a Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Sat, 13 Jun 2015 13:10:51 -0700 Subject: [PATCH] Add optional machine conversion into a pydot graph Both ironic and taskflow share this same code to convert their state machines into a pydot graph which then gets converted into SVG to form: - http://docs.openstack.org/developer/taskflow/states.html - http://docs.openstack.org/developer/ironic/dev/states.html So instead of duplicating it, provide a useful helper function that both (and others) can share to produce a dot/graphviz pretty diagram from a state machine. Change-Id: I218740910163a1ca2587d706edc55852af1c0c74 --- automaton/converters/__init__.py | 0 automaton/converters/pydot.py | 105 +++++++++++++++++++++++++++++++ doc/source/api.rst | 7 +++ 3 files changed, 112 insertions(+) create mode 100644 automaton/converters/__init__.py create mode 100644 automaton/converters/pydot.py diff --git a/automaton/converters/__init__.py b/automaton/converters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/automaton/converters/pydot.py b/automaton/converters/pydot.py new file mode 100644 index 0000000..15d02e8 --- /dev/null +++ b/automaton/converters/pydot.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +try: + import pydot + PYDOT_AVAILABLE = True +except ImportError: + PYDOT_AVAILABLE = False + + +def convert(machine, graph_name, + graph_attrs=None, node_attrs_cb=None, edge_attrs_cb=None, + add_start_state=True): + """Translates the state machine into a pydot graph. + + :param machine: state machine to convert + :type machine: FiniteMachine + :param graph_name: name of the graph to be created + :type graph_name: string + :param graph_attrs: any initial graph attributes to set + (see http://www.graphviz.org/doc/info/attrs.html for + what these can be) + :type graph_attrs: dict + :param node_attrs_cb: a callback that takes one argument ``state`` + and is expected to return a dict of node attributes + (see http://www.graphviz.org/doc/info/attrs.html for + what these can be) + :type node_attrs_cb: callback + :param edge_attrs_cb: a callback that takes three arguments ``start_state, + event, end_state`` and is expected to return a dict + of edge attributes (see + http://www.graphviz.org/doc/info/attrs.html for + what these can be) + :type edge_attrs_cb: callback + :param add_start_state: when enabled this creates a *private* start state + with the name ``__start__`` that will be a point + node that will have a dotted edge to the + ``default_start_state`` that your machine may have + defined (if your machine has no actively defined + ``default_start_state`` then this does nothing, + even if enabled) + :type add_start_state: bool + """ + if not PYDOT_AVAILABLE: + raise RuntimeError("pydot (or pydot2 or equivalent) is required" + " to convert a state machine into a pydot" + " graph") + graph_kwargs = { + 'rankdir': 'LR', + 'nodesep': '0.25', + 'overlap': 'false', + 'ranksep': '0.5', + 'size': "11x8.5", + 'splines': 'true', + 'ordering': 'in', + } + if graph_attrs is not None: + graph_kwargs.update(graph_attrs) + graph_kwargs['graph_name'] = graph_name + g = pydot.Dot(**graph_kwargs) + node_attrs = { + 'fontsize': '11', + } + nodes = {} + for (start_state, event, end_state) in machine: + if start_state not in nodes: + start_node_attrs = node_attrs.copy() + if node_attrs_cb is not None: + start_node_attrs.update(node_attrs_cb(start_state)) + nodes[start_state] = pydot.Node(start_state, + **start_node_attrs) + g.add_node(nodes[start_state]) + if end_state not in nodes: + end_node_attrs = node_attrs.copy() + if node_attrs_cb is not None: + end_node_attrs.update(node_attrs_cb(end_state)) + nodes[end_state] = pydot.Node(end_state, **end_node_attrs) + g.add_node(nodes[end_state]) + edge_attrs = {} + if edge_attrs_cb is not None: + edge_attrs.update(edge_attrs_cb(start_state, event, end_state)) + g.add_edge(pydot.Edge(nodes[start_state], nodes[end_state], + **edge_attrs)) + if add_start_state and machine.default_start_state: + start = pydot.Node("__start__", shape="point", width="0.1", + xlabel='start', fontcolor='green', **node_attrs) + g.add_node(start) + g.add_edge(pydot.Edge(start, nodes[machine.default_start_state], + style='dotted')) + return g diff --git a/doc/source/api.rst b/doc/source/api.rst index 8b9b3cb..bf851f5 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -22,6 +22,13 @@ Runners .. autoclass:: automaton.runners.HierarchicalRunner :members: +---------- +Converters +---------- + +.. automodule:: automaton.converters.pydot + :members: + ---------- Exceptions ----------