Replace internal fsm + table with automaton library

Instead of having our own inbuilt fsm and table library
used to print the fsm states just use the newly created
and released automaton that contains the same/similar
code but as a released library that others can use and
benefit from.

Library @ http://pypi.python.org/pypi/automaton

Change-Id: I1ca40a0805e704fbb37b0106c1831a7e45c6ad68
This commit is contained in:
Joshua Harlow
2014-12-05 21:23:56 -08:00
parent 9f846d0475
commit 50f710eaee
8 changed files with 33 additions and 773 deletions

View File

@@ -6,11 +6,9 @@ Types
Even though these types **are** made for public consumption and usage
should be encouraged/easily possible it should be noted that these may be
moved out to new libraries at various points in the future (for example
the ``FSM`` code *may* move to its own oslo supported ``automaton`` library
at some point in the future [#f1]_). If you are using these
types **without** using the rest of this library it is **strongly**
encouraged that you be a vocal proponent of getting these made
moved out to new libraries at various points in the future. If you are
using these types **without** using the rest of this library it is
**strongly** encouraged that you be a vocal proponent of getting these made
into *isolated* libraries (as using these types in this manner is not
the expected and/or desired usage).
@@ -24,11 +22,6 @@ Failure
.. automodule:: taskflow.types.failure
FSM
===
.. automodule:: taskflow.types.fsm
Graph
=====
@@ -45,11 +38,6 @@ Sets
.. automodule:: taskflow.types.sets
Table
=====
.. automodule:: taskflow.types.table
Timing
======
@@ -60,5 +48,3 @@ Tree
.. automodule:: taskflow.types.tree
.. [#f1] See: https://review.openstack.org/#/c/141961 for a proposal to
do this.

View File

@@ -37,6 +37,9 @@ monotonic>=0.1 # Apache-2.0
# Used for structured input validation
jsonschema!=2.5.0,<3.0.0,>=2.0.0
# For the state machine we run with
automaton>=0.2.0 # Apache-2.0
# For common utilities
oslo.utils>=1.6.0 # Apache-2.0
oslo.serialization>=1.4.0 # Apache-2.0

View File

@@ -14,10 +14,13 @@
# License for the specific language governing permissions and limitations
# under the License.
from automaton import machines
from automaton import runners
from taskflow import logging
from taskflow import states as st
from taskflow.types import failure
from taskflow.types import fsm
# Waiting state timeout (in seconds).
_WAITING_TIMEOUT = 60
@@ -236,7 +239,7 @@ class Runner(object):
watchers['on_exit'] = on_exit
watchers['on_enter'] = on_enter
m = fsm.FSM(_UNDEFINED)
m = machines.FiniteMachine()
m.add_state(_GAME_OVER, **watchers)
m.add_state(_UNDEFINED, **watchers)
m.add_state(st.ANALYZING, **watchers)
@@ -247,6 +250,7 @@ class Runner(object):
m.add_state(st.SUSPENDED, terminal=True, **watchers)
m.add_state(st.WAITING, **watchers)
m.add_state(st.FAILURE, terminal=True, **watchers)
m.default_start_state = _UNDEFINED
m.add_transition(_GAME_OVER, st.REVERTED, _REVERTED)
m.add_transition(_GAME_OVER, st.SUCCESS, _SUCCESS)
@@ -267,12 +271,14 @@ class Runner(object):
m.add_reaction(st.WAITING, _WAIT, wait)
m.freeze()
return (m, memory)
r = runners.FiniteRunner(m)
return (m, r, memory)
def run_iter(self, timeout=None):
"""Runs iteratively using a locally built state machine."""
machine, memory = self.build(timeout=timeout)
for (_prior_state, new_state) in machine.run_iter(_START):
machine, runner, memory = self.build(timeout=timeout)
for (_prior_state, new_state) in runner.run_iter(_START):
# NOTE(harlowja): skip over meta-states.
if new_state not in _META_STATES:
if new_state == st.FAILURE:

View File

@@ -14,19 +14,18 @@
# License for the specific language governing permissions and limitations
# under the License.
from automaton import exceptions as excp
import six
from taskflow.engines.action_engine import compiler
from taskflow.engines.action_engine import executor
from taskflow.engines.action_engine import runner
from taskflow.engines.action_engine import runtime
from taskflow import exceptions as excp
from taskflow.patterns import linear_flow as lf
from taskflow import states as st
from taskflow import storage
from taskflow import test
from taskflow.tests import utils as test_utils
from taskflow.types import fsm
from taskflow.types import notifier
from taskflow.utils import persistence_utils as pu
@@ -184,9 +183,9 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin):
flow.add(*tasks)
rt = self._make_runtime(flow, initial_state=st.RUNNING)
machine, memory = rt.runner.build()
machine, machine_runner, memory = rt.runner.build()
self.assertTrue(rt.runner.runnable())
self.assertRaises(fsm.NotInitialized, machine.process_event, 'poke')
self.assertRaises(excp.NotInitialized, machine.process_event, 'poke')
# Should now be pending...
self.assertEqual(st.PENDING, rt.storage.get_atom_state(tasks[0].name))
@@ -253,10 +252,10 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin):
flow.add(*tasks)
rt = self._make_runtime(flow, initial_state=st.RUNNING)
machine, memory = rt.runner.build()
machine, machine_runner, memory = rt.runner.build()
self.assertTrue(rt.runner.runnable())
transitions = list(machine.run_iter('start'))
transitions = list(machine_runner.run_iter('start'))
self.assertEqual((runner._UNDEFINED, st.RESUMING), transitions[0])
self.assertEqual((runner._GAME_OVER, st.SUCCESS), transitions[-1])
self.assertEqual(st.SUCCESS, rt.storage.get_atom_state(tasks[0].name))
@@ -267,10 +266,10 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin):
flow.add(*tasks)
rt = self._make_runtime(flow, initial_state=st.RUNNING)
machine, memory = rt.runner.build()
machine, machine_runner, memory = rt.runner.build()
self.assertTrue(rt.runner.runnable())
transitions = list(machine.run_iter('start'))
transitions = list(machine_runner.run_iter('start'))
self.assertEqual((runner._GAME_OVER, st.FAILURE), transitions[-1])
self.assertEqual(1, len(memory.failures))
@@ -280,10 +279,10 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin):
flow.add(*tasks)
rt = self._make_runtime(flow, initial_state=st.RUNNING)
machine, memory = rt.runner.build()
machine, machine_runner, memory = rt.runner.build()
self.assertTrue(rt.runner.runnable())
transitions = list(machine.run_iter('start'))
transitions = list(machine_runner.run_iter('start'))
self.assertEqual((runner._GAME_OVER, st.REVERTED), transitions[-1])
self.assertEqual(st.REVERTED, rt.storage.get_atom_state(tasks[0].name))
@@ -294,8 +293,8 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin):
flow.add(*tasks)
rt = self._make_runtime(flow, initial_state=st.RUNNING)
machine, memory = rt.runner.build()
transitions = list(machine.run_iter('start'))
machine, machine_runner, memory = rt.runner.build()
transitions = list(machine_runner.run_iter('start'))
occurrences = dict((t, transitions.count(t)) for t in transitions)
self.assertEqual(10, occurrences.get((st.SCHEDULING, st.WAITING)))

