python-statemachine
python-statemachine copied to clipboard
Checking if conditions are valid before going through the transition
Thank you for this library, it's been very helpful! However I've been trying to check before going through a transition to see if it was possible to do it, and I couldn't figure it out.
I've been looking through the documentation, the source code (although not 100% of it), the issues on GitHub, stack overflow, and asked an LLM how to do it but I couldn't figure it out, so excuse me if I simply missed it.
Again, I'm looking to check wether a transition is possible before I attempt to do it. I'm aware that if it is not possible, it will throw an error, but I can't rely on this behavior as if there is no error then the transition actions are triggered (which make database modifications, so not easy to rollback).
The reason why I'm trying to do that, is that I have a Django application that leverages this library to transition on object states, and from a given state, I can go to multiple other states. There should only be one state possible at any given time, but I don't want to trigger it just yet, I just want to show a button in the UI for which one it is.
The way I'm doing it right now is a bit idiotic, I only use the "cond" parameter, with only AND and NOR boolean algebra conditions, and I made a sort of (very simple) custom AST parser to call the conditions and check that they all evaluate to True.
Did I miss something? Is what I'm doing an anti-pattern? I'd be fine with a cleaner workaround.
I am actually looking for same thig. Reading the docs I expected allowed_events to check the if conditions of the transition are met, but I don't see anything like that in the code.
In my understanding the conditions should not work like this. cond is supposed to be "look before you jump" and validators "better to ask forgiveness than permission".
I don't see much difference, because in the end cond would raise an Exception too.
Event claude is convinced I should use allowed_events
I just vibe coded the solution as creating a class inheriting from StateMachine and overriding allowed_events.
from typing import List
from statemachine import Event, StateMachine
from statemachine.callbacks import CallbackGroup
class CondStateMachine(StateMachine):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@property
def allowed_events(self) -> List[Event]:
return list(
{
getattr(self, event)
for transition in self.current_state.transitions
for event in transition.events
if self._callbacks.all(CallbackGroup.COND.build_key(transition._specs))
}
)
What do you think?
Hi @wipoulou and @yelonek, sorry for the late reply.
The current implementation of allowed_events only filters events that can potentially be triggered from the current state, without checking their conditions.
I agree that the allowed_events property would be more useful if it checked conditions and returned only those that can actually be executed.
I even wrote an example using allowed_events as dynamic options for a shell CLI.
I’m convinced that checking conditions would be a better default behavior. However, since this behavior has existed for a long time, changing it might break existing user code.
Given that the library supports both def and async def for conditions, I’ll need to introduce this as a new method — not a property — to support both sync and async contexts properly. A possible solution is to add a method like enabled_events().
What do you think?
Hi, thank you for following up on this issue. The hack I had to come up with made skewed too much the maintainability calculation and I ended up reworking the state machine with a big if/else tree which lowered barrier to entry to the code, with more simple and expected behaviors. As for if this counts as a bugfix, I would say no, even if it currently doesn't feel like the correct behavior, it probably warrants to be under a new method. As such, I think your solution sounds like a good one.
A possible solution is to add a method like
enabled_events()
+1 for this. It really lacks condition validation before checking the available transitions.
Could there be something else that checks if a machine can handle an event?
For example, given this machine:
class DoorMachine(StateMachine):
closed = State(initial=True)
opened = State()
locked = State()
open = closed.to(opened)
close = opened.to(closed)
lock = closed.to(locked, cond="has_key")
unlock = locked.to(closed, cond="has_key")
has_key: bool = False
When you instantiate and look for allowed_events, it returns with [BoundEvent('open'), BoundEvent('lock')], even though lock is not really possible here (because it won't pass the has_key condition).
I think it would be nice if we could check if we can do an event such as:
dm = DoorMachine
dm.can(dm.lock) # this would be False
☝🏼 The can method would accept an event and return with a boolean. This would clean up plenty of places where I am wrapping events in try/catch blocks because I have no way to test if a machine can handle an event given the current conditions.