Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
e647c3d
feat: Basic support for SCXML test suit
fgmacedo Nov 22, 2024
067d4d8
feat: Support for onentry/raise/assign/log/if/elseif/else elements; W…
fgmacedo Nov 22, 2024
91ae843
fix: SCXML Parsing of if/else element
fgmacedo Nov 23, 2024
28ccd8b
feat: Support for SCXML foreach tag
fgmacedo Nov 23, 2024
6637692
fix: Trigger eventless transition at startup
fgmacedo Nov 26, 2024
4eefc41
refac: Preserving original scxml file names for passing testcases
fgmacedo Nov 27, 2024
6a4f31c
fix: Allowing event without transition
fgmacedo Nov 27, 2024
55bed8e
feat: Delayed events; Support for SCXML <send> tag
fgmacedo Nov 27, 2024
ca3ecf7
refac: Attempt to use PriorityQueue
fgmacedo Nov 30, 2024
ed8b639
refac: Using get/set state for pickling
fgmacedo Nov 30, 2024
b069740
feat: Support for SCXML <cancel> tag. Allow cancelling delayed events
fgmacedo Nov 30, 2024
f52cce6
feat: Support for SCXML <script>, all W3C tests imported and ~50% pas…
fgmacedo Dec 3, 2024
7664c1e
feat: Support for SCXML <_event.name>
fgmacedo Dec 3, 2024
d9024af
chore: Drop support for Python3.7 and 3.8 due to the lack of support …
fgmacedo Dec 3, 2024
2bcd054
fix: Fix merge with develop
fgmacedo Dec 4, 2024
37f9d2d
refac: Split the SCXML parser into schema definition, parser and acti…
fgmacedo Dec 5, 2024
a90fb74
fix: Fix type hints for Python 3.9 as the | operator is not supported
fgmacedo Dec 5, 2024
11f5e8b
feat: Add syntax for compound and parallel states (only parser)
fgmacedo Dec 6, 2024
f699d9c
chore: Remove support for non-RTC model; Preparing processing_loop fo…
fgmacedo Dec 6, 2024
f001a79
chore: Fix cmd params for test coverage
fgmacedo Dec 6, 2024
4a80a31
feat: SCXML processing model
fgmacedo Dec 8, 2024
d0bca2e
fix: Fix compatibility with py3.9
fgmacedo Dec 8, 2024
a7022d8
feat: Hierarquical statemachines with compose and parallel
fgmacedo Dec 10, 2024
7e4c141
feat: Internal transitions
fgmacedo Dec 10, 2024
a262856
chore: New SCXML fail mark with the contents of the errors
fgmacedo Dec 11, 2024
1724ba1
chore: Microwave example with parallel state working
fgmacedo Dec 12, 2024
09fcf66
feat: New callback
fgmacedo Dec 12, 2024
6a57fd2
docs: Release example of create_machine_class_from_definition
fgmacedo Dec 12, 2024
815892b
fix: Fix parser of sub-final states
fgmacedo Dec 12, 2024
89da156
fix: Fix parallel enter/exit and checks
fgmacedo Dec 14, 2024
0e43ed4
chore: SCXML failing xfail marks updated
fgmacedo Dec 15, 2024
d8526a2
fix: Executing <initial> content on default entry
fgmacedo Dec 15, 2024
60d547d
chore: Fix pyright complaining about event calls
fgmacedo Dec 20, 2024
f552171
fix: Fix test533, ordering of entering states and exit/enter of paral…
fgmacedo Dec 21, 2024
77362a1
fix: Better support for targetless transitions
fgmacedo Dec 22, 2024
7de2ad4
fix: Fix sm name from scxml element
fgmacedo Dec 22, 2024
d2a002a
fix: Fix 326, _ioprocessor is the same for the session
fgmacedo Dec 22, 2024
deb84b2
fix: Fix some edge cases of initial state configuration
fgmacedo Dec 23, 2024
705ecb3
fix: Log may contain only the 'label' attr
fgmacedo Dec 23, 2024
29c22fb
fix: Reading from file on <scxml.datamodel.data.src> tag
fgmacedo Dec 23, 2024
6cb83f5
fix: Initials of scxml is respected; Added validate_disconnected_stat…
fgmacedo Dec 23, 2024
2af1eca
fix: SCXML _event should be bound only after the first event; the ins…
fgmacedo Dec 23, 2024
b91306e
fix: Fix event tests on the SCXML suit
fgmacedo Dec 23, 2024
eec118a
feat: Support for History pseudo state
fgmacedo Dec 28, 2024
fbc26c1
chore: Declaring new base StateChart with new defaults and keeping St…
fgmacedo Jan 27, 2025
c817b3e
chore: merge develop into macedo/scxml
fgmacedo Feb 11, 2026
261b5d7
fix: fix syntax error in scxml actions and update diagram doctest
fgmacedo Feb 11, 2026
f009179
fix: preserve initial states order from SCXML declaration
fgmacedo Feb 11, 2026
15eea06
fix: set ruff target-version to py39 (minimum supported version)
fgmacedo Feb 11, 2026
a0bfa10
Merge branch 'develop' into macedo/scxml
fgmacedo Feb 11, 2026
b44651f
feat: implement error.execution event handling per SCXML spec
fgmacedo Feb 11, 2026
84dd67a
feat: add error_ naming convention, statecharts docs, and error handl…
fgmacedo Feb 11, 2026
3ef1794
feat: implement SCXML donedata support for final states
fgmacedo Feb 12, 2026
388d86d
test: improve test coverage to 99% across statemachine package
fgmacedo Feb 12, 2026
a710687
feat: support multi-target transitions in Transition class
fgmacedo Feb 12, 2026
7f409be
feat: async engine parity, multi-target transitions, and SCXML send f…
fgmacedo Feb 12, 2026
21161ba
refactor: move donedata support from SCXML module to core State
fgmacedo Feb 12, 2026
381a2c7
feat: run SCXML test suite with both sync and async engines
fgmacedo Feb 12, 2026
cb75dd9
feat: diagram generation for compound, parallel, and history states
fgmacedo Feb 13, 2026
ecbc140
tests: Trying to figure out error on CI
fgmacedo Feb 13, 2026
c68741d
refactor: unify error.execution handling in engines
fgmacedo Feb 13, 2026
28c9616
Merge remote-tracking branch 'origin/develop' into macedo/scxml
fgmacedo Feb 13, 2026
6d95ced
fix: handle HistoryState separately in NestedStateFactory
fgmacedo Feb 13, 2026
bf8689b
test: add Python-syntax StateChart tests (55 tests)
fgmacedo Feb 13, 2026
22b3d85
fix: sort imports in test_signature.py
fgmacedo Feb 13, 2026
a0bd732
test: run StateChart tests on both sync and async engines
fgmacedo Feb 13, 2026
b6b5c06
feat: add done_state_ naming convention and complete StateChart docs
fgmacedo Feb 13, 2026
c4a438a
fix: None event label in create_machine_class_from_definition + docs
fgmacedo Feb 13, 2026
5250989
merge: integrate develop (v2.6.0) into macedo/scxml
fgmacedo Feb 14, 2026
ee71c7c
fix: address SonarCloud code smells
fgmacedo Feb 14, 2026
76b2a85
test: achieve 100% test coverage
fgmacedo Feb 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ Tell us what happened, what went wrong, and what you expected to happen.
Paste the command(s) you ran and the output.
If there was a crash, please include the traceback here.
```

If you're reporting a bug, consider providing a complete example that can be used directly in the automated tests. We allways write tests to reproduce the issue in order to avoid future regressions.
4 changes: 2 additions & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -46,7 +46,7 @@ jobs:
#----------------------------------------------
- name: Test with pytest
run: |
uv run pytest --cov-report=xml:coverage.xml
uv run pytest -n auto --cov --cov-report=xml:coverage.xml
uv run coverage xml
#----------------------------------------------
# upload coverage
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:

- name: Test
run: |
uv run pytest
uv run pytest -n auto --cov

- name: Build
run: |
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ repos:
pass_filenames: false
- id: pytest
name: Pytest
entry: uv run pytest
entry: uv run pytest -n auto
types: [python]
language: system
pass_filenames: false
11 changes: 9 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,15 @@ uv run pytest tests/test_signature.py::TestSignatureAdapter::test_wrap_fn_single
uv run pytest -m "not slow"
```