View File

@@ -15,15 +15,11 @@
# under the License.
import networkx as nx
import six
from six.moves import cPickle as pickle
from taskflow import exceptions as excp
from taskflow import test
from taskflow.types import fsm
from taskflow.types import graph
from taskflow.types import sets
from taskflow.types import table
from taskflow.types import tree
@@ -251,218 +247,6 @@ class TreeTest(test.TestCase):
'horse', 'human', 'monkey'], things)
class TableTest(test.TestCase):
def test_create_valid_no_rows(self):
tbl = table.PleasantTable(['Name', 'City', 'State', 'Country'])
self.assertGreater(0, len(tbl.pformat()))
def test_create_valid_rows(self):
tbl = table.PleasantTable(['Name', 'City', 'State', 'Country'])
before_rows = tbl.pformat()
tbl.add_row(["Josh", "San Jose", "CA", "USA"])
after_rows = tbl.pformat()
self.assertGreater(len(before_rows), len(after_rows))
def test_create_invalid_columns(self):
self.assertRaises(ValueError, table.PleasantTable, [])
def test_create_invalid_rows(self):
tbl = table.PleasantTable(['Name', 'City', 'State', 'Country'])
self.assertRaises(ValueError, tbl.add_row, ['a', 'b'])
class FSMTest(test.TestCase):
def setUp(self):
super(FSMTest, self).setUp()
# NOTE(harlowja): this state machine will never stop if run() is used.
self.jumper = fsm.FSM("down")
self.jumper.add_state('up')
self.jumper.add_state('down')
self.jumper.add_transition('down', 'up', 'jump')
self.jumper.add_transition('up', 'down', 'fall')
self.jumper.add_reaction('up', 'jump', lambda *args: 'fall')
self.jumper.add_reaction('down', 'fall', lambda *args: 'jump')
def test_bad_start_state(self):
m = fsm.FSM('unknown')
self.assertRaises(excp.NotFound, m.run, 'unknown')
def test_contains(self):
m = fsm.FSM('unknown')
self.assertNotIn('unknown', m)
m.add_state('unknown')
self.assertIn('unknown', m)
def test_duplicate_state(self):
m = fsm.FSM('unknown')
m.add_state('unknown')
self.assertRaises(excp.Duplicate, m.add_state, 'unknown')
def test_duplicate_reaction(self):
self.assertRaises(
# Currently duplicate reactions are not allowed...
excp.Duplicate,
self.jumper.add_reaction, 'down', 'fall', lambda *args: 'skate')
def test_bad_transition(self):
m = fsm.FSM('unknown')
m.add_state('unknown')
m.add_state('fire')
self.assertRaises(excp.NotFound, m.add_transition,
'unknown', 'something', 'boom')
self.assertRaises(excp.NotFound, m.add_transition,
'something', 'unknown', 'boom')
def test_bad_reaction(self):
m = fsm.FSM('unknown')
m.add_state('unknown')
self.assertRaises(excp.NotFound, m.add_reaction, 'something', 'boom',
lambda *args: 'cough')
def test_run(self):
m = fsm.FSM('down')
m.add_state('down')
m.add_state('up')
m.add_state('broken', terminal=True)
m.add_transition('down', 'up', 'jump')
m.add_transition('up', 'broken', 'hit-wall')
m.add_reaction('up', 'jump', lambda *args: 'hit-wall')
self.assertEqual(['broken', 'down', 'up'], sorted(m.states))
self.assertEqual(2, m.events)
m.initialize()
self.assertEqual('down', m.current_state)
self.assertFalse(m.terminated)
m.run('jump')
self.assertTrue(m.terminated)
self.assertEqual('broken', m.current_state)
self.assertRaises(excp.InvalidState, m.run, 'jump', initialize=False)
def test_on_enter_on_exit(self):
enter_transitions = []
exit_transitions = []
def on_exit(state, event):
exit_transitions.append((state, event))
def on_enter(state, event):
enter_transitions.append((state, event))
m = fsm.FSM('start')
m.add_state('start', on_exit=on_exit)
m.add_state('down', on_enter=on_enter, on_exit=on_exit)
m.add_state('up', on_enter=on_enter, on_exit=on_exit)
m.add_transition('start', 'down', 'beat')
m.add_transition('down', 'up', 'jump')
m.add_transition('up', 'down', 'fall')
m.initialize()
m.process_event('beat')
m.process_event('jump')
m.process_event('fall')
self.assertEqual([('down', 'beat'),
('up', 'jump'), ('down', 'fall')], enter_transitions)
self.assertEqual(
[('start', 'beat'), ('down', 'jump'), ('up', 'fall')],
exit_transitions)
def test_run_iter(self):
up_downs = []
for (old_state, new_state) in self.jumper.run_iter('jump'):
up_downs.append((old_state, new_state))
if len(up_downs) >= 3:
break
self.assertEqual([('down', 'up'), ('up', 'down'), ('down', 'up')],
up_downs)
self.assertFalse(self.jumper.terminated)
self.assertEqual('up', self.jumper.current_state)
self.jumper.process_event('fall')
self.assertEqual('down', self.jumper.current_state)
def test_run_send(self):
up_downs = []
it = self.jumper.run_iter('jump')
while True:
up_downs.append(it.send(None))
if len(up_downs) >= 3:
it.close()
break
self.assertEqual('up', self.jumper.current_state)
self.assertFalse(self.jumper.terminated)
self.assertEqual([('down', 'up'), ('up', 'down'), ('down', 'up')],
up_downs)
self.assertRaises(StopIteration, six.next, it)
def test_run_send_fail(self):
up_downs = []
it = self.jumper.run_iter('jump')
up_downs.append(six.next(it))
self.assertRaises(excp.NotFound, it.send, 'fail')
it.close()
self.assertEqual([('down', 'up')], up_downs)
def test_not_initialized(self):
self.assertRaises(fsm.NotInitialized,
self.jumper.process_event, 'jump')
def test_copy_states(self):
c = fsm.FSM('down')
self.assertEqual(0, len(c.states))
d = c.copy()
c.add_state('up')
c.add_state('down')
self.assertEqual(2, len(c.states))
self.assertEqual(0, len(d.states))
def test_copy_reactions(self):
c = fsm.FSM('down')
d = c.copy()
c.add_state('down')
c.add_state('up')
c.add_reaction('down', 'jump', lambda *args: 'up')
c.add_transition('down', 'up', 'jump')
self.assertEqual(1, c.events)
self.assertEqual(0, d.events)
self.assertNotIn('down', d)
self.assertNotIn('up', d)
self.assertEqual([], list(d))
self.assertEqual([('down', 'jump', 'up')], list(c))
def test_copy_initialized(self):
j = self.jumper.copy()
self.assertIsNone(j.current_state)
for i, transition in enumerate(self.jumper.run_iter('jump')):
if i == 4:
break
self.assertIsNone(j.current_state)
self.assertIsNotNone(self.jumper.current_state)
def test_iter(self):
transitions = list(self.jumper)
self.assertEqual(2, len(transitions))
self.assertIn(('up', 'fall', 'down'), transitions)
self.assertIn(('down', 'jump', 'up'), transitions)
def test_freeze(self):
self.jumper.freeze()
self.assertRaises(fsm.FrozenMachine, self.jumper.add_state, 'test')
self.assertRaises(fsm.FrozenMachine,
self.jumper.add_transition, 'test', 'test', 'test')
self.assertRaises(fsm.FrozenMachine,
self.jumper.add_reaction,
'test', 'test', lambda *args: 'test')
def test_invalid_callbacks(self):
m = fsm.FSM('working')
m.add_state('working')
m.add_state('broken')
self.assertRaises(ValueError, m.add_state, 'b', on_enter=2)
self.assertRaises(ValueError, m.add_state, 'b', on_exit=2)
class OrderedSetTest(test.TestCase):
def test_pickleable(self):

