From 151f447a8afdd547bf6081ead8a8ec323662fb77 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Tue, 30 Jun 2015 14:58:28 -0700 Subject: [PATCH] Allow providing and using a 'nested_start_state_fetcher' For hierarchical machines it seems like it would be quite useful to allow nested machines to initialize to states that are not just none so to enable this allow for providing a callback that will be used to determine the callback for all nested machines (and nested-nested machines) so that they can be initialized to the users desire. Change-Id: I45a0c0e0c2a976e0df094e6c728f943766be7c7a --- automaton/machines.py | 64 ++++++++++++++++++++++++++++++++----- automaton/tests/test_fsm.py | 11 +++++++ doc/source/conf.py | 1 + 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/automaton/machines.py b/automaton/machines.py index 7875697..f0640fb 100644 --- a/automaton/machines.py +++ b/automaton/machines.py @@ -105,6 +105,7 @@ class FiniteMachine(object): @property def current_state(self): + """The current state the machine is in (or none if not initialized).""" if self._current is not None: return self._current.name return None @@ -119,11 +120,11 @@ class FiniteMachine(object): def add_state(self, state, terminal=False, on_enter=None, on_exit=None): """Adds a given state to the state machine. - The on_enter and on_exit callbacks, if provided will be expected to - take two positional parameters, these being the state being exited (for - on_exit) or the state being entered (for on_enter) and a second - parameter which is the event that is being processed that caused the - state transition. + The ``on_enter`` and ``on_exit`` callbacks, if provided will be + expected to take two positional parameters, these being the state + being exited (for ``on_exit``) or the state being entered (for + ``on_enter``) and a second parameter which is the event that is + being processed that caused the state transition. """ if self.frozen: raise excp.FrozenMachine() @@ -228,7 +229,13 @@ class FiniteMachine(object): return self._post_process_event(event, result) def initialize(self, start_state=None): - """Sets up the state machine (sets current state to start state...).""" + """Sets up the state machine (sets current state to start state...). + + :param start_state: explicit start state to use to initialize the + state machine to. If ``None`` is provided then + the machine's default start state will be used + instead. + """ if start_state is None: start_state = self._default_start_state if start_state not in self._states: @@ -366,6 +373,15 @@ class HierarchicalFiniteMachine(FiniteMachine): def add_state(self, state, terminal=False, on_enter=None, on_exit=None, machine=None): + """Adds a given state to the state machine. + + :param machine: the nested state machine that will be transitioned + into when this state is entered + :type machine: :py:class:`.FiniteMachine` + + Further arguments are interpreted as + for :py:meth:`.FiniteMachine.add_state`. + """ if machine is not None and not isinstance(machine, FiniteMachine): raise ValueError( "Nested state machines must themselves be state machines") @@ -384,13 +400,45 @@ class HierarchicalFiniteMachine(FiniteMachine): c._nested_machines = self._nested_machines.copy() return c - def initialize(self, start_state=None): + def initialize(self, start_state=None, + nested_start_state_fetcher=None): + """Sets up the state machine (sets current state to start state...). + + :param start_state: explicit start state to use to initialize the + state machine to. If ``None`` is provided then the + machine's default start state will be used + instead. + :param nested_start_state_fetcher: A callback that can return start + states for any nested machines + **only**. If not ``None`` then it + will be provided a single argument, + the machine to provide a starting + state for and it is expected to + return a starting state (or + ``None``) for each machine called + with. Do note that this callback + will also be passed to other nested + state machines as well, so it will + also be used to initialize any state + machines they contain (recursively). + """ super(HierarchicalFiniteMachine, self).initialize( start_state=start_state) for data in six.itervalues(self._states): if 'machine' in data: - data['machine'].initialize() + nested_machine = data['machine'] + nested_start_state = None + if nested_start_state_fetcher is not None: + nested_start_state = nested_start_state_fetcher( + nested_machine) + if isinstance(nested_machine, HierarchicalFiniteMachine): + nested_machine.initialize( + start_state=nested_start_state, + nested_start_state_fetcher=nested_start_state_fetcher) + else: + nested_machine.initialize(start_state=nested_start_state) @property def nested_machines(self): + """Dictionary of **all** nested state machines this machine may use.""" return self._nested_machines diff --git a/automaton/tests/test_fsm.py b/automaton/tests/test_fsm.py index 4b4f7d4..bc70e88 100644 --- a/automaton/tests/test_fsm.py +++ b/automaton/tests/test_fsm.py @@ -323,6 +323,17 @@ class HFSMTest(FSMTest): dialer, _number_calling = self._make_phone_dialer() self.assertEqual(1, len(dialer.nested_machines)) + def test_nested_machine_initializers(self): + dialer, _number_calling = self._make_phone_dialer() + queried_for = [] + + def init_with(nested_machine): + queried_for.append(nested_machine) + return None + + dialer.initialize(nested_start_state_fetcher=init_with) + self.assertEqual(1, len(queried_for)) + def test_phone_dialer_iter(self): dialer, number_calling = self._make_phone_dialer() self.assertEqual(0, len(number_calling)) diff --git a/doc/source/conf.py b/doc/source/conf.py index b72874a..8f8849a 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -24,6 +24,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.inheritance_diagram', + 'sphinx.ext.viewcode', 'oslosphinx', ]