Skip to content

Transitions

What @state.transition(...) produces, and the objects you get back when you look a transition method up.

Transition

One declared move. A decorator with several sources builds one of these per source.

Attribute Type Description
func callable The undecorated method
source state value The state it leaves, or State.ANY
target state value The state it lands in, DEFAULT, or a dynamic target
conditions list The predicates, in declaration order
custom dict Your metadata. Never read by the library
label str _label, else func.label, else func.__name__.title()
slug str func.__name__
>>> transition = list(publication.Article.submit.get_transitions())[0]
>>> transition.slug, transition.label
('submit', 'Submit')
>>> transition.source, transition.target
(<ReviewState.DRAFT: 'DRAFT'>, <ReviewState.IN_REVIEW: 'IN_REVIEW'>)
Method Returns
conditions_met(instance) bool — all conditions pass
get_unmet_condition(instance) (condition, result) for the first failure, else None
declared_targets() The states this can be known to lead to, ahead of a call
resolve_precall_target(instance, args, kwargs) The target, resolving a GET_STATE
resolve_return_value_target(result) The target, for a RETURN_VALUE

declared_targets() returns a single-element list for an ordinary target, the declared states for a restricted dynamic target, and [] for an unrestricted one:

>>> transition.declared_targets()
[<ReviewState.IN_REVIEW: 'IN_REVIEW'>]

Transition sorts by label, so a list of them can be ordered for display with plain sorted().

TransitionBoundMethod

What instance.method gives you. Callable, and answers questions without calling.

Member Description
__call__(*args, **kwargs) Runs the transition — the full lifecycle
can_proceed(check_conditions=True) Whether the transition is possible now
original(*args, **kwargs) Calls the undecorated function, bypassing the machine
label The label of the transition available from the current state
get_transitions() Every transition declared on this method
>>> article = publication.Article('Ready', body='A body.')
>>> article.submit.can_proceed()
True
>>> article.submit.can_proceed(check_conditions=False)
True
>>> article.submit.label
'Submit'

can_proceed(check_conditions=False) asks only about the state. The difference between the two answers is what separates "wrong stage" from "right stage, something missing" — see Conditions.

TransitionMethod

What Cls.method gives you. Not callable — there is no instance to move.

Member Description
get_transitions() Every transition declared on this method
slug func.__name__
>>> sorted(
...     (str(transition.source), str(transition.target))
...     for transition in publication.Article.archive.get_transitions()
... )
[('ANY', 'ReviewState.ARCHIVED')]

StateDescriptor

What Cls.state gives you. Proxies unknown attributes to the underlying State, so Cls.state.propname works.

Method Returns
get_transitions() {TransitionMethod: [Transition, ...]} for the whole class
get_outgoing_transitions(state) Transitions leaving state
get_available_transitions(flow, state) Those whose conditions pass for flow

The result of get_transitions() is cached on the owner class under __fsm_<propname>_transitions, and methods are discovered with inspect.getmembers, so the order is alphabetical by method name.

The outgoing rule

A transition is outgoing from state when:

transition.source == state
or (transition.source == State.ANY and state not in transition.declared_targets())

The second clause stops a State.ANY transition being listed as a self-loop on its own target. It affects listings only — the lookup performed by an actual call is the plain one, so a wildcard still matches from its own target. See Transitions → State.ANY.

An unrestricted dynamic target has no declared targets, so it can never be ruled out and is always listed.

The call lifecycle

TransitionBoundMethod.__call__ runs:

  1. Read the current state.
  2. Look up the transition for it, falling back to State.ANY. No match → NoTransition.
  3. Evaluate conditions in order. First falsy → TransitionConditionsUnmet.
  4. Resolve and set the target, unless it is a State.RETURN_VALUE. An invalid GET_STATE result → InvalidTargetState, before the body runs.
  5. Call the method body.
  6. On BaseException: restore the original state and re-raise.
  7. On success: resolve a RETURN_VALUE target and set it, then call @state.on_success() with the caller's keyword arguments.