Tests include doctests from both source modules (`--doctest-modules`) and markdown docs
(`--doctest-glob=*.md`). Coverage is enabled by default.
When trying to run all tests, prefer to use xdist (`-n`) as some SCXML tests uses timeout of 30s to verify fallback mechanism.
Don't specify the directory `tests/`, because this will exclude doctests from both source modules (`--doctest-modules`) and markdown docs
(`--doctest-glob=*.md`) (enabled by default):

```bash
uv run pytest -n auto
```

Coverage is enabled by default.

## Linting and formatting

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Or get a complete state representation for debugging purposes:

```py
>>> sm.current_state
State('Yellow', id='yellow', value='yellow', initial=False, final=False)
State('Yellow', id='yellow', value='yellow', initial=False, final=False, parallel=False)

```

Expand Down
32 changes: 32 additions & 0 deletions docs/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ StateMachine in execution.
There are callbacks that you can specify that are generic and will be called
when something changes, and are not bound to a specific state or event:

- `prepare_event()`

- `before_transition()`

- `on_exit_state()`
Expand Down Expand Up @@ -297,6 +299,32 @@ In addition to {ref}`actions`, you can specify {ref}`validators and guards` that
See {ref}`conditions` and {ref}`validators`.
```

### Preparing events

You can use the `prepare_event` method to add custom information
that will be included in `**kwargs` to all other callbacks.

A not so usefull example:

```py
>>> class ExampleStateMachine(StateMachine):
... initial = State(initial=True)
...
... loop = initial.to.itself()
...
... def prepare_event(self):
... return {"foo": "bar"}
...
... def on_loop(self, foo):
... return f"On loop: {foo}"
...

