2

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.

Gulzar
  • 23,452
  • 27
  • 113
  • 201

1 Answers1

3

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)
aleneum
  • 2,083
  • 12
  • 29
  • getting back to work in a few days, will check this out then and probably accept, thanks! – Gulzar Jul 01 '21 at 11:06
  • Also, is there *"a way"* to read the docs? I read quite a mass there, and didn't stumble on the event technique. Should I have searched for something? I'm not assuming the authors expect a user to read *all* of the docs? – Gulzar Jul 01 '21 at 11:09
  • send_event is documented in the 'Passing Data' section. Writing good documentation is tough. If you have any feedback considering how it could be improved, creating PRs or Issues (or writing SO post as you did :) ) is much appreciated! – aleneum Jul 01 '21 at 15:22
  • I still hadn't gotten to check this because I lack one more thing - Can I be notified by the machine, via a callback, when some state occured? For example, I want `on_enter_A` to also trigger an *external* callback. Is there an API to subscribe to `on_enter_A` with multiple callbacks, externally? I hope this is clear... – Gulzar Jul 04 '21 at 15:57
  • What I mean is, I want to decouple the state and transitions from the outside world that wants its own events to occur due to events inside the machine. I want to let the machine notify the outside world of the events. – Gulzar Jul 04 '21 at 15:59
  • Handling this alone would be `def on_enter_A(self, event): self._on_enter_A_callback1(event.kwargs["data1"])` and also handling subscribing and unsubscribing for the event. – Gulzar Jul 04 '21 at 16:00
  • Have a look at the documentation section about [state callbacks](https://github.com/pytransitions/transitions#callbacks). You can add multiple callbacks to an 'enter/exit state' event. You can also pass references and module imports as callbacks (see [callback resolution](https://github.com/pytransitions/transitions#callable-resolution)). If this does not help, maybe create a new SO question considering multiple/external callbacks because this one is about passing parameters. – aleneum Jul 05 '21 at 07:25
  • Please see [the followup](https://stackoverflow.com/questions/68259677/how-to-externalize-subscriptions-to-states-of-a-transitions-state-machine) – Gulzar Jul 05 '21 at 16:56
  • I was not able to find how to dynamically add state subscriptions in the doc you linked to, I honestly tried to read the relevant parts multiple times and could not find the relevant line... Even so, this *may* not be enough, but maybe I don't understand the intended design. – Gulzar Jul 05 '21 at 17:04