StateMachine 1.0.1

January 11, 2023

Welcome to StateMachine 1.0.1!

This version is a huge refactoring adding a lot of new and exciting features. We hope that you enjoy it.

These release notes cover the new features in 1.0, as well as some backward incompatible changes you’ll want to be aware of when upgrading from StateMachine 0.9.0 or earlier. We’ve begun the deprecation process for some features.

Python compatibility in 1.0

StateMachine 1.0 supports Python 2.7, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10, and 3.11.

This is the last release to support Python 2.7, 3.5, and 3.6.

What’s new in 1.0

Added validators and Guards

Transitions now support cond and unless parameters, to restrict the execution.

    class ApprovalMachine(StateMachine):
        "A workflow machine"
        requested = State("Requested", initial=True)
        accepted = State("Accepted")
        rejected = State("Rejected")
        completed = State("Completed")

        validate = requested.to(accepted, cond="is_ok") | requested.to(rejected)

See also

See Validators and guards for more details.

Support for diagrams

You can generate diagrams from your state machine.

Example:

OrderControl

See also

See Diagrams for more details.

Unified dispatch mechanism for callbacks (actions and guards)

Every single callback, being Actions or Guards, is now handled equally by the library.

Also, we’ve improved the internals in a way that you can implement your callbacks with any number of arbitrary positional or keyword arguments (*args, **kwargs), and the dispatch will match the available arguments with your method signature.

This means that if on your on_enter_<state>() or on_execute_<event>() method, you also need to know the source (State), or the event (Event), or access a keyword argument passed with the trigger, you’re covered. Just add this parameter to the method and It will be passed by the dispatch mechanics.

Example of what’s available:

def action_or_guard_method_name(self, *args, event_data, event, source, state, model, **kwargs):
    pass

See also

See Dynamic dispatch for more details.

Add observers to a running StateMachine

Observers are a way do generically add behavior to a StateMachine without changing it’s internal implementation.

The StateMachine itself is registered as an observer, so by using StateMachine.add_observer() an external object can have the same level of functionalities provided to the built-in class.

See also

See Observers for more details.

Minor features in 1.0

  • Fixed mypy complaining about incorrect type for StateMachine class.

  • The initial State is now entered when the machine starts. The Actions, if defined, on_enter_state and on_enter_<state> are now called.

Backward incompatible changes in 1.0

Multiple targets from the same origin state

Prior to this release, as we didn’t have Validators and guards, there wasn’t an elegant way to declare multiple target states starting from the same pair (event, state). But the library allowed a near-hackish way, by declaring a target state as the result of the on_<event> callback.

So, the previous code (not valid anymore):

class ApprovalMachine(StateMachine):
    "A workflow machine"
    requested = State('Requested', initial=True)
    accepted = State('Accepted')
    rejected = State('Rejected')

    validate = requested.to(accepted, rejected)

    def on_validate(self, current_time):
        if self.model.is_ok():
            self.model.accepted_at = current_time
            return self.accepted
        else:
            return self.rejected

Should be rewritten to use Guards, like this:

class ApprovalMachine(StateMachine):
    "A workflow machine"
    requested = State("Requested", initial=True)
    accepted = State("Accepted")
    rejected = State("Rejected")

    validate = requested.to(accepted, conditions="is_ok") | requested.to(rejected)

    def on_validate(self, current_time):
        self.model.accepted_at = current_time

See also

See Validators and guards of more details.

StateMachine now enters the initial state

This issue was reported at #265.

Now StateMachine will execute the actions associated with the on_enter_state and on_enter_<state>` when initialized if they exist.

See also

See State actions for more details.

Integrity is checked at class definition

Statemachine integrity checks are now performed at class declaration (import time) instead of on instance creation. This allows early feedback on invalid definitions.

This was the previous behavior, you only got an error when trying to instantiate a StateMachine:

class CampaignMachine(StateMachine):
    "A workflow machine"
    draft = State('Draft', initial=True)
    producing = State('Being produced')
    closed = State('Closed', initial=True)  # Should raise an Exception when instantiated

    add_job = draft.to(draft) | producing.to(producing)
    produce = draft.to(producing)
    deliver = producing.to(closed)

with pytest.raises(exceptions.InvalidDefinition):
    CampaignMachine()

Not this is performed as the class definition is performed:

with pytest.raises(exceptions.InvalidDefinition):

    class CampaignMachine(StateMachine):
        "A workflow machine"
        draft = State("Draft", initial=True)
        producing = State("Being produced")
        closed = State(
            "Closed", initial=True
        )  # Should raise an Exception right after the class is defined

        add_job = draft.to(draft) | producing.to(producing)
        produce = draft.to(producing)
        deliver = producing.to(closed)

Other backward incompatible changes in 1.0

  • Due to the check validations and setup performed at the machine initialization, it’s now harder to perform monkey-patching to add callbacks at runtime (not a bad thing after all).

  • TransitionNotAllowed changed internal attr from transition to event.

  • CombinedTransition does not exist anymore. State now holds a flat Transition list called TransitionList that implements de OR operator. This turns a valid StateMachine traversal much easier: [transition for state in machine.states for transition in state.transitions].

  • StateMachine.get_transition is removed. See Event.

  • The previous exceptions MultipleStatesFound and MultipleTransitionCallbacksFound are removed. Since now you can have more than one callback defined to the same transition.

  • on_enter_state and on_exit_state now accepts any combination of parameters following the Dynamic dispatch rules. Previously it only accepted the state param.

  • Transition.__init__ param on_execute renamed to simply on, and now follows the Dynamic dispatch.

  • Transition.destinations removed in favor of Transition.target (following SCXML convention). Now each transition only points to a unique target. Each source->target pair is held by a single Transition.

Deprecated features in 1.0

Statemachine class

  • StateMachine.run is deprecated in favor of StateMachine.send.

  • StateMachine.allowed_transitions is deprecated in favor of StateMachine.allowed_events.

  • Statemachine.is_<state> is deprecated in favor of StateMachine.<state>.is_active.

State class

  • State.identification is deprecated in favor of State.id.