Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion statemachine/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class TransitionDict(TypedDict, total=False):
event: "str | None"
internal: bool
initial: bool
validators: bool
validators: "str | ActionProtocol | Sequence[str] | Sequence[ActionProtocol]"
cond: "str | ActionProtocol | Sequence[str] | Sequence[ActionProtocol]"
unless: "str | ActionProtocol | Sequence[str] | Sequence[ActionProtocol]"
on: "str | ActionProtocol | Sequence[str] | Sequence[ActionProtocol]"
Expand Down Expand Up @@ -191,6 +191,7 @@ def create_machine_class_from_definition(
"event": transition_event_name,
"internal": transition_data.get("internal"),
"initial": transition_data.get("initial"),
"validators": transition_data.get("validators"),
"cond": transition_data.get("cond"),
"unless": transition_data.get("unless"),
"on": transition_data.get("on"),
Expand Down
32 changes: 32 additions & 0 deletions tests/test_io.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for statemachine.io module (dictionary-based state machine definitions)."""

import pytest
from statemachine.io import _parse_history
from statemachine.io import create_machine_class_from_definition

Expand Down Expand Up @@ -44,3 +45,34 @@ def test_transition_with_both_parent_and_own_event_name(self):
event_ids = sorted(e.id for e in sm.events)
assert "parent_evt" in event_ids
assert "sub_evt" in event_ids


class TestTransitionValidators:
"""A ``validators`` entry in a transition definition must be materialized
onto the Transition (the explicit-rejection channel), not silently dropped.
"""

def test_validators_from_definition_run_on_send(self):
class Rejected(Exception):
pass

def reject(*args, **kwargs):
raise Rejected("not allowed")

sm_cls = create_machine_class_from_definition(
"ValidatedMachine",
states={
"s1": {
"initial": True,
"on": {"go": [{"target": "s2", "validators": reject}]},
},
"s2": {"final": True},
},
)
sm = sm_cls()

# Without the validator being materialized, send() would succeed and
# the machine would land in s2. With it, the validator aborts.
with pytest.raises(Rejected):
sm.send("go")
assert sm.current_state.id == "s1"