>>> sm = ExampleStateMachine()

>>> sm.loop()
'On loop: bar'

```

## Ordering

Expand All @@ -314,6 +342,10 @@ Actions registered on the same group don't have order guaranties and are execute
- Action
- Current state
- Description
* - Preparation
- `prepare_event()`
- `source`
- Add custom event metadata.
* - Validators
- `validators()`
- `source`
Expand Down
55 changes: 55 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# API

## StateChart

```{versionadded} 3.0.0
```

```{eval-rst}
.. autoclass:: statemachine.statemachine.StateChart
:members:
:undoc-members:
```

## StateMachine

```{eval-rst}
Expand All @@ -20,6 +31,16 @@
:members:
```

## HistoryState

```{versionadded} 3.0.0
```

```{eval-rst}
.. autoclass:: statemachine.state.HistoryState
:members:
```

## States (class)

```{eval-rst}
Expand Down Expand Up @@ -79,3 +100,37 @@
.. autoclass:: statemachine.event_data.EventData
:members:
```

## Callback conventions

These are convention-based callbacks that you can define on your state machine
subclass. They are not methods on the base class — define them in your subclass
to enable the behavior.

### `prepare_event`

Called before every event is processed. Returns a `dict` of keyword arguments
that will be merged into `**kwargs` for all subsequent callbacks (guards, actions,
entry/exit handlers) during that event's processing:

```python
class MyMachine(StateMachine):
initial = State(initial=True)
loop = initial.to.itself()

def prepare_event(self):
return {"request_id": generate_id()}

def on_loop(self, request_id):
# request_id is available here
...
```

## create_machine_class_from_definition

```{versionadded} 3.0.0
```

```{eval-rst}
.. autofunction:: statemachine.io.create_machine_class_from_definition
```
28 changes: 28 additions & 0 deletions docs/async.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,31 @@ before the event is handled:
Initial

```

## StateChart async support

```{versionadded} 3.0.0
```

`StateChart` works identically with the async engine. All statechart features —
compound states, parallel states, history pseudo-states, eventless transitions,
and `done.state` events — are fully supported in async code. The same
`activate_initial_state()` pattern applies:

```python
async def run():
sm = MyStateChart()
await sm.activate_initial_state()
await sm.send("event")
```

### Async-specific limitations

- **Initial state activation**: In async code, you must `await sm.activate_initial_state()`
before inspecting `sm.configuration` or `sm.current_state`. In sync code this happens
automatically at instantiation time.
- **Delayed events**: Both sync and async engines support `delay=` on `send()`. The async
engine uses `asyncio.sleep()` internally, so it integrates naturally with event loops.
- **Thread safety**: The processing loop uses a non-blocking lock (`_processing.acquire`).
All callbacks run on the same thread they are called from — do not share a state machine
instance across threads without external synchronization.
2 changes: 1 addition & 1 deletion docs/diagram.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Graphviz. For example, on Debian-based systems (such as Ubuntu), you can use the
>>> dot = graph()

>>> dot.to_string() # doctest: +ELLIPSIS
'digraph list {...
'digraph OrderControl {...

