pythonstate-machinepytransitions

Is it possible to block execution of a trigger if the transition isn't valid?


It appears the trigger methods still run then raise the MachineError exception afterwards when transition is not valid from current state. Is there a way to block the execution of a trigger so that a call to the trigger on the model will simply raise the exception and not execute the trigger?

Sorry, forgot to mention using the overridden _checked_assignment from the FAQ which may be reason for this behavior.

from transitions import State, Machine

class StateMachine(Machine):

    def _checked_assignment(self, model, name, func):
        if hasattr(model, name):
            predefined_func = getattr(model, name)
            def nested_func(*args, **kwargs):
                predefined_func()
                func(*args, **kwargs)
            setattr(model, name, nested_func)
        else:
            setattr(model, name, func)

class Rocket(StateMachine):
    def __init__():
        StateMachine.__init__(
            self,
            states=["on_pad", "fueling", "ready", "launched", "meco", "second_stage", "orbit"],
            transitions=[
                {'trigger': 'fuel', 'source': 'on_pad', 'dest': 'fueling'},
                {'trigger': 'power_on', 'source': 'fueling', 'dest': 'ready'},
                {'trigger': 'launch', 'source': 'ready', 'dest': 'launched'}
            ],
            initial='on_pad'
        )

    def fuel():
        print("cryos loading...")

    def launch():
        print("launching")

def main():
    rocket = Rocket()
    rocket.launch()  # prints "launching" then throws Machine Error, need to block actual method execution

Solution

  • While you could wrap your callbacks with an override of Machine._checked_assignment as described in this answer, I'd recommend tying methods that should be called in the context of a transition to its callbacks. Callbacks can be called on multiple occasions during a transition as described in the documentation's chapter Callback execution order. The caveat is that callbacks must not have the same name as intended triggers but this is usually a minor setback and also enables you to add multiple callbacks to the same event. I reworked your example a bit. Rocket acts as the stateful model but the machine itself has been separated. You could also manage the state machine completely independently of your Rocket in case you plan to use multiple instances. One machine can handle multiple stateful objects. Furthermore, I renamed your callbacks slightly and passed them to the before keyword of the transitions. As mentioned earlier, this could also be a list ({'before': ['on_launch']} is also valid). This way, they will be called right before the transition will happen and will not be called when a) Rocket is not in the correct state or b) condition checks for the transition in question failed.

    from transitions import Machine, MachineError
    
    
    class Rocket:
        
        def __init__(self):
            self.machine = Machine(
                self,
                states=["on_pad", "fueling", "ready", "launched", "meco", "second_stage", "orbit"],
                transitions=[
                    {'trigger': 'fuel', 'source': 'on_pad', 'dest': 'fueling', 'before': 'on_fueling'},
                    {'trigger': 'power_on', 'source': 'fueling', 'dest': 'ready'},
                    {'trigger': 'launch', 'source': 'ready', 'dest': 'launched', 'before': 'on_launch'}
                ],
                initial='on_pad'
            )
    
        def on_fueling(self):
            print("cryos loading...")
    
        def on_launch(self):
            print("launching")
    
    
    rocket = Rocket()
    try:
        rocket.launch()
        assert False
    except MachineError:
        pass
    
    rocket.fuel()  # >>> cryos loading...
    rocket.power_on() 
    rocket.launch()  # >>> launching