diff --git a/automaton/machines.py b/automaton/machines.py index 3051bc5..bd325b0 100644 --- a/automaton/machines.py +++ b/automaton/machines.py @@ -107,6 +107,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 @@ -121,11 +122,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() @@ -249,7 +250,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: @@ -391,6 +398,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") @@ -409,13 +425,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 a94db2c..7cf226c 100644 --- a/automaton/tests/test_fsm.py +++ b/automaton/tests/test_fsm.py @@ -328,6 +328,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', ]