View File

@@ -1,381 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014 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.
import collections
import six
from taskflow import exceptions as excp
from taskflow.types import table
from taskflow.utils import misc
class _Jump(object):
"""A FSM transition tracks this data while jumping."""
def __init__(self, name, on_enter, on_exit):
self.name = name
self.on_enter = on_enter
self.on_exit = on_exit
class FrozenMachine(Exception):
"""Exception raised when a frozen machine is modified."""
def __init__(self):
super(FrozenMachine, self).__init__("Frozen machine can't be modified")
class NotInitialized(excp.TaskFlowException):
"""Error raised when an action is attempted on a not inited machine."""
class FSM(object):
"""A finite state machine.
This state machine can be used to automatically run a given set of
transitions and states in response to events (either from callbacks or from
generator/iterator send() values, see PEP 342). On each triggered event, a
on_enter and on_exit callback can also be provided which will be called to
perform some type of action on leaving a prior state and before entering a
new state.
NOTE(harlowja): reactions will *only* be called when the generator/iterator
from run_iter() does *not* send back a new event (they will always be
called if the run() method is used). This allows for two unique ways (these
ways can also be intermixed) to use this state machine when using
run_iter(); one where *external* events trigger the next state transition
and one where *internal* reaction callbacks trigger the next state
transition. The other way to use this state machine is to skip using run()
or run_iter() completely and use the process_event() method explicitly and
trigger the events via some *external* functionality.
"""
def __init__(self, start_state):
self._transitions = {}
self._states = collections.OrderedDict()
self._start_state = start_state
self._current = None
self.frozen = False
@property
def start_state(self):
return self._start_state
@property
def current_state(self):
"""Return the current state name.
:returns: current state name
:rtype: string
"""
if self._current is not None:
return self._current.name
return None
@property
def terminated(self):
"""Returns whether the state machine is in a terminal state.
:returns: whether the state machine is in
terminal state or not
:rtype: boolean
"""
if self._current is None:
return False
return self._states[self._current.name]['terminal']
@misc.disallow_when_frozen(FrozenMachine)
def add_state(self, state, terminal=False, on_enter=None, on_exit=None):
"""Adds a given state to the state machine.
:param on_enter: callback, if provided will be expected to take
two positional parameters, these being state being
entered and the second parameter is the event that is
being processed that caused the state transition
:param on_exit: callback, if provided will be expected to take
two positional parameters, these being state being
entered and the second parameter is the event that is
being processed that caused the state transition
:param state: state being entered or exited
:type state: string
"""
if state in self._states:
raise excp.Duplicate("State '%s' already defined" % state)
if on_enter is not None:
if not six.callable(on_enter):
raise ValueError("On enter callback must be callable")
if on_exit is not None:
if not six.callable(on_exit):
raise ValueError("On exit callback must be callable")
self._states[state] = {
'terminal': bool(terminal),
'reactions': {},
'on_enter': on_enter,
'on_exit': on_exit,
}
self._transitions[state] = collections.OrderedDict()
@misc.disallow_when_frozen(FrozenMachine)
def add_reaction(self, state, event, reaction, *args, **kwargs):
"""Adds a reaction that may get triggered by the given event & state.
:param state: the last stable state expressed
:type state: string
:param event: event that caused the transition
:param args: non-keyworded arguments
:type args: list
:param kwargs: key-value pair arguments
:type kwargs: dictionary
Reaction callbacks may (depending on how the state machine is ran) be
used after an event is processed (and a transition occurs) to cause
the machine to react to the newly arrived at stable state. The
expected result of a callback is expected to be a
new event that the callback wants the state machine to react to.
This new event may (depending on how the state machine is ran) get
processed (and this process typically repeats) until the state
machine reaches a terminal state.
"""
if state not in self._states:
raise excp.NotFound("Can not add a reaction to event '%s' for an"
" undefined state '%s'" % (event, state))
if not six.callable(reaction):
raise ValueError("Reaction callback must be callable")
if event not in self._states[state]['reactions']:
self._states[state]['reactions'][event] = (reaction, args, kwargs)
else:
raise excp.Duplicate("State '%s' reaction to event '%s'"
" already defined" % (state, event))
@misc.disallow_when_frozen(FrozenMachine)
def add_transition(self, start, end, event):
"""Adds an allowed transition from start -> end for the given event.
:param start: start of the transition
:param end: end of the transition
:param event: event that caused the transition
"""
if start not in self._states:
raise excp.NotFound("Can not add a transition on event '%s' that"
" starts in a undefined state '%s'" % (event,
start))
if end not in self._states:
raise excp.NotFound("Can not add a transition on event '%s' that"
" ends in a undefined state '%s'" % (event,
end))
self._transitions[start][event] = _Jump(end,
self._states[end]['on_enter'],
self._states[start]['on_exit'])
def process_event(self, event):
"""Trigger a state change in response to the provided event.
:param event: event to be processed to cause a potential transition
"""
current = self._current
if current is None:
raise NotInitialized("Can only process events after"
" being initialized (not before)")
if self._states[current.name]['terminal']:
raise excp.InvalidState("Can not transition from terminal"
" state '%s' on event '%s'"
% (current.name, event))
if event not in self._transitions[current.name]:
raise excp.NotFound("Can not transition from state '%s' on"
" event '%s' (no defined transition)"
% (current.name, event))
replacement = self._transitions[current.name][event]
if current.on_exit is not None:
current.on_exit(current.name, event)
if replacement.on_enter is not None:
replacement.on_enter(replacement.name, event)
self._current = replacement
return (
self._states[replacement.name]['reactions'].get(event),
self._states[replacement.name]['terminal'],
)
def initialize(self):
"""Sets up the state machine (sets current state to start state...)."""
if self._start_state not in self._states:
raise excp.NotFound("Can not start from a undefined"
" state '%s'" % (self._start_state))
if self._states[self._start_state]['terminal']:
raise excp.InvalidState("Can not start from a terminal"
" state '%s'" % (self._start_state))
# No on enter will be called, since we are priming the state machine
# and have not really transitioned from anything to get here, we will
# though allow 'on_exit' to be called on the event that causes this
# to be moved from...
self._current = _Jump(self._start_state, None,
self._states[self._start_state]['on_exit'])
def run(self, event, initialize=True):
"""Runs the state machine, using reactions only."""
for _transition in self.run_iter(event, initialize=initialize):
pass
def copy(self):
"""Copies the current state machine.
NOTE(harlowja): the copy will be left in an *uninitialized* state.
"""
c = FSM(self.start_state)
c.frozen = self.frozen
for state, data in six.iteritems(self._states):
copied_data = data.copy()
copied_data['reactions'] = copied_data['reactions'].copy()
c._states[state] = copied_data
for state, data in six.iteritems(self._transitions):
c._transitions[state] = data.copy()
return c
def run_iter(self, event, initialize=True):
"""Returns a iterator/generator that will run the state machine.
NOTE(harlowja): only one runner iterator/generator should be active for
a machine, if this is not observed then it is possible for
initialization and other local state to be corrupted and cause issues
when running...
"""
if initialize:
self.initialize()
while True:
old_state = self.current_state
reaction, terminal = self.process_event(event)
new_state = self.current_state
try:
sent_event = yield (old_state, new_state)
except GeneratorExit:
break
if terminal:
break
if reaction is None and sent_event is None:
raise excp.NotFound("Unable to progress since no reaction (or"
" sent event) has been made available in"
" new state '%s' (moved to from state '%s'"
" in response to event '%s')"
% (new_state, old_state, event))
elif sent_event is not None:
event = sent_event
else:
cb, args, kwargs = reaction
event = cb(old_state, new_state, event, *args, **kwargs)
def __contains__(self, state):
"""Returns if this state exists in the machines known states.
:param state: input state
:type state: string
:returns: checks whether the state exists in the machine
known states
:rtype: boolean
"""
return state in self._states
def freeze(self):
"""Freezes & stops addition of states, transitions, reactions..."""
self.frozen = True
@property
def states(self):
"""Returns the state names."""
return list(six.iterkeys(self._states))
@property
def events(self):
"""Returns how many events exist.
:returns: how many events exist
:rtype: number
"""
c = 0
for state in six.iterkeys(self._states):
c += len(self._transitions[state])
return c
def __iter__(self):
"""Iterates over (start, event, end) transition tuples."""
for state in six.iterkeys(self._states):
for event, target in six.iteritems(self._transitions[state]):
yield (state, event, target.name)
def pformat(self, sort=True):
"""Pretty formats the state + transition table into a string.
NOTE(harlowja): the sort parameter can be provided to sort the states
and transitions by sort order; with it being provided as false the rows
will be iterated in addition order instead.
**Example**::
>>> from taskflow.types import fsm
>>> f = fsm.FSM("sits")
>>> f.add_state("sits")
>>> f.add_state("barks")
>>> f.add_state("wags tail")
>>> f.add_transition("sits", "barks", "squirrel!")
>>> f.add_transition("barks", "wags tail", "gets petted")
>>> f.add_transition("wags tail", "sits", "gets petted")
>>> f.add_transition("wags tail", "barks", "squirrel!")
>>> print(f.pformat())
+-----------+-------------+-----------+----------+---------+
Start | Event | End | On Enter | On Exit
+-----------+-------------+-----------+----------+---------+
barks | gets petted | wags tail | |
sits[^] | squirrel! | barks | |
wags tail | gets petted | sits | |
wags tail | squirrel! | barks | |
+-----------+-------------+-----------+----------+---------+
"""
def orderedkeys(data):
if sort:
return sorted(six.iterkeys(data))
return list(six.iterkeys(data))
tbl = table.PleasantTable(["Start", "Event", "End",
"On Enter", "On Exit"])
for state in orderedkeys(self._states):
prefix_markings = []
if self.current_state == state:
prefix_markings.append("@")
postfix_markings = []
if self.start_state == state:
postfix_markings.append("^")
if self._states[state]['terminal']:
postfix_markings.append("$")
pretty_state = "%s%s" % ("".join(prefix_markings), state)
if postfix_markings:
pretty_state += "[%s]" % "".join(postfix_markings)
if self._transitions[state]:
for event in orderedkeys(self._transitions[state]):
target = self._transitions[state][event]
row = [pretty_state, event, target.name]
if target.on_enter is not None:
try:
row.append(target.on_enter.__name__)
except AttributeError:
row.append(target.on_enter)
else:
row.append('')
if target.on_exit is not None:
try:
row.append(target.on_exit.__name__)
except AttributeError:
row.append(target.on_exit)
else:
row.append('')
tbl.add_row(row)
else:
tbl.add_row([pretty_state, "", "", "", ""])
return tbl.pformat()

