pytransitions / transitions

A lightweight, object-oriented finite state machine implementation in Python with many extensions
MIT License
5.68k stars 530 forks source link

'machine.on_exit' and 'machine.on_enter' callbacks? #374

Closed sergei3000 closed 4 years ago

sergei3000 commented 4 years ago

As a feature request.

It would be great if it was possible to declare callbacks 'on_exit' and 'on_enter' on entire model. Similar to 'machine.prepare_event', 'machine.before_state_change', 'machine.after_state_change' and 'machine.finalize_event'.

aleneum commented 4 years ago

Hello @sergei3000,

if this still is a valid issue for you: Could you provide a minimal example where passing the callback to before_state_change and/or after_state_change is not sufficient?

aleneum commented 4 years ago

Will close that issue due to inactivity. If this problem is still relevant to you, feel free to comment. I will reopen the issue when necessary.

sergei3000 commented 4 years ago

Sorry for not responding. I moved to another project at the beginning of the year, so it's not relevant for me now, and I just missed your previous comment. But I remember I had to go a harder way without such a feature. Can't remember the exact case for now, sorry. Will try to search in code a bit later.

pylipp commented 3 years ago

Hej @aleneum, I have a use case when I want a machine-global callback to be executed after state change only if the transition is not reflexive or internal. When I use the after_state_change parameter, the callback still fires, see here:

import transitions

def info():
    print("changed")

machine = transitions.Machine(
    states=["initial"],
    transitions=[["test", "initial", "="]],  # same issue with internal transition (dest=None)
    initial="initial",
    after_state_change=info),
)

machine.test() # prints "changed"
print(machine.state)

One solution could be to have a global machine on_exit or exit_state callback, as suggested above.

Alternatively I'm fine with adding some logic to my after_state_change callback. I was wondering whether there is another way to find out the previous state, or to find out whether a transition is reflexive or internal:

import transitions

class System:
    def __init__(self):
        self.machine = transitions.Machine(
            states=["initial"],
            transitions=[["test", "initial", "="]],
            initial="initial",
            before_state_change=self.backup_state,
            after_state_change=self.info,
            model=self,
        )
        self.previous_state = None

    def backup_state(self):
        self.previous_state = self.state

    def info(self):
        if self.previous_state != self.state:
            print("changed")

system = System()
system.test()  # does not print anything
print(system.state)
aleneum commented 3 years ago

Hello @pylipp,

When I use the after_state_change parameter, the callback still fires

I get your reasoning here. The naming after/before_state_change suggests that an actual state change has happened. The ReadMe defines it the following way:

Default actions meant to be executed before or after every transition can be passed to Machine during initialization with before_state_change and after_state_change respectively.

The naming might be a bit misleading, I agree. This is some 'historical baggage' since those keywords were introduced before internal transitions were supported. Personally, I would consider a reflexive transition--where the state is exited and entered again--as a state change. One could argue that internal transitions by definition cause no state change though. Callbacks defined on Machine (or better in the constructor) are currently quite 'transition-focused' while 'state-focused' callbacks such as on_enter_<state>/on_exit_<state> are defined by the model(s) (or added via Machine methods on_enter/exit_<state> after initialization). For consistency, it might be more comprehensible when we treat on_enter and on_exit defined on the model as global callbacks (and maybe allow machine.on_enter to add callbacks to all current states)

. I was wondering whether there is another way to find out the previous state, or to find out whether a transition is reflexive or internal:

When you pass send_event=True to the machine contructor, every callback will be handed an EventData object which contains the currently evaluated Transition. Such a Transition has a source and dest property. For internal transitions, dest is None.

from transitions import Machine

def conditional_task(event_data):
    if event_data.transition.dest is not None and event_data.transition.source != event_data.transition.dest:
        print("do the thing")
    else:
        print("don't do the thing")

transitions = [
    ['internal', 'B', None],
    ['reflexive', 'B', '='],
    ['external', 'A', 'B']
]

machine = Machine(states=['A', 'B'], transitions=transitions, initial='A', send_event=True,
                  after_state_change=conditional_task)

machine.external()  # >>> do the thing
machine.reflexive()  # >>> don't do the thing
machine.internal()  # >>> don't do the thing
machine.to_A()  # >>> do the thing 
pylipp commented 3 years ago

Thank you for the quick and elaborate answer! As a work-around for not having to store the source state of a transition it is fine for me to use send_event=True.

From your explanation I can't really read whether you'd be in favor of updating the global machine callbacks, or not, however I'd leave it up to you ;)