summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/source/types.rst20
-rw-r--r--requirements.txt3
-rw-r--r--taskflow/engines/action_engine/runner.py16
-rw-r--r--taskflow/tests/unit/action_engine/test_runner.py23
-rw-r--r--taskflow/tests/unit/test_types.py216
-rw-r--r--taskflow/types/fsm.py381
-rw-r--r--taskflow/types/table.py139
-rwxr-xr-xtools/state_graph.py8
8 files changed, 33 insertions, 773 deletions
diff --git a/doc/source/types.rst b/doc/source/types.rst
index 84d446a..254ed28 100644
--- a/doc/source/types.rst
+++ b/doc/source/types.rst
@@ -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.
diff --git a/requirements.txt b/requirements.txt
index ebb42af..4c9740c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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.9.0 # Apache-2.0
oslo.serialization>=1.4.0 # Apache-2.0
diff --git a/taskflow/engines/action_engine/runner.py b/taskflow/engines/action_engine/runner.py
index 9b6043a..e8cd173 100644
--- a/taskflow/engines/action_engine/runner.py
+++ b/taskflow/engines/action_engine/runner.py
@@ -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:
diff --git a/taskflow/tests/unit/action_engine/test_runner.py b/taskflow/tests/unit/action_engine/test_runner.py
index 9d43f31..7af2bd7 100644
--- a/taskflow/tests/unit/action_engine/test_runner.py
+++ b/taskflow/tests/unit/action_engine/test_runner.py
@@ -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
@@ -185,9 +184,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))
@@ -254,10 +253,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))
@@ -268,10 +267,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))
@@ -281,10 +280,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))
@@ -295,8 +294,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)))
diff --git a/taskflow/tests/unit/test_types.py b/taskflow/tests/unit/test_types.py
index 8980aa5..1d3f541 100644
--- a/taskflow/tests/unit/test_types.py
+++ b/taskflow/tests/unit/test_types.py
@@ -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):
diff --git a/taskflow/types/fsm.py b/taskflow/types/fsm.py
deleted file mode 100644
index 1ed3193..0000000
--- a/taskflow/types/fsm.py
+++ /dev/null
@@ -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()
diff --git a/taskflow/types/table.py b/taskflow/types/table.py
deleted file mode 100644
index 5966051..0000000
--- a/taskflow/types/table.py
+++ /dev/null
@@ -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()
diff --git a/tools/state_graph.py b/tools/state_graph.py
index 635ec68..c9bdd0b 100755
--- a/tools/state_graph.py
+++ b/tools/state_graph.py
@@ -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)