View File

@@ -1,139 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014 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.
import itertools
import os
import six
class PleasantTable(object):
"""A tiny pretty printing table (like prettytable/tabulate but smaller).
Creates simply formatted tables (with no special sauce)::
>>> from taskflow.types import table
>>> tbl = table.PleasantTable(['Name', 'City', 'State', 'Country'])
>>> tbl.add_row(["Josh", "San Jose", "CA", "USA"])
>>> print(tbl.pformat())
+------+----------+-------+---------+
Name | City | State | Country
+------+----------+-------+---------+
Josh | San Jose | CA | USA
+------+----------+-------+---------+
"""
# Constants used when pretty formatting the table.
COLUMN_STARTING_CHAR = ' '
COLUMN_ENDING_CHAR = ''
COLUMN_SEPARATOR_CHAR = '|'
HEADER_FOOTER_JOINING_CHAR = '+'
HEADER_FOOTER_CHAR = '-'
LINE_SEP = os.linesep
@staticmethod
def _center_text(text, max_len, fill=' '):
return '{0:{fill}{align}{size}}'.format(text, fill=fill,
align="^", size=max_len)
@classmethod
def _size_selector(cls, possible_sizes):
"""Select the maximum size, utility function for adding borders.
The number two is used so that the edges of a column have spaces
around them (instead of being right next to a column separator).
:param possible_sizes: possible sizes available
:returns: maximum size
:rtype: number
"""
try:
return max(x + 2 for x in possible_sizes)
except ValueError:
return 0
def __init__(self, columns):
if len(columns) == 0:
raise ValueError("Column count must be greater than zero")
self._columns = [column.strip() for column in columns]
self._rows = []
def add_row(self, row):
if len(row) != len(self._columns):
raise ValueError("Row must have %s columns instead of"
" %s columns" % (len(self._columns), len(row)))
self._rows.append([six.text_type(column) for column in row])
def pformat(self):
# Figure out the maximum column sizes...
column_count = len(self._columns)
column_sizes = [0] * column_count
headers = []
for i, column in enumerate(self._columns):
possible_sizes_iter = itertools.chain(
[len(column)], (len(row[i]) for row in self._rows))
column_sizes[i] = self._size_selector(possible_sizes_iter)
headers.append(self._center_text(column, column_sizes[i]))
# Build the header and footer prefix/postfix.
header_footer_buf = six.StringIO()
header_footer_buf.write(self.HEADER_FOOTER_JOINING_CHAR)
for i, header in enumerate(headers):
header_footer_buf.write(self.HEADER_FOOTER_CHAR * len(header))
if i + 1 != column_count:
header_footer_buf.write(self.HEADER_FOOTER_JOINING_CHAR)
header_footer_buf.write(self.HEADER_FOOTER_JOINING_CHAR)
# Build the main header.
content_buf = six.StringIO()
content_buf.write(header_footer_buf.getvalue())
content_buf.write(self.LINE_SEP)
content_buf.write(self.COLUMN_STARTING_CHAR)
for i, header in enumerate(headers):
if i + 1 == column_count:
if self.COLUMN_ENDING_CHAR:
content_buf.write(headers[i])
content_buf.write(self.COLUMN_ENDING_CHAR)
else:
content_buf.write(headers[i].rstrip())
else:
content_buf.write(headers[i])
content_buf.write(self.COLUMN_SEPARATOR_CHAR)
content_buf.write(self.LINE_SEP)
content_buf.write(header_footer_buf.getvalue())
# Build the main content.
row_count = len(self._rows)
if row_count:
content_buf.write(self.LINE_SEP)
for i, row in enumerate(self._rows):
pieces = []
for j, column in enumerate(row):
pieces.append(self._center_text(column, column_sizes[j]))
if j + 1 != column_count:
pieces.append(self.COLUMN_SEPARATOR_CHAR)
blob = ''.join(pieces)
if self.COLUMN_ENDING_CHAR:
content_buf.write(self.COLUMN_STARTING_CHAR)
content_buf.write(blob)
content_buf.write(self.COLUMN_ENDING_CHAR)
else:
blob = blob.rstrip()
if blob:
content_buf.write(self.COLUMN_STARTING_CHAR)
content_buf.write(blob)
if i + 1 != row_count:
content_buf.write(self.LINE_SEP)
content_buf.write(self.LINE_SEP)
content_buf.write(header_footer_buf.getvalue())
return content_buf.getvalue()

View File

@@ -29,10 +29,11 @@ sys.path.insert(0, top_dir)
# $ pip install pydot2
import pydot
from automaton import machines
from taskflow.engines.action_engine import runner
from taskflow.engines.worker_based import protocol
from taskflow import states
from taskflow.types import fsm
# This is just needed to get at the runner builder object (we will not
@@ -52,7 +53,7 @@ def clean_event(name):
def make_machine(start_state, transitions):
machine = fsm.FSM(start_state)
machine = machines.FiniteMachine()
machine.add_state(start_state)
for (start_state, end_state) in transitions:
if start_state not in machine:
@@ -62,6 +63,7 @@ def make_machine(start_state, transitions):
# Make a fake event (not used anyway)...
event = "on_%s" % (end_state)
machine.add_transition(start_state, end_state, event.lower())
machine.default_start_state = start_state
return machine
@@ -192,7 +194,7 @@ def main():
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[source.start_state], style='dotted'))
g.add_edge(pydot.Edge(start, nodes[source.default_start_state], style='dotted'))
print("*" * len(graph_name))
print(graph_name)