Closed S1MP50N closed 3 years ago
Hello @S1MP50N,
transitions
differentiates between a Machine
which defines transitions, events and state objects and a Model
which is the actual stateful object and defines all callbacks that should be resolved by name. When you do not explicitely pass a model to a machine, the machine itself will act as a model.
The documentation states that
However, since 0.8.0 (Nested)State instances are just referenced which means changes in one machine's collection of states and events will influence the other machine instance. Models and their state will not be shared though.
When you instantiate your parent machine and pass an already instantiated child machine, it will only reference state objects and transitions from the child machine. But it will be the parent that acts as a model. There have been several issues discussing this (e.g. #457, #456). You could either split your machine and model definitions or pass callbacks by reference instead of by name (self.top_bottom_transition
instead of 'top_bottom_transition')
from transitions.extensions import HierarchicalMachine
class ChildFSM(HierarchicalMachine):
def top_bottom_transition(self):
print("top -> bottom")
def bottom_top_transition(self):
print("bottom -> top")
def __init__(self):
states = ['top', 'bottom']
HierarchicalMachine.__init__(self, states=states, initial='top', auto_transitions=False)
self.add_transition('down', 'top', 'bottom', after=self.top_bottom_transition)
self.add_transition('up', 'bottom', 'top', after=self.bottom_top_transition)
class ParentFSM(HierarchicalMachine):
def init_running_transition(self):
print("init->running")
def __init__(self, running):
states = ['init', {'name': 'running', 'children': running}]
HierarchicalMachine.__init__(self, states=states, initial='init', auto_transitions=False)
self.add_transition('start', 'init', 'running', after=self.init_running_transition)
childFSM = ChildFSM()
parentFSM = ParentFSM(childFSM)
print(parentFSM.state)
parentFSM.start()
print(parentFSM.state)
parentFSM.down()
print(parentFSM.state)
parentFSM.up()
print(parentFSM.state)
However, childFSM
's state will be independent of the state of parentFSM
.
Hi @aleneum,
Thanks for the quick and comprehensive reply. Still digesting what you have shared but had a follow up query.
I think the pass by reference is the simplest fix for the problem I presented. I would like to try and maintain the "siloing" of the FSM content as much as possible. Creating a common shared model class would negate that. Likewise any of the attempts to the find callbacks would probably start imposing limits on what engineers on my team could do or use, which I really don't like.
Assuming the pass by reference approach, we were hoping to load the state and transition information from JSON. Something that might be generated by another set of tooling. In this case, I assume it would revert to being broken as we cannot define the reference as part of that file?
It may not be a deal breaker to configure inside python but it was an area we were actively looking towards.
Thanks, Shaun
Hello Shaun (@S1MP50N),
configuring state machines via JSON or YAML is no problem (see first passage in FAQ). There is a third way how callbacks can be passed: You can pass a callbacks full path in a module. Consider this example:
File structure:
- ./modules
-- __init__.py
-- foo.py
-- bar.py
./main.py
foo.py
from transitions.extensions import HierarchicalMachine
def do_something():
print("Do something foo style")
def is_always_true():
print("check something foo style")
return True
FOO_CONFIG = {
"name": "Foo",
"states": ["a", "b", "c"],
"transitions": [
{"trigger": "go", "source": "a", "dest": "b", "after": "modules.foo.do_something"},
{"trigger": "go", "source": "b", "dest": "c", "conditions": "modules.foo.is_always_true"}
],
"initial": "a"
}
class FooFSM(HierarchicalMachine):
def __init__(self):
super().__init__(**FOO_CONFIG)
bar.py
from transitions.extensions import HierarchicalMachine
def do_something():
print("Do something bar style")
def is_always_true():
print("check something bar style")
return False
BAR_CONFIG = {
"name": "Bar",
"states": ["1", "2", "3"],
"transitions": [
{"trigger": "go", "source": "1", "dest": "2", "after": "modules.bar.do_something"},
{"trigger": "go", "source": "2", "dest": "3", "conditions": "modules.bar.is_always_true"}
],
"initial": "1"
}
class BarFSM(HierarchicalMachine):
def __init__(self):
super().__init__(**BAR_CONFIG)
main.py
from transitions.extensions.nesting import HierarchicalMachine
from modules.foo import FooFSM
from modules.bar import BarFSM
CONFIG = {
"name": "HSM",
"states": [
"A",
{"name": "B", "children": FooFSM()},
{"name": "C", "children": BarFSM()},
],
"initial": "A"
}
machine = HierarchicalMachine(**CONFIG)
assert machine.is_A()
assert machine.to_B()
assert machine.is_B_a()
assert machine.go()
assert machine.go()
assert machine.is_B_c()
assert machine.to_C()
assert machine.go()
assert not machine.go()
assert machine.is_C_2()
I used a Python dict
rather than JSON but converting one into the other should not be too difficult. If you want to reuse the CONFIG
dictionaries and skip instantiation you have to alter the config slightly:
import copy
from modules.foo import FOO_CONFIG
foo_config = copy.deepcopy(FOO_CONFIG)
foo_config['name'] = 'B'
foo_config['children'] = foo_config.pop('states', [])
# ...
CONFIG = {
"name": "HSM",
"states": [
"A",
foo_config,
# ...
],
"initial": "A"
}
This has the advantage that FooFSM
and BarFSM
could derive from Machine
instead of HierarchicalMachine
.
When you use HSM variants derived from MarkupMachine
(e.g. HierarchicalMarkupMachine
; MarkupMachine
is also briefly introduced in the FAQ) you can retrieve a machine configuration with machine_config = machine.markup
. This is quite useful to keep track of machine configurations and send their configs around.
Just fyi, I am working on a little change that will allow using either 'children' or 'states' in a configuration for easier reuse. The changes mentioned in the last codeblock (renaming) will become optional.
With 0a58778, you can pass a config as a 'state entry' without further tinkering:
from modules.foo import FOO_CONFIG
from modules.bar import BAR_CONFIG
CONFIG = {
"name": "HSM",
"states": [
"A",
FOO_CONFIG,
BAR_CONFIG
],
# "transitions": [...]
"initial": "A"
}
machine = HierarchicalMachine(**CONFIG)
assert machine.is_A()
machine.to_Foo()
assert machine.is_Foo_a()
Hi @aleneum,
Brilliant! Replying to JSON stuff first; I had considered using the module level includes for bounding each sub FSM. Coming from a C++ background I was reticent to put code outside of the class for encapsulation purposes, but I think in this, the file level scoping is good enough. Appreciate the examples it makes it very clear.
With regards to the latest modifications; excellent! that is a very useful clean up of the declaration syntax and makes it much cleaner. Will definitely consider using that as part of the model configuration.
Appreciate your very quick responses and support, really looking forward to using transitions to solve the problem at hand.
I am going to close this for now as you have address my immediate issues, thank you again.
Thanks, Shaun
Hi,
I am attempting to create a set of nested hierarchical state machines. As part of the application logic handling, I want to use the "after" callback on specific transitions to perform code actions. In order to keep the functionality co-located with each discrete machine the after callback methods are defined within each Machine class. This results in an exception that the 'after' method cannot be found when invoking events on the parent fsm? This feels incorrect? It would make sense that it should reference the child Machine class for the method (either if not found in the parent or as an override of the parents method possibly).
I have created a simplified code example which demonstrates the exception.
Error
Any assistance would be appreciated.
Thanks, Shaun