Skip to content

Latest commit

 

History

History
460 lines (340 loc) · 12.3 KB

File metadata and controls

460 lines (340 loc) · 12.3 KB

(events)= (event)=

Events

New to statecharts? See [](concepts.md) for an overview of how states,
transitions, events, and actions fit together.

An event is a named signal that drives the state machine forward. When you assign a transition to a class-level name, that name becomes an event — the library creates an Event object automatically. Events are the external interface of your machine: callers send event names, and the machine decides which transitions to take.

(declaring-events)=

Declaring events

The simplest way to declare an event is by assigning a transition to a name:

>>> from statemachine import Event, State, StateChart

>>> class SimpleSM(StateChart):
...     initial = State(initial=True)
...     final = State(final=True)
...
...     start = initial.to(final)

>>> isinstance(SimpleSM.start, Event)
True

The name start is automatically converted to an Event with id="start". Multiple transitions can share the same event using the | operator:

>>> class TrafficLight(StateChart):
...     green = State(initial=True)
...     yellow = State()
...     red = State()
...
...     cycle = green.to(yellow) | yellow.to(red) | red.to(green)

>>> sm = TrafficLight()
>>> sm.send("cycle")
>>> sm.yellow.is_active
True

For better IDE support (autocompletion, type checking) or to set a human-readable display name, use the Event class explicitly:

>>> class SimpleSM(StateChart):
...     initial = State(initial=True)
...     final = State(final=True)
...
...     start = Event(initial.to(final), name="Start the machine")

>>> SimpleSM.start.name
'Start the machine'

>>> SimpleSM.start.id
'start'

(event-identity)=

Event identity: id vs name

Every event has two string properties:

  • id — the programmatic identifier, derived from the class attribute name. Use this in send(), guards, and comparisons.
  • name — a human-readable label for display purposes. Auto-generated from the id by replacing _ and . with spaces and capitalizing the first word. You can override the automatic name by passing name= explicitly when declaring the event:
>>> TrafficLight.cycle.id
'cycle'

>>> TrafficLight.cycle.name
'Cycle'

>>> class Example(StateChart):
...     on = State(initial=True)
...     off = State(final=True)
...     shut_down = Event(on.to(off), name="Shut the system down")

>>> Example.shut_down.name
'Shut the system down'
Always use `event.id` for programmatic checks. The `name` property is intended
for UI display and may differ from the `id`.

(triggering-events)= (triggering events)=

Triggering events

Once declared, events are triggered on a {ref}StateChart <statechart> instance in two ways:

  • As a method call: sm.cycle() — when the event name is known at development time.
  • Via send(): sm.send("cycle") — when the event name is dynamic (e.g., from user input, a message queue, or a data file).

Both styles produce the same result. The machine evaluates {ref}guard conditions <validators and guards>, executes {ref}actions, and updates the {ref}configuration <querying-configuration>.

See {ref}`sending-events` for the full runtime API — `send()`, `raise_()`,
delayed events, and cancellation.

(events-as-method-protocol)=

Events as a method-call protocol

Because every event is a callable method, a state machine can act as a guard for the order in which your object's methods may be called. Set allow_event_without_transition = False so that any call that doesn't match an enabled transition raises TransitionNotAllowed instead of being silently ignored. The transitions then describe the legal sequence, and the machine enforces it for you:

>>> from statemachine import State, StateChart

>>> class Connection(StateChart):
...     "A client whose methods can only be called in a valid order."
...     allow_event_without_transition = False  # reject any unexpected call
...
...     disconnected = State(initial=True)
...     connected = State()
...     authenticated = State()
...
...     connect = disconnected.to(connected)
...     authenticate = connected.to(authenticated)
...     disconnect = connected.to(disconnected) | authenticated.to(disconnected)
...
...     def on_connect(self):
...         print("socket opened")
...
...     def on_authenticate(self):
...         print("credentials accepted")

Calling the methods in a valid order works:

>>> conn = Connection()
>>> conn.connect()
socket opened
>>> conn.authenticate()
credentials accepted
>>> "authenticated" in conn.configuration_values
True

A call that isn't valid in the current state raises, so an out-of-order method call fails loudly instead of doing nothing:

>>> fresh = Connection()

>>> fresh.authenticate()  # must connect first
Traceback (most recent call last):
    ...
statemachine.exceptions.TransitionNotAllowed: Can't Authenticate when in Disconnected.

The same protection applies to unknown event names sent dynamically:

>>> fresh.send("teleport")
Traceback (most recent call last):
    ...
statemachine.exceptions.TransitionNotAllowed: Can't teleport when in Disconnected.
{ref}`behaviour` explains `allow_event_without_transition` and the other
class-level flags that switch between this strict mode and the tolerant,
event-driven default.

(event-parameter)=

The event parameter on transitions

Each transition accepts an optional event parameter that binds it to a specific event, overriding the default (which is the class-level attribute name). This lets individual transitions within a group respond to their own event identifiers:

>>> from statemachine import Event, State, StateChart

>>> class TrafficLightMachine(StateChart):
...     green = State(initial=True)
...     yellow = State()
...     red = State()
...
...     slowdown = Event(name="Slowing down")
...
...     cycle = Event(
...         green.to(yellow, event=slowdown)
...         | yellow.to(red, event="stop")
...         | red.to(green, event="go"),
...         name="Loop",
...     )

>>> sm = TrafficLightMachine()