```

Expand Down
Binary file modified docs/images/order_control_machine_initial.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/order_control_machine_initial_300dpi.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/order_control_machine_processing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/readme_trafficlightmachine.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/test_state_machine_internal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mixins
integrations
diagram
processing_model
statecharts
api
auto_examples/index
contributing
Expand Down
69 changes: 5 additions & 64 deletions docs/processing_model.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,8 @@ In the literature, It's expected that all state-machine events should execute on

The main point is: What should happen if the state machine triggers nested events while processing a parent event?

```{hint}
The importance of this decision depends on your state machine definition. Also the difference between RTC
and non-RTC processing models is more pronounced in a multi-threaded system than in a single-threaded system.
In other words, even if you run in {ref}`Non-RTC model`, only one external {ref}`event` will be
handled at a time and all internal events will run before the next external event is called,
so you only notice the difference if your state machine definition has nested event triggers while
processing these external events.
```

There are two distinct models for processing events in the library. The default is to run in
{ref}`RTC model` to be compliant with the specs, where the {ref}`event` is put on a
queue before processing. You can also configure your state machine to run in
{ref}`Non-RTC model`, where the {ref}`event` will be run immediately.
This library atheres to the {ref}`RTC model` to be compliant with the specs, where the {ref}`event` is put on a
queue before processing.

Consider this state machine:

Expand Down Expand Up @@ -60,13 +49,13 @@ Consider this state machine:

In a run-to-completion (RTC) processing model (**default**), the state machine executes each event to completion before processing the next event. This means that the state machine completes all the actions associated with an event before moving on to the next event. This guarantees that the system is always in a consistent state.

If the machine is in `rtc` mode, the event is put on a queue.
Internally, the events are put on a queue before processing.

```{note}
While processing the queue items, if others events are generated, they will be processed sequentially.
While processing the queue items, if others events are generated, they will be processed sequentially in FIFO order.
```

Running the above state machine will give these results on the RTC model:
Running the above state machine will give these results:

```py
>>> sm = ServerConnection()
Expand All @@ -88,51 +77,3 @@ after 'connection_succeed' from 'connecting' to 'connected'
```{note}
Note that the events `connect` and `connection_succeed` are executed sequentially, and the `connect.after` runs on the expected order.
```

## Non-RTC model

```{deprecated} 2.3.2
`StateMachine.rtc` option is deprecated. We'll keep only the **run-to-completion** (RTC) model.
```

In contrast, in a non-RTC (synchronous) processing model, the state machine starts executing nested events
while processing a parent event. This means that when an event is triggered, the state machine
chains the processing when another event was triggered as a result of the first event.

```{warning}
This can lead to complex and unpredictable behavior in the system if your state-machine definition triggers **nested
events**.
```

If your state machine does not trigger nested events while processing a parent event,
and you plan to use the API in an _imperative programming style_, you can consider using the synchronous mode (non-RTC).

In this model, you can think of events as analogous to simple method calls.

```{note}
While processing the {ref}`event`, if others events are generated, they will also be processed immediately, so a **nested** behavior happens.
```

Running the above state machine will give these results on the non-RTC (synchronous) model:

```py
>>> sm = ServerConnection(rtc=False)
enter 'disconnected' from '' given '__initial__'

>>> sm.send("connect")
exit 'disconnected' to 'connecting' given 'connect'
on 'connect' from 'disconnected' to 'connecting'
enter 'connecting' from 'disconnected' given 'connect'
exit 'connecting' to 'connected' given 'connection_succeed'
on 'connection_succeed' from 'connecting' to 'connected'
enter 'connected' from 'connecting' given 'connection_succeed'
after 'connection_succeed' from 'connecting' to 'connected'
after 'connect' from 'disconnected' to 'connecting'
['on_transition', 'on_connect']

```

```{note}
Note that the events `connect` and `connection_succeed` are nested, and the `connect.after`
unexpectedly only runs after `connection_succeed.after`.
```
5 changes: 4 additions & 1 deletion docs/releases/2.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ including tolerance to unknown {ref}`event` triggers.
The default value is ``False``, that keeps the backward compatible behavior of when an
event does not result in a {ref}`transition`, an exception ``TransitionNotAllowed`` will be raised.

```py
```
>>> import pytest
>>> pytest.skip("Since 3.0.0 `allow_event_without_transition` is now a class attribute.")

>>> sm = ApprovalMachine(allow_event_without_transition=True)

>>> sm.send("unknow_event_name")
Expand Down
Loading
Loading