Skip to content

v2.5.0

Latest
Compare
Choose a tag to compare
@fgmacedo fgmacedo released this 04 Jun 01:01
· 6 commits to develop since this release

StateMachine 2.5.0

December 3, 2024

What's new in 2.5.0

This release improves Condition expressions and explicit definition of Events and introduces the helper State.from_.any().

Python compatibility in 2.5.0

StateMachine 2.5.0 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, and 3.13.

Helper to declare transition from any state

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
True

Allowed events are now bounded to the state machine instance

Since 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()
Choose an action:
0 - Suspend
1 - Overdraft
2 - Close account
User input: 0
Running the option 0 - Suspend
Choose an action:
0 - Activate
1 - Close account
User input: 0
Running the option 0 - Activate
Choose an action:
0 - Suspend
1 - Overdraft
2 - Close account
User input: 2
Running the option 2 - Close account
Account has been closed.

>>> print(f"SM is in {sm.current_state.name} state.")
SM is in Closed state.

Conditions expressions in 2.5.0

This release adds support for comparison operators into Condition expressions.

The following comparison operators are supported:

  1. > — Greather than.
  2. >= — Greather than or equal.
  3. == — Equal.
  4. != — Not equal.
  5. < — Lower than.
  6. <= — 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()
Traceback (most recent call last):
TransitionNotAllowed: Can't finish order when in Start.

>>> sm.order_value = 135.0
>>> sm.submit()
>>> sm.current_state.id
'end'
See [Condition expressions](https://python-statemachine.readthedocs.io/en/v2.5.0/guards.html#condition-expressions)  for more details or take a look at the {ref}`sphx_glr_auto_examples_lor_machine.py` example.

Decorator callbacks with explicit event creation in 2.5.0

Now you can add callbacks using the decorator syntax using 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...'

Bugfixes in 2.5.0

  • Fixes #500 issue adding support for Pickle.

Misc in 2.5.0

  • We're now using uv #491.
  • Simplification of the engines code #498.
  • The dispatcher and callback modules where refactored with improved separation of concerns #490.