Skip to content

feat: initial transition actions for compound states, including on_init_<state> - #694

Open
fgmacedo wants to merge 4 commits into
mainfrom
fgmacedo/uml-w2c_initial
Open

fgmacedo wants to merge 4 commits into
mainfrom
fgmacedo/uml-w2c_initial

Conversation

@fgmacedo

@fgmacedo fgmacedo commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

Closes #587
Refs #692

Builds on #693 (merged), which makes default entry run the initial transition it chose.

Why

#587 asks for the UML "init" of a compound state: an initial transition that targets a nested descendant and runs an action. d.to(d.d2.d21.d212, initial=True, on="init_action") entered the deep target, but the factory also registered the document-order initial d -> d1, so the state held two initial transitions and init_action never ran. There was also no callback convention for it, which the issue thread promised as on_init_<state>().

What changes

  • An explicit initial transition on a non-parallel compound is its only initial transition: no document-order one is added. Two explicit initial transitions on the same state raise InvalidDefinition.
  • Its on= action runs after the compound's enter callbacks and before any child is entered, on every default entry (start, re-entry, history default), on sync and async engines.
  • New naming convention on_init_<state>(), on the chart or on a listener: runs as part of the compound's initial transition, explicit or by document order, after the on= action, and only on default entry. A transition that targets a descendant directly skips it. A parallel state never runs its own; its regions do.
  • Errors in the initial action or in on_init_<state>() follow transition on rules: in a StateChart they become error.execution and entry completes, instead of aborting the microstep.
  • A method name bound both to an event and to a state is reported once per class at build time, and for the model and each listener when attached (constructor, listeners = [...], add_listener()): on_init_<state> with an event init_<state> raises InvalidDefinition; the existing on_enter_ / on_exit_ / on_invoke_ overlaps (which already run twice on main) emit a DeprecationWarning pointing at user code, to raise in 4.0. A rejected listener is not attached. validate_callback_name_conflicts = False opts out.
  • Behavior change: a chart whose first child was reachable only through the extra document-order initial transition now fails the reachability check with InvalidDefinition.
  • Docs: "Explicit initial transition" section in docs/states.md (doctests, including a root-level init by wrapping the states in a top compound), callback table in docs/actions.md, a new validation in docs/validations.md, and 3.3.0 release notes (feature, behavior change, deprecation, bug fix).
  • tests/test_statechart_error.py had a real overlap (event enter_danger with on_enter_danger, so the handler ran twice); its event is renamed to go_danger.

Evidence

  • timeout 120 uv run pytest tests/test_statechart_initial_transition.py: 28 passed (sync and async via sm_runner). Before this PR, 15 of the explicit-initial tests fail on main and 8 of the 10 on_init_ cases fail without the transition.py change.
  • PSiCC2 chart from UML initial transitions: target a nested descendant directly and support on_init_<state> actions #587 with d.to(d.d2, initial=True, on=...) and d2.to(d2.d21.d211, initial=True, on=...) prints d-ENTRY;d-INIT;d2-ENTRY;d2-INIT;d21-ENTRY;d211-ENTRY; transition C: d2 -> d1 prints d1-ENTRY;d1-INIT;d11-ENTRY.
  • timeout 120 uv run pytest tests/test_callback_name_conflicts.py: 25 passed.
  • Cost of the name check (timeit, PersonalityMachine): 2.5 µs per class build, 38 ns per listener check with no ambiguous name, against 54 µs to create an instance.
  • timeout 300 uv run pytest -n 4 --cov=statemachine --cov-branch --cov-fail-under=100 -W "error:.*bound both:DeprecationWarning" on main ec5d817: 3028 passed, 26 skipped, 126 xfailed; coverage 100.00%. PSSM suite (tests/uml/pssm/, xfail_strict): 150 passed, 1 skipped, 82 xfailed, no case changes status. Pre-commit (ruff, mypy, pyright, pytest) passes.
  • Adversarial review, 5 rounds by fresh subagents. Round 1 (explicit initial fix): nothing material; minor pre-existing gaps listed under Not verified. Round 2 (on_init_<state>): one material finding, the init_<x> event name overlap, now rejected; doc wording nits fixed. Round 3 (name check): methods in nested state bodies escaped the check, and deepcopy/pickle warned again; both fixed. Round 4: a rejected listener stayed in active_listeners and came back on copy; fixed by checking before attaching. Round 5: nothing material. Verified OK there: ordering with on= and deep history targets, default-entry-only, parallel regions, errors on StateChart and StateMachine, subclasses, listeners added after start, SCXML ids with dots and dashes.

Not verified

  • With MachineMixin (Django models) or a user __init__ override between the caller and StateMachine.__init__, the DeprecationWarning is attributed to library or intermediate frames, so Python's default filters may hide it outside pytest or -W.
  • A subclass of a class that already warned warns again at its own class statement. Instance attributes set in __init__ (self.on_init_x = ...) are not checked.
  • The new error and warning messages are not in the translation catalogs.
  • Diagrams still draw the initial arrow to the first child and show no initial action (pre-existing).
  • An explicit initial silently wins over a contradictory initial=True child; an initial target outside the compound is not validated (pre-existing).
  • A root-level initial action has no direct API; only the top-compound workaround is documented.

@fgmacedo fgmacedo added this to the 3.3.0 milestone Oct 14, 2026
@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (ec5d817) to head (bbc2b75).

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #694   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           53        54    +1     
  Lines         5900      5959   +59     
  Branches       939       952   +13     
=========================================
+ Hits          5900      5959   +59     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

… a default one

Refs #587
Refs #692

Signed-off-by: Fernando Macedo <fgmacedo@gmail.com>
Refs #587
Refs #692

Signed-off-by: Fernando Macedo <fgmacedo@gmail.com>
A method named on_init_<state>() is resolved as an action of the state's
initial transition, explicit or by document order. It runs after the
state's enter callbacks and only on default entry, giving the UML
statechart init action a callback convention.

Closes #587

Signed-off-by: Fernando Macedo <fgmacedo@gmail.com>
@fgmacedo
fgmacedo force-pushed the fgmacedo/uml-w2c_initial branch from 0ad7a1d to 20cb9a8 Compare October 16, 2026 22:21
@fgmacedo fgmacedo changed the title fix: run the effect of an explicit initial transition and register it as the only initial feat: initial transition actions for compound states, including on_init_<state> Oct 16, 2026
A method named on_init_<state> is also the on_ action of an event named
init_<state>, so it would run for both. The class build now finds those
names once per class and checks the class, the model and every listener
attached by the constructor or add_listener against them: on_init_
names raise InvalidDefinition, and the existing on_enter_, on_exit_ and
on_invoke_ overlaps emit a DeprecationWarning until 4.0.
validate_callback_name_conflicts = False opts out.

Refs #587

Signed-off-by: Fernando Macedo <fgmacedo@gmail.com>
@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UML initial transitions: target a nested descendant directly and support on_init_<state> actions

1 participant