March 5, 2023
Welcome to StateMachine 2.0.0!
This version is the first to take advantage of the Python3 improvements and is a huge internal refactoring removing the deprecated features on 1.*. We hope that you enjoy it.
These release notes cover the , as well as some backward incompatible changes you'll want to be aware of when upgrading from StateMachine 1.*.
StateMachine 2.0 supports Python 3.7, 3.8, 3.9, 3.10, and 3.11.
There are now two distinct methods for processing events in the library. The new default is to run in
{ref}RTC model to be compliant with the specs, where the {ref}event is put on a queue before processing.
You can also configure your state machine to run back in {ref}Non-RTC model, where the {ref}event will
be run immediately and nested events will be chained.
This means that the state machine now completes all the actions associated with an event before moving on to the next event. Even if you trigger an event inside an action.
See {ref}`processing model` for more details.
{ref}State names are now by default derived from the class variable that they are assigned to.
You can keep declaring explicit names, but we encourage you to only assign a name
when it is different than the one derived from its id.
>>> from statemachine import StateMachine, State
>>> class ApprovalMachine(StateMachine):
... pending = State(initial=True)
... waiting_approval = State()
... approved = State(final=True)
...
... start = pending.to(waiting_approval)
... approve = waiting_approval.to(approved)
...
>>> ApprovalMachine.pending.name
'Pending'
>>> ApprovalMachine.waiting_approval.name
'Waiting approval'
>>> ApprovalMachine.approved.name
'Approved'An internal transition is like a {ref}self transition, but in contrast, no entry or exit actions
are ever executed as a result of an internal transition.
>>> from statemachine import StateMachine, State
>>> class TestStateMachine(StateMachine):
... initial = State(initial=True)
...
... loop = initial.to.itself(internal=True)See {ref}`internal transition` for more details.
You can now instantiate a {ref}StateMachine with allow_event_without_transition=True,
so the state machine will allow triggering events that may not lead to a state {ref}transition,
including tolerance to unknown {ref}event triggers.
The default value is False, that keeps the backward compatible behavior of when an
event does not result in a {ref}transition, an exception TransitionNotAllowed will be raised.
>>> import pytest
>>> pytest.skip("Since 3.0.0 `allow_event_without_transition` is now a class attribute.")
>>> sm = ApprovalMachine(allow_event_without_transition=True)
>>> sm.send("unknow_event_name")
>>> sm.pending.is_active
True
>>> sm.send("approve")
>>> sm.pending.is_active
True
>>> sm.send("start")
>>> sm.waiting_approval.is_active
True
Now the library messages can be translated into any language.
See {ref}Add a translation on how to contribute with translations.
- Modernization of the development tools to use linters and improved mypy support.
- #342: Guards now supports the evaluation of truthy and falsy values.
- #342: Assignment of
Transitionguards using decorators is now possible. - #331: Added a way to generate diagrams using QuickChart.io instead of GraphViz. See {ref}
diagramsfor more details. - #353: Support for abstract state machine classes, so you can subclass
StateMachineto add behavior on your own base class. AbstractStateMachinecannot be instantiated. - #355: Now is possible to trigger an event as an action by registering the event name as the callback param.
- #341: Fix dynamic dispatch on methods with default parameters.
- #365: Fix transition with multiple events was calling actions of all events.
- Dropped support for Django <=
1.6for auto-discovering and registeringStateMachineclasses to be used on {ref}django integration.
Prior to #365, when you {ref}Declare transition actions by naming convention, all callbacks of the transition were called even if the triggered event was not the one that originated the transition.
This behavior was fixed in this release. Now, only the transitions associated with the triggered event or directly assigned to the transition are called.
Consider the following state machine as an example:
>>> from statemachine import State
>>> from statemachine import StateMachine
>>> class TrafficLightMachine(StateMachine):
... "A traffic light machine"
... green = State(initial=True)
... yellow = State()
... red = State()
...
... slowdown = green.to(yellow)
... stop = yellow.to(red)
... go = red.to(green)
...
... cycle = slowdown | stop | go
...
... def before_slowdown(self):
... print("Slowdown")
...
... def before_cycle(self, event: str, source: State, target: State):
... print(f"Running {event} from {source.id} to {target.id}")Before, if you send the cycle event, the behavior was to also trigger actions associated with
slowdown, because they're sharing the same instance of {ref}Transition:
>>> sm = TrafficLightMachine()
>>> sm.send("cycle") # doctest: +SKIP
Slowdown
Running cycle from green to yellowNow the behavior is to only execute actions bound to the triggered {ref}event or directly
associated to the {ref}Transition:
>>> sm = TrafficLightMachine()
>>> sm.send("cycle")
Running cycle from green to yellowIf you want to emulate the previous behavior, consider one of these alternatives.
You can {ref}Bind transition actions using params or {ref}Bind transition actions using decorator syntax:
>>> from statemachine import State
>>> from statemachine import StateMachine
>>> class TrafficLightMachine(StateMachine):
... "A traffic light machine"
... green = State(initial=True)
... yellow = State()
... red = State()
...
... slowdown = green.to(yellow, before="do_before_slowdown") # assign to the transition
... stop = yellow.to(red)
... go = red.to(green)
...
... cycle = slowdown | stop | go
...
... def do_before_slowdown(self):
... print("Slowdown")
...
... @stop.before # assign to the transition
... def do_before_stop(self):
... print("Stop")
...
... def before_cycle(self, event: str, source: State, target: State):
... print(f"Running {event} from {source.id} to {target.id}")You can go an step further and if the events are not called externally, get rid of them and put the actions directly on the transitions:
>>> from statemachine import State
>>> from statemachine import StateMachine
>>> class TrafficLightMachine(StateMachine):
... "A traffic light machine"
... green = State(initial=True)
... yellow = State()
... red = State()
...
... cycle = (
... green.to(yellow, before="slowdown")
... | yellow.to(red, before="stop")
... | red.to(green, before="go")
... )
...
... def slowdown(self):
... print("Slowdown")
...
... def stop(self):
... print("Stop")
...
... def go(self):
... print("Go")
...
... def before_cycle(self, event: str, source: State, target: State):
... print(f"Running {event} from {source.id} to {target.id}")>>> sm = TrafficLightMachine()
>>> [sm.send("cycle") for _ in range(3)]
Slowdown
Running cycle from green to yellow
Stop
Running cycle from yellow to red
Go
Running cycle from red to green
[[None, None], [None, None], [None, None]]While we've figured out a way to keep near complete backwards compatible changes to the new
{ref}Run to completion (RTC) by default feature (all built-in examples run without change),
if you encounter problems when upgrading to this version, you can still switch back to the old
{ref}Non-RTC model. Be aware that we may remove the {ref}Non-RTC model in the future.
from tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
sm.run("cycle")Should become:
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
>>> sm = TrafficLightMachine()
>>> sm.send("cycle")
Running cycle from green to yellowfrom tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
assert [t.name for t in sm.allowed_transitions] == ["cycle"]Should become:
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
>>> sm = TrafficLightMachine()
>>> assert [t.name for t in sm.allowed_events] == ["cycle"]from tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
assert sm.is_greenShould become:
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
>>> sm = TrafficLightMachine()
>>> assert sm.green.is_activefrom tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
assert sm.current_state.identification == "green"Should become:
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
>>> sm = TrafficLightMachine()
>>> assert sm.current_state.id == "green"