transitions icon indicating copy to clipboard operation
transitions copied to clipboard

Very confusing in inherited HierarchicalMachine.

Open DYFeng opened this issue 2 months ago • 1 comments

encountering a problem when inheriting HierarchicalMachine.

from transitions.extensions import HierarchicalMachine as HSM
logging.basicConfig(level=logging.DEBUG)
logging.getLogger('transitions').setLevel(logging.INFO)

class P(HSM):
    def __init__(self):
        HSM.__init__(self, states=["A", "B"],
                     transitions=[["run", "A", "B"], ["run", "B", "A"]],
                     initial="A")

    def on_enter_A(self):
        print("enter A")

class T(HSM):
    def __init__(self):
        HSM.__init__(self, states=["Q", {"name": "P", "children": P()}],
                     transitions=[["go", "Q", "P"], ["go", "P", "Q"]],
                     initial="Q")

t = T()
t.go()


INFO:transitions.core:Finished processing state Q exit callbacks.
INFO:transitions.core:Finished processing state P enter callbacks.
Traceback (most recent call last):
  File "/home/gauss/Projects/slam_project_ws/src/slamer/scripts/task_server.py", line 32, in <module>
    t.go()
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/extensions/nesting.py", line 807, in trigger_event
    return self._process(partial(self._trigger_event, event_data, trigger))
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/core.py", line 1211, in _process
    return trigger()
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/extensions/nesting.py", line 812, in _trigger_event
    res = self._trigger_event_nested(event_data, trigger, None)
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/extensions/nesting.py", line 1157, in _trigger_event_nested
    tmp = event_data.event.trigger_nested(event_data)
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/extensions/nesting.py", line 140, in trigger_nested
    self._process(event_data)
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/extensions/nesting.py", line 154, in _process
    event_data.result = trans.execute(event_data)
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/core.py", line 277, in execute
    self._change_state(event_data)
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/extensions/nesting.py", line 281, in _change_state
    func()
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/extensions/nesting.py", line 214, in scoped_enter
    self.enter(event_data)
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/core.py", line 129, in enter
    event_data.machine.callbacks(self.on_enter, event_data)
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/core.py", line 1146, in callbacks
    self.callback(func, event_data)
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/core.py", line 1163, in callback
    func = self.resolve_callable(func, event_data)
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/core.py", line 1181, in resolve_callable
    func = getattr(event_data.model, func)
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/core.py", line 1267, in __getattr__
    state = self.get_state(target)
  File "/home/gauss/.local/lib/python3.8/site-packages/transitions/extensions/nesting.py", line 624, in get_state
    raise ValueError("State '%s' is not a registered state." % state)
ValueError: State '['A']' is not a registered state.

DYFeng avatar Apr 11 '24 14:04 DYFeng

Hello @DYFeng,

thank you for the report. I see what you are trying to achieve and how transitions behaviour is confusing here. I agree that the error message is not very helpful and transitions tries to achieve the wrong thing here.

Let's talk about what you are trying to achieve though. In your setup I'd suggest to work with 'explicit' callbacks rather than defining "on_enter_". Some things needs to be considered: There are two ways of working with callbacks: by name and by reference. Callbacks passed by name will be expected to be valid methods of the machine's model. To illustrate the difference I changed your code:

from transitions.extensions import HierarchicalMachine as HSM

class P(HSM):
    def __init__(self):
        HSM.__init__(self, states=[{"name": "A", "on_enter": ["notify", self.notify]}, "B"],   # [1]
                     transitions=[["run", "A", "B"], ["run", "B", "A"]],
                     initial="A")

    def notify(self):  # [2]
        print("State was entered")

class T(HSM):
    def __init__(self):
        HSM.__init__(self, states=["Q", {"name": "P", "children": P()}],  # [3]
                     transitions=[["go", "Q", "P"], ["go", "P", "Q"]],
                     initial="Q")

    def notify(self):  # [4]
        print("notify was overridden")

p = P()
p.to_A()  # [5]
# >>> State was entered
# >>> State was entered
t = T()
t.go()  # [6]
# >>> notify was overridden
# >>> State was entered

Instead of on_enter I pass callbacks to the state definition in P, once by reference and once by name (see [1]). If you enter state A via "p.to_A()" [5], P.notify [2] will be called twice: Once because the underlying state object has a direct reference and once when the callback name "notify" is resolved to a method of the model. In your case, both the machines P and T also act as their respective model. This is valid but not required. Consequently. T acts as a model for T but *P is not a model of/for T. What this means is illustrated in the next part.

When you pass an instance of P to T, transitions will reference the state objects owned by the create instance of P [3] which will behave similarly to our explicitly created p [5]. When t.go() is called the state object will process the event in the same way but there is a difference now. The callback name will be resolved on the model of machine T (self) and transitions will have a look for notify on t instead of p. If you comment [4] you will see that this resolution fails.

The Readme says:

(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. Note that events and transitions are also copied by reference and will be shared by both instances if you do not use the remap keyword.

The catch is that machines do not share models. If you define callbacks by name, a model that uses a machine that makes use of nested machines must implement the callbacks. If you explicitly want to call a method of a nested machine then you should use references instead.

Concerning the error/bug: on_enter_A of machines might be better passed by reference instead of by name when models are added to a machine. This happens during the instantiation. I will check wether this is suitable or cause side effects. Nevertheless, I hope the example above allows you to continue working on your task.

aleneum avatar Apr 12 '24 08:04 aleneum