From 2c9021249ea9c79c898f0855dbe569c4473ca1fe Mon Sep 17 00:00:00 2001 From: Christian Sentis Date: Sat, 13 Jun 2026 14:35:35 +0200 Subject: [PATCH] fix(io): materialize transition validators from dict definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit create_machine_class_from_definition threaded cond/unless/on/before/after into Transition(), but dropped validators — so a 'validators' entry in a dict/JSON definition was silently ignored and never ran at send(). The TransitionDict TypedDict also mistyped validators as bool. Pass validators through to Transition() and fix the type to the same callback-spec union as cond/unless. Adds a regression test proving a definition-supplied validator runs (and aborts) on send(). Closes #. Signed-off-by: Christian Sentis --- statemachine/io/__init__.py | 3 ++- tests/test_io.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) 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"