feat: StateChart (support for compound / parallel / historical states including SCXML notation)#501
Merged
feat: StateChart (support for compound / parallel / historical states including SCXML notation)#501
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #501 +/- ##
============================================
+ Coverage 99.77% 100.00% +0.22%
============================================
Files 25 31 +6
Lines 1756 3592 +1836
Branches 230 553 +323
============================================
+ Hits 1752 3592 +1840
+ Misses 2 0 -2
+ Partials 2 0 -2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
ecc7957 to
1b74bf5
Compare
f1edb7f to
1e7f40d
Compare
0890680 to
35f4bed
Compare
4 tasks
e2b6907 to
b392cbd
Compare
Contributor
|
Is this in a state (🤣) that I could try it out? I have several use-cases where the sub-state functionality would be useful and I'd be glad to give feedback if I run into issues. Note, the user experience I had expected looked something like this: class SubMachine(StateMachine):
test = State(initial=True)
test_complete = State(final=True)
next = test.to(test_complete)
class TestSM(StateMachine):
initial_state = State(initial=True)
sub_machine_as_state = SubMachine()
some_other_state = State(final=True)
goto_sub_machine = initial_state.to(sub_machine_as_state)
goto_finish = sub_machine_as_state.to(some_other_state)And then something like sm = TestSM()
sm.send('goto_sub_machine')
sm.send('next') # TestSM now has the submachine states as states it can traverse.
sm.send('goto_finish') |
…hen entering a top level final state the SM terminates
…for TypedDict and | operator
With target-version py314, ruff format applies PEP 758 (parentheses-free except clauses), which is invalid syntax on Python < 3.14. Since the project supports Python 3.9+, the target must match the minimum version to prevent incompatible syntax in formatted code.
# Conflicts: # pyproject.toml # uv.lock
Add `error_on_execution` flag (True on StateChart, False on StateMachine for backward compat) that catches runtime exceptions in callbacks and queues an `error.execution` internal event instead of propagating. Error handling is layered by responsibility: - callbacks.call(on_error=...): per-block isolation for onentry/onexit — errors in one block don't affect other blocks - callbacks.all(on_error=...): condition errors treated as False - Engine._conditions_match: delegates to callbacks.all with on_error - Engine.microstep: catches errors in transition actions with rollback - SCXML if_action: condition errors treated as false (SCXML translation) - InvalidDefinition always propagates regardless of error_on_execution Also fixes: - Event with custom id in factory (preserve SCXML event ids like "error.execution" while keeping Python attribute accessible) - SCXML _event system variable initialized to None before first event - Infinite loop guard for error.execution in transition handlers
…ing tests Auto-register dot-notation aliases for event attributes starting with `error_` (e.g. `error_execution` matches both `error_execution` and `error.execution`), removing the need for verbose `id="error.execution"`. Add `docs/statecharts.md` documenting StateChart vs StateMachine class-level flags (allow_event_without_transition, enable_self_transition_entries, atomic_configuration_update, error_on_execution) and error.execution handling. Comprehensive test coverage for error handling with conditions, listeners, flow control, and SCXML spec compliance using LOTR-themed scenarios.
c18149e to
84dd67a
Compare
Parse <donedata> elements (<param> and <content>) from <final> states, evaluate expressions at runtime, and pass the data correctly to done.state.* events. Error cases (invalid expr/location) raise error.execution per SCXML spec. Fixes W3C conformance tests: 294, 298, 343, 488, 527, 528, 529.
Add comprehensive tests organized by scope in existing test files: - Async engine: error propagation, InvalidDefinition handling, start noop - Error execution: internal event error handling, microstep error paths - State machine: compound state configuration, abstract SM, raise_ events - Diagrams: compound/parallel subgraphs, dashed style, lhead attributes - SCXML units: parser errors, action callables, schema edge cases - Transitions: non-callable call, loop transitions, event name tests Mark confirmed dead code with pragma no cover: - factory.py: abstract class checks unreachable after early return - sync.py: _run_microstep safety net (microstep handles errors internally) - sync.py: secondary internal queue drain (macrostep loop already drains) - base.py: is_in_final_state parallel/atomic branches (nested parallel)
The `target` parameter now accepts `State | List[State] | None`, enabling SCXML parallel region entry where a single transition targets multiple states. Internally stores as `_targets` list with backward-compatible `target` property (returns first target) and new `targets` property (returns full list).
…ixes - Rewrite AsyncEngine to mirror SyncEngine's 3-phase macrostep architecture with full StateChart support - Extract pure computation helpers from BaseEngine so both engines share the same logic - Add on_error callback support to async dispatch methods - Support multi-target initial transitions for SCXML parallel regions in engine, IO layer, parser, and factory - Fix SCXML send error handling: error.communication for undispatchable session targets, error.execution for invalid targets/types, and namelist variable validation - Remove resolved xfail markers (test364, test496, test521, test553)
Add `donedata` parameter to `State.__init__()` so Python-defined final states can specify donedata callables directly. The SCXML processor now passes donedata as a separate key instead of mixing it into enter callbacks. Also removes the no-op `_processing_loop()` call from `ExecuteBlock`.
Add async SCXML test variant using a minimal AsyncListener to trigger AsyncEngine selection, keeping SCXMLProcessor free of async concerns. Fix __initial__ handling in AsyncEngine (break instead of continue in phase 3) to ensure internal events are processed before external ones.
- Render initial pseudo-state (black dot → initial child) inside all compound subgraphs, not just root level - Add history state rendering as UML circles labeled "H" / "H*" - Annotate parallel state subgraph labels with ☷ indicator - Support multi-target transitions (one edge per target) - Extract _add_transitions() helper to reduce _graph_states() complexity - Remove stale TODO comment - Fix NestedStateFactory to propagate kwargs (e.g. parallel=True) from State.Parallel/State.Compound base classes to subclass-created States - Export HistoryState from statemachine.__init__ - Register event label "None" bug as release blocker in PLAN
2e1ce94 to
cb75dd9
Compare
Replace scattered `if self.sm.error_on_execution` checks and the broken `_on_error_execution` property with two unified methods: - `_on_error_handler(trigger_data)`: returns a bound callable (or None) for per-block callback error isolation (onentry/onexit/conditions). - `_handle_error(error, trigger_data)`: for try/except blocks in microstep/_run_microstep — sends error.execution or re-raises. Fixes two bugs in the previous code: - base.py used `partial(self._on_error_execution, trigger_data)` which bound trigger_data to the wrong positional parameter (error), causing AttributeError on any per-block callback error. - async_.py passed `self._on_error_execution` without binding trigger_data at all, which would fail with a missing argument. Also fixes `_send_error_execution` parameter order to (error, trigger_data) and removes the unused `functools.partial` import.
NestedStateFactory.__new__ was treating HistoryState as regular State (via isinstance) and adding it to states=[]. HistoryState instances need to go into history=[] for proper initialization in State.__init__.
Comprehensive test coverage for StateChart features using Python class syntax: compound states, parallel states, history states, eventless transitions, donedata, delayed events, error.execution, and In() conditions.
Add SMRunner fixture parametrized over sync/async to conftest.py. Engine-dependent tests (53) now run on both engines via sm_runner, producing 108 test cases total. Definition-only tests (2) remain sync.
Add done_state_ prefix handling in factory.py alongside the existing error_ convention. Attributes starting with done_state_ auto-register both the underscore and dot forms (e.g., done_state_quest matches both "done_state_quest" and "done.state.quest"). Only the prefix is replaced, preserving multi-word state names (done_state_lonely_mountain → done.state.lonely_mountain). Simplify existing tests, examples, and docs to use the convention instead of explicit id= parameters. Add comprehensive StateChart documentation: compound states, parallel states, history pseudo-states, eventless transitions, DoneData, delayed events, In() conditions, error.execution, and the new done_state_ convention.
Fix bug where transition_data.get("event") returning None was
interpolated as literal "None" in event IDs (e.g., "next None").
Complete remaining documentation: configuration property in states.md,
cross-boundary transitions and transition priority in transitions.md,
async-specific limitations in async.md, prepare_event convention and
create_machine_class_from_definition docstring in api.md.
Resolved conflicts in engines (adapted enabled_events for StateChart architecture using configuration instead of current_state), statemachine.py, transition_mixin.py (took develop's TypeError/decorator-syntax bugfix), pyproject.toml (merged python_version markers with xdist/timeout deps), and test files (kept both scxml and enabled_events tests). Also restored engine.start() call in __setstate__ for async copy support.
- S1186: add comments to empty method bodies in tests - S125: remove commented-out code in conftest.py and test_microwave.py - S1192: extract _ERROR_EXECUTION constant in engines/base.py - S108: invert condition to eliminate empty pass block in base engine - S5713: remove redundant IndentationError (subclass of SyntaxError) - S1940: use != instead of not == in processor.py - S2772: remove unneeded pass in schema.py Action dataclass - S1854: remove dead assignment to all_initial_states in parser.py - S5806: rename id -> func_id to avoid shadowing builtin in spec_parser.py - S1172: remove unused is_root parameter from diagram._graph_states - S5655: fix Events.match type hint to accept str | None - S5886: fix return type of create_machine_class_from_definition
Add tests and pragmas to eliminate all coverage gaps: - Add tests for enabled_events dedup, history diagram node, IO definition parsing (_parse_history, event name concatenation), and SCXML history without transitions - Remove unused trigger_data param from _on_error_handler - Remove unused Transition._get_event_by_id method - Mark genuinely unreachable defensive code with pragma comments (callbacks on_error path, SCXML algorithm guards, factory parallel initial transitions, parser/processor safety checks)
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Statecharts are here
This PR brings full statechart support to python-statemachine — the most significant evolution of the library since its creation. What started as an exploration of nested states grew into a comprehensive implementation of the SCXML specification (W3C), introducing compound states, parallel states, history pseudo-states, eventless transitions, error handling, and much more.
This is a major version release (3.0.0) with backwards-incompatible changes. The
StateMachineclass is preserved for backwards compatibility, while the newStateChartclass provides full SCXML-compliant behavior out of the box.Compound states
States can now contain inner substates using
State.Compoundwith Python class syntax:Entering a compound state activates both the parent and its initial child. Exiting removes the parent and all descendants.
Parallel states
Multiple regions can be active simultaneously using
State.Parallel:Events in one region don't affect others — true orthogonal concurrency.
History pseudo-states
The
HistoryStaterecords the configuration of a compound state when exited. Re-entering via history restores the previously active child (shallow or deep):Eventless (automatic) transitions
Transitions without an event trigger fire automatically when their source state is active, cascading through the entire chain in a single macrostep.
DoneData on final states
Final states can provide data to
done.statehandlers via thedonedataparameter, following the SCXML spec for communicating completion results.Error handling with
error.executionWhen
error_on_executionis enabled (default inStateChart), runtime exceptions are caught and result in an internalerror.executionevent — following the SCXML error handling specification. Aerror_naming convention makes it easy:Additional features
sm.send("event", delay=500)with cancellation viasm.cancel_event(send_id)*supportIn(state)condition checks —cond="In('state_id')"to check active configurationprepare_eventcallback — inject custom kwargs into all callbackscreate_machine_class_from_definition()for dynamic state machine creationEngine architecture
Both the sync and async engines have been rewritten to implement the SCXML processing model:
configuration(ordered set of active states) replacescurrent_stateSCXML compliance
The test suite includes 198 W3C SCXML test cases (mandatory + optional), run on both sync and async engines. Currently 152 pass and 46 are marked as expected failures (mostly requiring
<invoke>which is deferred to v3.1+).Quality
Backwards compatibility
The
StateMachineclass preserves the old behavior through class-level attribute defaults:StateChartStateMachineallow_event_without_transitionTrueFalseenable_self_transition_entriesTrueFalseatomic_configuration_updateFalseTrueerror_on_executionTrueFalseExisting
StateMachinesubclasses continue to work as before.Known limitations (deferred to v3.1+)
<invoke>— invoking external services or sub-machines<finalize>— processing data returned from invoked servicesCloses #439, #455. Supersedes #329.