December 3, 2024
This release improves {ref}Condition expressions and explicit definition of {ref}Events and introduces the helper State.from_.any().
StateMachine 2.5.0 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, and 3.13.
You can now declare that a state is accessible from any other state with a simple constructor. Using State.from_.any(), the state machine meta class automatically creates transitions from all non-final states to the target state.
Furthermore, both State.from_.itself() and State.to.itself() have been refactored to support type hints and are now fully visible for code completion in your preferred editor.
>>> from statemachine import Event
>>> class AccountStateMachine(StateMachine):
... active = State("Active", initial=True)
... suspended = State("Suspended")
... overdrawn = State("Overdrawn")
... closed = State("Closed", final=True)
...
... suspend = Event(active.to(suspended))
... activate = Event(suspended.to(active))
... overdraft = Event(active.to(overdrawn))
... resolve_overdraft = Event(overdrawn.to(active))
...
... close_account = Event(closed.from_.any(cond="can_close_account"))
...
... can_close_account: bool = True
...
... def on_close_account(self):
... print("Account has been closed.")
>>> sm = AccountStateMachine()
>>> sm.close_account()
Account has been closed.
>>> sm.closed.is_active
TrueSince 2.0, the state machine can return a list of allowed events given the current state:
>>> sm = AccountStateMachine()
>>> [str(e) for e in sm.allowed_events]
['suspend', 'overdraft', 'close_account']
Event instances are now bound to the state machine instance, allowing you to pass the event by reference and call it like a method, which triggers the event in the state machine.
You can think of the event as an implementation of the command design pattern.
On this example, we iterate until the state machine reaches a final state, listing the current state allowed events and executing the simulated user choice:
import random
random.seed("15")
sm = AccountStateMachine()
while not sm.current_state.final:
allowed_events = sm.allowed_events
print("Choose an action: ")
for idx, event in enumerate(allowed_events):
print(f"{idx} - {event.name}")
user_input = random.randint(0, len(allowed_events)-1)
print(f"User input: {user_input}")
event = allowed_events[user_input]
print(f"Running the option {user_input} - {event.name}")
event()
print(f"SM is in {sm.current_state.name} state.")
# SM is in Closed state.This release adds support for comparison operators into {ref}Condition expressions.
The following comparison operators are supported:
>— Greather than.>=— Greather than or equal.==— Equal.!=— Not equal.<— Lower than.<=— Lower than or equal.
Example:
from statemachine import StateMachine, State, Event
class AnyConditionSM(StateMachine):
start = State(initial=True)
end = State(final=True)
submit = Event(
start.to(end, cond="order_value > 100"),
name="finish order",
)
order_value: float = 0
sm = AnyConditionSM()
sm.submit()
# TransitionNotAllowed: Can't finish order when in Start.
sm.order_value = 135.0
sm.submit()
sm.current_state.id
# 'end'See {ref}`Condition expressions` for more details or take a look at the {ref}`sphx_glr_auto_examples_lor_machine.py` example.
Now you can add callbacks using the decorator syntax using {ref}Events. Note that this syntax is also available without the explicit Event.
>>> from statemachine import StateMachine, State, Event
>>> class StartMachine(StateMachine):
... created = State(initial=True)
... started = State(final=True)
...
... start = Event(created.to(started), name="Launch the machine")
...
... @start.on
... def call_service(self):
... return "calling..."
...
>>> sm = StartMachine()
>>> sm.start()
'calling...'
- Fixes #500 issue adding support for Pickle.