pythontransitionstate-machinefsmpytransitions

How to pass parameters to on_enter callbacks in `transitions` library?


I want to use transitions, and need a rather trivial feature I could not find in the docs, and was wondering if it was implemented:

I want to define a on_enter callback on some state, but pass a parameter to that callback. At least to know from which state I am entering the state.

From the docs:

class Matter(object):
    def say_hello(self): print("hello, new state!")
    def say_goodbye(self): print("goodbye, old state!")

lump = Matter()

# Same states as above, but now we give StateA an exit callback
states = [
    State(name='solid', on_exit=['say_goodbye']),
    'liquid',
    { 'name': 'gas', 'on_exit': ['say_goodbye']}
    ]

machine = Machine(lump, states=states)
machine.add_transition('sublimate', 'solid', 'gas')

# Callbacks can also be added after initialization using
# the dynamically added on_enter_ and on_exit_ methods.
# Note that the initial call to add the callback is made
# on the Machine and not on the model.
machine.on_enter_gas('say_hello')

# Test out the callbacks...
machine.set_state('solid')
lump.sublimate()
>>> 'goodbye, old state!'
>>> 'hello, new state!'

What I lack is

def say_hello(self, param): print(f"hello, new state! here is your param: {param}")

Can this be done nicely somehow?

An obvious bad solution would be to keep a self._last_state argument and maintain that myself.
I am looking for something built-in.


Solution

  • The section of transitions' documentation called Passing Data:

    ... you can pass any positional or keyword arguments directly to the trigger methods (created when you call add_transition()) [...] You can pass any number of arguments you like to the trigger. There is one important limitation to this approach: every callback function triggered by the state transition must be able to handle all of the arguments.

    For your particular example this could look like this:

    from transitions import Machine
    
    class Matter(object):
        def say_hello(self, param):
            print(f"hello, new state! Here is your param: {param}")
    
        # Every callback MUST be able to process possible callback parameters
        # If parameters are not needed, just use *args and **kwargs in the definition
        def say_goodbye(self, *args):
            print("goodbye, old state!")
    
    
    lump = Matter()
    machine = Machine(lump, states=[{'name': 'solid', 'on_exit': 'say_goodbye'},
                                    'liquid',
                                    {'name': 'gas', 'on_enter': 'say_hello'}],
                      transitions=[['sublimate', 'solid', 'gas']], initial='solid')
    
    # pass param as arg
    lump.sublimate(lump.state)
    # or as kwarg
    # lump.sublimate(param=lump.state)
    
    

    There is also a second way to pass data by passing send_event=True in the Machine constructor. This will change the way how transitions passes trigger parameters to callbacks:

    If you set send_event=True at Machine initialization, all arguments to the triggers will be wrapped in an EventData instance and passed on to every callback. (The EventData object also maintains internal references to the source state, model, transition, machine, and trigger associated with the event, in case you need to access these for anything.)

    This might be more suitable for your use case since an EventData object also contains information about the executed transition which contains the name of the source state:

    from transitions import Machine, EventData
    
    class Matter(object):
        def say_hello(self, event: EventData):
            print(f"hello, new state! Here is your param: {event.kwargs['param']}. "
                  f"I came here from state '{event.transition.source}'.")
    
        def say_goodbye(self, event):
            print("goodbye, old state!")
    
    
    lump = Matter()
    machine = Machine(lump, states=[{'name': 'solid', 'on_exit': 'say_goodbye'},
                                    'liquid',
                                    {'name': 'gas', 'on_enter': 'say_hello'}],
                      transitions=[['sublimate', 'solid', 'gas']], initial='solid', send_event=True)
    
    lump.sublimate(param=42)