Errors¶
Every exception open-fsm raises, what triggers it, and what it carries.
>>> from open_fsm import (
... InvalidTargetState,
... NoTransition,
... TransitionConditionsUnmet,
... TransitionNotAllowed,
... )
The hierarchy¶
Exception
└── TransitionNotAllowed
├── NoTransition
├── TransitionConditionsUnmet
└── InvalidTargetState
| Exception | Raised when | State afterwards |
|---|---|---|
NoTransition |
No transition is registered for the current state | Unchanged |
TransitionConditionsUnmet |
A condition returned falsy | Unchanged |
InvalidTargetState |
A dynamic target resolved outside its declared states | Unchanged |
AttributeError |
instance.state = value |
Unchanged |
ValueError |
target=None, or @state.super() with no base |
— |
TypeError |
A class declares no State, or several |
— |
Catch TransitionNotAllowed to handle any refusal:
TransitionNotAllowed¶
The base class. It carries nothing of its own — catch it when the reason does not change what you do, and a subclass when it does.
NoTransition¶
The current state has no transition registered for the method that was called,
and there is no State.ANY fallback.
| Attribute | Description |
|---|---|
label |
Label of the transition method that was called |
state |
The state the instance was in |
>>> article = publication.Article('Ready', body='A body.')
>>> article.publish()
Traceback (most recent call last):
...
open_fsm.base.NoTransition: Publish :: no transition from "DRAFT"
>>> try:
... article.publish()
... except NoTransition as error:
... print(error.label, '|', error.state)
Publish | ReviewState.DRAFT
The method body never ran.
TransitionConditionsUnmet¶
The transition was legal from the current state, but a condition returned falsy. Conditions are evaluated in declaration order and the first failure raises.
| Attribute | Description |
|---|---|
transition |
The Transition that was refused |
failed_condition |
The first callable that returned falsy |
unmet_message |
The State.CONDITION message, or '' |
>>> conditions.ExplainedSubmission('too short').submit()
Traceback (most recent call last):
...
open_fsm.base.TransitionConditionsUnmet: 'Submit' transition conditions have not been met: is_long_enough (the body is 9 characters, 80 are required)
>>> try:
... conditions.ExplainedSubmission('too short').submit()
... except TransitionConditionsUnmet as error:
... print(error.transition.slug)
... print(error.failed_condition.__name__)
... print(error.unmet_message)
submit
is_long_enough
the body is 9 characters, 80 are required
unmet_message is empty for a plain falsy result
A condition returning False gives the exception nothing to report. Return
State.CONDITION(False, unmet='...') to put a reason in front of a user —
see Conditions.
InvalidTargetState¶
A State.RETURN_VALUE or
State.GET_STATE target resolved to a
state outside its declared set.
| Attribute | Description |
|---|---|
transition |
The Transition that was attempted |
target |
The resolved, invalid state |
allowed_states |
The declared states — a tuple for RETURN_VALUE, a list for GET_STATE |
>>> class Review(StateEngine):
... state = State(['new', 'approved'], default='new')
... @state.transition(source='new', target=State.RETURN_VALUE('approved'))
... def decide(self):
... return 'deleted'
>>> Review().decide()
Traceback (most recent call last):
...
open_fsm.base.InvalidTargetState: 'Decide' resolved to 'deleted', which is not in the allowed target states ['approved']
>>> try:
... Review().decide()
... except InvalidTargetState as error:
... print(error.target, '|', error.allowed_states)
deleted | ('approved',)
When it raises depends on the target kind
A GET_STATE target is validated before the body runs, so nothing
happened. A RETURN_VALUE target is validated after, so the body's side
effects are already done and only the state is rolled back. See
Dynamic targets.
AttributeError¶
Assigning to the state field. Raised by State.__set__, always.
>>> article.state = publication.ReviewState.PUBLISHED
Traceback (most recent call last):
...
AttributeError: Direct state modification is not allowed
A dataclass with an annotated state: State field raises this from its
generated __init__ — see
Dataclasses → The annotation trap.
ValueError¶
Two cases, both at class-definition time rather than at call time.
target=None is ambiguous with no target:
>>> class Broken(StateEngine):
... state = State(['a', 'b'], default='a')
... @state.transition(source='a', target=None)
... def go(self):
... """Never declared."""
Traceback (most recent call last):
...
ValueError: target=None is ambiguous with 'no target'. Omit the target argument entirely for a transition that doesn't change state.
@state.super() with no base transition raises
ValueError('Base transition not found') when the wrapper is first resolved.
TypeError¶
Raised by get_state_field(), so it surfaces
on the first introspection call rather than at import.
More than one State field:
>>> class TwoMachines(StateEngine):
... state = State(['a'], default='a')
... stage = State(['x'], default='x')
>>> TwoMachines().get_transitions()
Traceback (most recent call last):
...
TypeError: TwoMachines declares 2 State fields ('state', 'stage'); open-fsm models one state machine per class
None at all:
>>> get_state_field(object)
Traceback (most recent call last):
...
TypeError: object declares no State field
A third TypeError comes from the mixin's argument-free signature, when a state
is passed to it — see Engine.
Handling them in a view¶
from open_fsm import TransitionConditionsUnmet, TransitionNotAllowed
def submit_article(request, pk):
flow = ArticleFlow(get_object_or_404(Article, pk=pk))
try:
flow.submit()
except TransitionConditionsUnmet as error:
return JsonResponse({'detail': error.unmet_message or str(error)}, status=422)
except TransitionNotAllowed as error:
return JsonResponse({'detail': str(error)}, status=409)
return JsonResponse({'state': flow.state})
The split is worth making. TransitionConditionsUnmet means the object is not
ready — something the client can fix, so tell them what. Any other
TransitionNotAllowed means this is not a legal move from here, which is a
conflict rather than a bad request.
ValueError and TypeError are programming errors — a machine that was
declared wrong. Let them surface.
Related pages¶
- Conditions — producing a useful
unmet_message - Dynamic targets — when
InvalidTargetStatefires - Transitions reference — the call lifecycle