diff --git a/statemachine/io/__init__.py b/statemachine/io/__init__.py index 529fc344..46f90821 100644 --- a/statemachine/io/__init__.py +++ b/statemachine/io/__init__.py @@ -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]" @@ -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"), diff --git a/tests/test_io.py b/tests/test_io.py index 8171ac39..e2cbbc8e 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -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 @@ -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"