>>> sm.send("cycle")  # umbrella event — dispatches green→yellow
>>> sm.yellow.is_active
True

>>> sm.send("stop")   # individual event — dispatches yellow→red
>>> sm.red.is_active
True

>>> sm.send("go")     # individual event — dispatches red→green
>>> sm.green.is_active
True

The event parameter accepts a string, an Event instance, a reference to a previously declared Event (like slowdown above), or a list of any of these. A space-separated string is also accepted and split into individual events automatically:

>>> class MultiEvent(StateChart):
...     a = State(initial=True)
...     b = State(final=True)
...
...     # Both forms are equivalent — the transition responds to "move", "go" and "start"
...     move = a.to(b, event=["go", "start"])

>>> sm = MultiEvent()
>>> sm.send("move")
>>> sm.b.is_active
True

>>> sm = MultiEvent()
>>> sm.send("go")
>>> sm.b.is_active
True

>>> sm = MultiEvent()
>>> sm.send("start")
>>> sm.b.is_active
True
This is an advanced feature. Most state machines only need the simple
`name = source.to(target)` form. Use the `event` parameter when you need
fine-grained control over event routing within a composite transition group.

(done-state-events)=

Automatic events

The engine generates certain events automatically in response to structural changes. You don't send these yourself — you define transitions that react to them.

done.state events

When a {ref}compound state's <compound-states> final child is entered, the engine queues a done.state.{parent_id} internal event. Define a transition for this event to react when a compound's work is complete:

>>> from statemachine import State, StateChart

>>> class QuestWithDone(StateChart):
...     class quest(State.Compound):
...         traveling = State(initial=True)
...         arrived = State(final=True)
...         finish = traveling.to(arrived)
...     celebration = State(final=True)
...     done_state_quest = quest.to(celebration)

>>> sm = QuestWithDone()
>>> sm.send("finish")
>>> set(sm.configuration_values) == {"celebration"}
True

For {ref}parallel states <parallel-states>, the done.state event fires only when all regions have reached a final state:

>>> from statemachine import State, StateChart

>>> class WarWithDone(StateChart):
...     class war(State.Parallel):
...         class quest(State.Compound):
...             start_q = State(initial=True)
...             end_q = State(final=True)
...             finish_q = start_q.to(end_q)
...         class battle(State.Compound):
...             start_b = State(initial=True)
...             end_b = State(final=True)
...             finish_b = start_b.to(end_b)
...     peace = State(final=True)
...     done_state_war = war.to(peace)

>>> sm = WarWithDone()
>>> sm.send("finish_q")
>>> "war" in sm.configuration_values
True

>>> sm.send("finish_b")
>>> set(sm.configuration_values) == {"peace"}
True

(donedata)=

DoneData

Final states can carry data to their done.state handlers via the donedata parameter. The value should be a callable (or method name string) that returns a dict, which is forwarded as keyword arguments to the transition handler:

>>> from statemachine import Event, State, StateChart

>>> class QuestCompletion(StateChart):
...     class quest(State.Compound):
...         traveling = State(initial=True)
...         completed = State(final=True, donedata="get_result")
...         finish = traveling.to(completed)
...         def get_result(self):
...             return {"hero": "frodo", "outcome": "victory"}
...     epilogue = State(final=True)
...     done_state_quest = Event(quest.to(epilogue, on="capture_result"))
...     def capture_result(self, hero=None, outcome=None, **kwargs):
...         self.result = f"{hero}: {outcome}"

>>> sm = QuestCompletion()
>>> sm.send("finish")
>>> sm.result
'frodo: victory'
`donedata` can only be specified on `final=True` states. Attempting to use it
on a non-final state raises `InvalidDefinition`.

error.execution events

When a callback raises during a macrostep and {ref}catch_errors_as_events <behaviour> is enabled, the engine dispatches an error.execution internal event. Define a transition for this event to recover from errors within the statechart:

>>> from statemachine import State, StateChart

>>> class ResilientChart(StateChart):
...     working = State(initial=True)
...     failed = State(final=True)
...
...     go = working.to.itself(on="do_work")
...     error_execution = working.to(failed)
...
...     def do_work(self):
...         raise RuntimeError("something went wrong")

>>> sm = ResilientChart()
>>> sm.send("go")
>>> "failed" in sm.configuration_values
True
See {ref}`error-execution` for the full error handling reference: recovery
patterns, `after` as a finalize hook, and nested error scenarios.

(naming-conventions)=

Dot-notation naming conventions

SCXML uses dot-separated event names (done.state.quest, error.execution), but Python identifiers cannot contain dots. The library provides prefix-based naming conventions that automatically register both forms:

(done-state-convention)=

done_state_ prefix

Any event attribute starting with done_state_ matches both the underscore form and the dot-notation form. Only the prefix is replaced — the suffix is kept as-is, preserving multi-word state names:

Attribute name Matches event names
done_state_quest "done_state_quest" and "done.state.quest"
done_state_lonely_mountain "done_state_lonely_mountain" and "done.state.lonely_mountain"

error_ prefix

Any event attribute starting with error_ matches both the underscore form and the dot-notation form. Unlike done_state_, all underscores after the prefix are replaced with dots:

Attribute name Matches event names
error_execution "error_execution" and "error.execution"
If you provide an explicit `id=` parameter on the `Event`, it takes precedence
and the naming convention is not applied.