diff options
-rw-r--r-- | doc/source/types.rst | 20 | ||||
-rw-r--r-- | requirements.txt | 3 | ||||
-rw-r--r-- | taskflow/engines/action_engine/runner.py | 16 | ||||
-rw-r--r-- | taskflow/tests/unit/action_engine/test_runner.py | 23 | ||||
-rw-r--r-- | taskflow/tests/unit/test_types.py | 216 | ||||
-rw-r--r-- | taskflow/types/fsm.py | 381 | ||||
-rw-r--r-- | taskflow/types/table.py | 139 | ||||
-rwxr-xr-x | tools/state_graph.py | 8 |
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 24414c6..25f2dcc 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.6.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 401cf50..4e917df 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 @@ -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))) 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 c37cd70..e7f2d13 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) |