Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Very confusing in inherited HierarchicalMachine. #652

Closed
DYFeng opened this issue Apr 11, 2024 · 2 comments
Closed

Very confusing in inherited HierarchicalMachine. #652

DYFeng opened this issue Apr 11, 2024 · 2 comments
Assignees
Labels

Comments

@DYFeng
Copy link

DYFeng commented Apr 11, 2024

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.
@aleneum
Copy link
Member

aleneum commented Apr 12, 2024

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
Copy link
Member

aleneum commented Jun 10, 2024

I tested passing on_enter_<state> by reference instead of by name but this has unwanted side effects: When a model is removed from the machine, its callback is not removed from State and thus called when any model enterst <state> the next time. I'd say this causes unfortunately more problems than it solves. For now, I'd say explicitly passing function references to a transition is the better options.

I am closing this issue. Feel free to comment anyway if this issue still persists. I will reopen this issue or create a new one should a feature request or bug emerge.

@aleneum aleneum closed this as completed Jun 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants