I have been looking at closed issues on github, SO, and googling to solve this issue. But I haven't been able to solve my problem and this seems to be the right place. I already opened an issue on github, but I am not sure if that was the right thing to do. I am making a state machine which can include several substates, which are also all state machines. So this basically boils down to reusing HSM according to readme.
my highest level SM looks like this:
from transitions.extensions import LockedHierarchicalMachine as Machine
from coordination.running import RunningStateMachine
logging.basicConfig(level=logging.ERROR)
logging.getLogger("transitions").setLevel(logging.INFO)
class RPPStateMachine(Machine):
def __init__(self, name):
self._running = RunningStateMachine()
self.name = name
states = [
"init",
{"name": "running", "children": self._running},
"stop",
]
Machine.__init__(self, states=states, initial="init")
self.add_transition("e_run", "init", "run", after=self.run_machine)
self.add_transition("e_stop", "*", "stop")
def run_machine(self):
self._running.initialize()
As you see a state machine with three states init
, running
and stop
. Once the event e_run()
is sent via something like
machine = RPPStateMachine("my_machine")
machine.e_run()
machine transitions to running
state.
I do it in an indirect way because I wanted things to happen automatically. e_run()
causes transition to running
and afterwards run_machine
calls initialize
method of running class, which fires an event to start up the chain of events. Below I show running
and that clears thing up.
So the running state is defined as
from transitions.extensions import LockedHierarchicalMachine as Machine
from coordination.test_mode import TestingStateMachine
from coordination.release_mode import ReleaseStateMachine
class RunningStateMachine(Machine):
def __init__(self):
self._test_mode = TestingStateMachine()
self._release_demo = ReleaseStateMachine()
states = [
"init",
"configuration",
"idle",
{"name": "test_mode", "children": self._test_mode},
{"name": "release_mode", "children": self._release_mode},
]
Machine.__init__(self, states=states, initial="init")
self.add_transition("e_start_running", "init", "configuration", after=self.configuration)
self.add_transition("e_success_config", "configuration", "idle")
self.add_transition("e_test_mode", "idle", "test_mode")
self.add_transition("e_release_mode", "idle", "release_mode")
self.add_transition("e_start_running", "idle", "init")
def initialize(self):
print("Initialization step for running, emitting e_start.")
self.e_start_running()
def configuration(self):
print("Configuring...")
print( "Current state: " + self.state)
self.e_success_config()
which similar to its parent, is composed of a few states and a few substates. I have also enabled logging to see which states I enter and exit. To my experience, nesting state machines is very useful as you can reuse the states you have written before. Besides as your state machine grows, it helps to keep things more modular. So no state becomes huge and difficult to read/understand.
So the unusual behavior is that when e_run()
is called I get prints of
INFO:transitions.core:Entered state running
INFO:transitions.core:Entered state running_init
Initialization step for running, emitting e_start.
INFO:transitions.core:Exited state init
INFO:transitions.core:Entered state configuration
Configuring...
current state: configuration
INFO:transitions.core:Exited state configuration
INFO:transitions.core:Entered state idle
As you see
machine.state
>>> 'running_init'
while
machine._running.state
>>> 'idle'
I can of course move the transition definitions to the parent state, but that's unhandy. I cannot do that for all sub-states. Obviously, I want each substate to be responsible for it's own behavior. What is the common practice here? Is this a bug or intended behavior?
How can I neatly nest state machines under each other?
As of transitions
0.7.1, passing a state machine as a child of another state machine will copy all states of the passed machine to the parent. The passed state machine stays unaltered (as we discussed here).
from transitions.extensions import MachineFactory
HSM = MachineFactory.get_predefined(nested=True)
fsm = HSM(states=['A', 'B'], initial='A')
hsm = HSM(states=['1', {'name': '2', 'children': fsm}])
# states object have been copied instead of referenced, they are not identical
assert fsm.states['A'] is not hsm.states['2_A']
hsm.to_2_A()
# both machines work with different models
assert fsm.models[0] is not hsm.models[0]
assert fsm.state is not hsm.state
The currently recommended workflow is to split models and machines and consider machines only as some sort of 'blueprint' for its parent:
from transitions.extensions import MachineFactory
class Model:
pass
HSM = MachineFactory.get_predefined(nested=True)
# creating fsm as a blueprint, it does not need a model
fsm = HSM(model=None, states=['A', 'B'], initial='A')
# use a model AND also
model = Model()
hsm = HSM(model=['self', model], states=['1', {'name': '2', 'children': fsm}])
# will only update the machine's state
hsm.to_1()
assert model.state != hsm.state
# will update ALL model states
hsm.dispatch("to_2_B")
assert model.state == hsm.state
However, this does not replace a properly isolated (and/or scoped) nesting of machines into parent machines. A feature draft has been created and will hopefully be realized in the foreseeable future.