StateChart 3.2.1

Not released yet

Bug fixes in 3.2.1

Symmetric state for on_exit_state across compound boundaries

When exiting a compound state directly (a transition like child -> outsider), the generic on_exit_state() callback reported the transition’s source for every exited state. Exiting child and its parent parent both arrived with state and source bound to child, so the parent level was never observable and the two exit calls were indistinguishable.

This was asymmetric with on_enter_state(), which already binds state (and target) to each individual state being entered. The exit side now matches: state (and source) is bound to the individual state being exited.

>>> from statemachine import State, StateChart

>>> class FSM(StateChart):
...     orphan = State(initial=True)
...
...     class parent(State.Compound):
...         child = State()
...
...     switch = orphan.to(parent.child) | parent.child.to(orphan)
...
...     def on_exit_state(self, source, state):
...         print(f"exit {state.id} (source={source.id})")

>>> sm = FSM()
>>> sm.send("switch")
exit orphan (source=orphan)
>>> sm.send("switch")
exit child (source=child)
exit parent (source=parent)

Before this fix, the last line read exit child (source=child), hiding the parent. State-specific callbacks (on_exit_<state>) were already correctly keyed per state and are unaffected. Flat (non-compound) machines are also unaffected, since there the exited state is always the transition’s source.

#634.

Negative indices in OrderedSet.__getitem__

OrderedSet.__getitem__ raised ValueError (leaking from itertools.islice) when called with a negative index, instead of following the sequence protocol. Negative indices now count from the end like any Python sequence, and an index that is still out of range after normalisation raises IndexError:

>>> from statemachine.orderedset import OrderedSet

>>> s = OrderedSet([1, 2, 3])
>>> s[-1]
3
>>> s[-3]
1
>>> s[-4]
Traceback (most recent call last):
    ...
IndexError: index -4 out of range

#633.

Lazy / translation proxy objects as name

A State (or Event) name can now be any object castable to str, including lazy translation proxies (e.g. django.utils.translation.gettext_lazy). Earlier 3.x releases stored the value as-is and later assumed it was a real str, so a proxy broke message formatting (notably the TransitionNotAllowed message) and str(state). The proxy is now kept untouched and only resolved via str() at the point of display, so the active locale is honored at render time instead of at class-definition time.

>>> from statemachine import State, StateMachine

>>> class Lazy:  # stand-in for a translation proxy (resolved on str())
...     def __init__(self, value):
...         self.value = value
...     def __str__(self):
...         return self.value

>>> class SM(StateMachine):
...     draft = State(Lazy("Rascunho"), initial=True)
...     published = State(Lazy("Publicado"), final=True)
...     publish = draft.to(published)

>>> str(SM.draft)
'Rascunho'

#632.