Skip to content

Commit 5250989

Browse files
committed
merge: integrate develop (v2.6.0) into macedo/scxml
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.
2 parents c4a438a + d062de9 commit 5250989

26 files changed

Lines changed: 1167 additions & 128 deletions

docs/contributing.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,133 @@ Before you submit a pull request, check that it meets these guidelines:
159159
feature to the list in the next release notes.
160160
3. Consider adding yourself to the contributor's list.
161161
4. The pull request should work for all supported Python versions.
162+
163+
## Releasing a New Version
164+
165+
This project uses [git-flow](https://github.com/nvie/gitflow) for release management and
166+
publishes to PyPI automatically via GitHub Actions when a version tag is pushed.
167+
168+
### Prerequisites
169+
170+
- You must be on the `develop` branch with a clean working tree.
171+
- `git-flow` must be installed and initialized:
172+
173+
```shell
174+
brew install git-flow # macOS
175+
git flow init # use main for production, develop for next release
176+
```
177+
178+
- All changes intended for the release must already be merged into `develop`.
179+
180+
### Step-by-step release process
181+
182+
The following steps use version `X.Y.Z` as a placeholder. Replace it with the actual version
183+
number (e.g., `2.6.0`).
184+
185+
#### 1. Start the release branch
186+
187+
```shell
188+
git checkout develop
189+
git pull origin develop
190+
git flow release start X.Y.Z
191+
```
192+
193+
This creates and switches to a `release/X.Y.Z` branch based on `develop`.
194+
195+
#### 2. Bump the version number
196+
197+
Update the version string in **both** files:
198+
199+
- `pyproject.toml` — the `version` field under `[project]`
200+
- `statemachine/__init__.py` — the `__version__` variable
201+
202+
#### 3. Update translations
203+
204+
Extract new translatable strings, merge them into all existing `.po` files, translate the
205+
new entries, and compile:
206+
207+
```shell
208+
uv run pybabel extract statemachine -o statemachine/locale/statemachine.pot
209+
uv run pybabel update -i statemachine/locale/statemachine.pot -d statemachine/locale/ -D statemachine
210+
# Edit each .po file to translate new empty msgstr entries
211+
uv run pybabel compile -d statemachine/locale/ -D statemachine
212+
```
213+
214+
```{note}
215+
The `.pot` and `.mo` files are git-ignored. Only the `.po` source files are committed.
216+
The compiled `.mo` files may cause test failures if your system locale matches a translated
217+
language (error messages will appear translated instead of in English). Delete them after
218+
verifying translations work: `rm -f statemachine/locale/*/LC_MESSAGES/statemachine.mo`
219+
```
220+
221+
#### 4. Write release notes
222+
223+
Create `docs/releases/X.Y.Z.md` documenting all changes since the previous release. Include
224+
sections for new features, bugfixes, performance improvements, and miscellaneous changes.
225+
Reference GitHub issues/PRs where applicable.
226+
227+
Add the new file to the toctree in `docs/releases/index.md` (at the top of the appropriate
228+
major version section).
229+
230+
Update any related documentation pages (e.g., if a bugfix adds a new behavior that users
231+
should know about).
232+
233+
#### 5. Run linters and tests
234+
235+
```shell
236+
uv run ruff check .
237+
uv run ruff format --check .
238+
uv run mypy statemachine/
239+
uv run pytest -n auto
240+
```
241+
242+
All checks must pass before committing.
243+
244+
#### 6. Commit
245+
246+
Stage all changed files and commit. The pre-commit hooks will run ruff, mypy, and pytest
247+
automatically.
248+
249+
```shell
250+
git add <files>
251+
git commit -m "chore: prepare release X.Y.Z"
252+
```
253+
254+
#### 7. Finish the release
255+
256+
```shell
257+
git flow release finish X.Y.Z -m "vX.Y.Z"
258+
```
259+
260+
This will:
261+
- Merge `release/X.Y.Z` into `main`
262+
- Create an annotated tag `X.Y.Z` on `main`
263+
- Merge `main` back into `develop`
264+
- Delete the `release/X.Y.Z` branch
265+
266+
```{note}
267+
If tagging fails (e.g., GPG or editor issues), create the tag manually and re-run:
268+
`git tag -a X.Y.Z -m "vX.Y.Z"` then `git flow release finish X.Y.Z -m "vX.Y.Z"`.
269+
```
270+
271+
#### 8. Update the `latest` tag and push
272+
273+
```shell
274+
git tag latest -f
275+
git push origin main develop --tags -f
276+
```
277+
278+
Force-pushing tags is needed to move the `latest` tag.
279+
280+
#### 9. Verify the release
281+
282+
The tag push triggers the `release` GitHub Actions workflow (`.github/workflows/release.yml`),
283+
which will:
284+
285+
1. Check out the tag
286+
2. Run the full test suite
287+
3. Build the sdist and wheel with `uv build`
288+
4. Publish to PyPI using trusted publishing
289+
290+
Monitor the workflow run at `https://github.com/fgmacedo/python-statemachine/actions` to
291+
confirm the release was published successfully.

docs/guards.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,79 @@ So, a condition `s1.to(s2, cond=lambda: [])` will evaluate as `False`, as an emp
159159
**falsy** value.
160160
```
161161

162+
### Checking enabled events
163+
164+
The {ref}`StateMachine.allowed_events` property returns events reachable from the current state,
165+
but it does **not** evaluate `cond`/`unless` guards. To check which events actually have their
166+
conditions satisfied, use {ref}`StateMachine.enabled_events`.
167+
168+
```{testsetup}
169+
170+
>>> from statemachine import StateMachine, State
171+
172+
```
173+
174+
```py
175+
>>> class ApprovalMachine(StateMachine):
176+
... pending = State(initial=True)
177+
... approved = State(final=True)
178+
... rejected = State(final=True)
179+
...
180+
... approve = pending.to(approved, cond="is_manager")
181+
... reject = pending.to(rejected)
182+
...
183+
... is_manager = False
184+
185+
>>> sm = ApprovalMachine()
186+
187+
>>> [e.id for e in sm.allowed_events]
188+
['approve', 'reject']
189+
190+
>>> [e.id for e in sm.enabled_events()]
191+
['reject']
192+
193+
>>> sm.is_manager = True
194+
195+
>>> [e.id for e in sm.enabled_events()]
196+
['approve', 'reject']
197+
198+
```
199+
200+
`enabled_events` is a method (not a property) because conditions may depend on runtime
201+
arguments. Any `*args`/`**kwargs` passed to `enabled_events()` are forwarded to the
202+
condition callbacks, just like when triggering an event:
203+
204+
```py
205+
>>> class TaskMachine(StateMachine):
206+
... idle = State(initial=True)
207+
... running = State(final=True)
208+
...
209+
... start = idle.to(running, cond="has_enough_resources")
210+
...
211+
... def has_enough_resources(self, cpu=0):
212+
... return cpu >= 4
213+
214+
>>> sm = TaskMachine()
215+
216+
>>> sm.enabled_events()
217+
[]
218+
219+
>>> [e.id for e in sm.enabled_events(cpu=8)]
220+
['start']
221+
222+
```
223+
224+
```{tip}
225+
This is useful for UI scenarios where you want to show or hide buttons based on whether
226+
an event's conditions are currently satisfied.
227+
```
228+
229+
```{note}
230+
An event is considered **enabled** if at least one of its transitions from the current state
231+
has all conditions satisfied. If a condition raises an exception, the event is treated as
232+
enabled (permissive behavior).
233+
```
234+
162235
## Validators
163236

164237

docs/integrations.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,22 @@ class Campaign(models.Model, MachineMixin):
6969
```{seealso}
7070
Learn more about using the [](mixins.md#machinemixin).
7171
```
72+
73+
### Data migrations
74+
75+
Django's `apps.get_model()` returns **historical model** classes that are dynamically created
76+
and don't carry user-defined class attributes like `state_machine_name`. Since version 2.6.0,
77+
`MachineMixin` detects these historical models and gracefully skips state machine
78+
initialization, so data migrations that use `apps.get_model()` work without errors.
79+
80+
```{note}
81+
The state machine instance will **not** be available on historical model objects.
82+
If your data migration needs to interact with the state machine, set the attributes
83+
manually on the historical model class:
84+
85+
def backfill_data(apps, schema_editor):
86+
MyModel = apps.get_model("myapp", "MyModel")
87+
MyModel.state_machine_name = "myapp.statemachines.MyStateMachine"
88+
for obj in MyModel.objects.all():
89+
obj.statemachine # now available
90+
```

docs/releases/2.6.0.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# StateMachine 2.6.0
2+
3+
*February 2026*
4+
5+
## What's new in 2.6.0
6+
7+
This release adds the {ref}`StateMachine.enabled_events` method, Python 3.14 support,
8+
a significant performance improvement for callback dispatch, and several bugfixes
9+
for async condition expressions, type checker compatibility, and Django integration.
10+
11+
### Python compatibility in 2.6.0
12+
13+
StateMachine 2.6.0 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, and 3.14.
14+
15+
### Checking enabled events
16+
17+
A new {ref}`StateMachine.enabled_events` method lets you query which events have their
18+
`cond`/`unless` guards currently satisfied, going beyond {ref}`StateMachine.allowed_events`
19+
which only checks reachability from the current state.
20+
21+
This is particularly useful for **UI scenarios** where you want to enable or disable buttons
22+
based on whether an event's conditions are met at runtime.
23+
24+
```{testsetup}
25+
26+
>>> from statemachine import StateMachine, State
27+
28+
```
29+
30+
```py
31+
>>> class ApprovalMachine(StateMachine):
32+
... pending = State(initial=True)
33+
... approved = State(final=True)
34+
... rejected = State(final=True)
35+
...
36+
... approve = pending.to(approved, cond="is_manager")
37+
... reject = pending.to(rejected)
38+
...
39+
... is_manager = False
40+
41+
>>> sm = ApprovalMachine()
42+
43+
>>> [e.id for e in sm.allowed_events]
44+
['approve', 'reject']
45+
46+
>>> [e.id for e in sm.enabled_events()]
47+
['reject']
48+
49+
>>> sm.is_manager = True
50+
51+
>>> [e.id for e in sm.enabled_events()]
52+
['approve', 'reject']
53+
54+
```
55+
56+
Since conditions may depend on runtime arguments, any `*args`/`**kwargs` passed to
57+
`enabled_events()` are forwarded to the condition callbacks:
58+
59+
```py
60+
>>> class TaskMachine(StateMachine):
61+
... idle = State(initial=True)
62+
... running = State(final=True)
63+
...
64+
... start = idle.to(running, cond="has_enough_resources")
65+
...
66+
... def has_enough_resources(self, cpu=0):
67+
... return cpu >= 4
68+
69+
>>> sm = TaskMachine()
70+
71+
>>> sm.enabled_events()
72+
[]
73+
74+
>>> [e.id for e in sm.enabled_events(cpu=8)]
75+
['start']
76+
77+
```
78+
79+
```{seealso}
80+
See {ref}`Checking enabled events` in the Guards documentation for more details.
81+
```
82+
83+
### Performance: cached signature binding
84+
85+
Callback dispatch is now significantly faster thanks to cached signature binding in
86+
`SignatureAdapter`. The first call to a callback computes the argument binding and
87+
caches a fast-path template; subsequent calls with the same argument shape skip the
88+
full binding logic.
89+
90+
This results in approximately **60% faster** `bind_expected()` calls and
91+
around **30% end-to-end improvement** on hot transition paths.
92+
93+
See [#548](https://github.com/fgmacedo/python-statemachine/issues/548) for benchmarks.
94+
95+
96+
## Bugfixes in 2.6.0
97+
98+
- Fixes [#531](https://github.com/fgmacedo/python-statemachine/issues/531) domain model
99+
with falsy `__bool__` was being replaced by the default `Model()`.
100+
- Fixes [#535](https://github.com/fgmacedo/python-statemachine/issues/535) async predicates
101+
in condition expressions (`not`, `and`, `or`) were not being awaited, causing guards to
102+
silently return incorrect results.
103+
- Fixes [#548](https://github.com/fgmacedo/python-statemachine/issues/548)
104+
`VAR_POSITIONAL` and kwargs precedence bugs in the signature binding cache introduced
105+
by the performance optimization.
106+
- Fixes [#511](https://github.com/fgmacedo/python-statemachine/issues/511) Pyright/Pylance
107+
false positive "Argument missing for parameter f" when calling events. Static analyzers
108+
could not follow the metaclass transformation from `TransitionList` to `Event`.
109+
- Fixes [#551](https://github.com/fgmacedo/python-statemachine/issues/551) `MachineMixin`
110+
now gracefully skips state machine initialization for Django historical models in data
111+
migrations, instead of raising `ValueError`.
112+
- Fixes [#526](https://github.com/fgmacedo/python-statemachine/issues/526) sanitize project
113+
path on Windows for documentation builds.
114+
115+
116+
## Misc in 2.6.0
117+
118+
- Added Python 3.14 support [#552](https://github.com/fgmacedo/python-statemachine/pull/552).
119+
- Upgraded dev dependencies: ruff to 0.15.0, mypy to 1.14.1
120+
[#552](https://github.com/fgmacedo/python-statemachine/pull/552).
121+
- Clarified conditional transition evaluation order in documentation
122+
[#546](https://github.com/fgmacedo/python-statemachine/pull/546).
123+
- Added pydot DPI resolution settings to diagram documentation
124+
[#514](https://github.com/fgmacedo/python-statemachine/pull/514).
125+
- Fixed miscellaneous typos in documentation
126+
[#522](https://github.com/fgmacedo/python-statemachine/pull/522).
127+
- Removed Python 3.7 from CI build matrix
128+
[ef351d5](https://github.com/fgmacedo/python-statemachine/commit/ef351d5).

docs/releases/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Below are release notes through StateMachine and its patch releases.
2424
```{toctree}
2525
:maxdepth: 2
2626
27+
2.6.0
2728
2.5.0
2829
2.4.0
2930
2.3.6

0 commit comments

Comments
 (0)