Skip to content

Commit ac825d1

Browse files
committed
chore: prepare release 2.6.0
- Bump version to 2.6.0 in pyproject.toml and __init__.py - Add release notes (docs/releases/2.6.0.md) - Add data migrations section to Django integration docs - Update translations (extract, merge, translate 3 new messages) - Add babel to dev dependencies
1 parent 7211235 commit ac825d1

10 files changed

Lines changed: 340 additions & 95 deletions

File tree

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
@@ -15,6 +15,7 @@ Below are release notes through StateMachine and its patch releases.
1515
```{toctree}
1616
:maxdepth: 2
1717
18+
2.6.0
1819
2.5.0
1920
2.4.0
2021
2.3.6

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "python-statemachine"
3-
version = "2.5.0"
3+
version = "2.6.0"
44
description = "Python Finite State Machines made easy."
55
authors = [{ name = "Fernando Macedo", email = "fgmacedo@gmail.com" }]
66
maintainers = [{ name = "Fernando Macedo", email = "fgmacedo@gmail.com" }]
@@ -55,6 +55,7 @@ dev = [
5555
"furo >=2024.5.6; python_version >'3.8'",
5656
"sphinx-copybutton >=0.5.2; python_version >'3.8'",
5757
"pdbr>=0.8.9; python_version >'3.8'",
58+
"babel >=2.16.0; python_version >='3.8'",
5859
]
5960

6061
[build-system]

statemachine/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44

55
__author__ = """Fernando Macedo"""
66
__email__ = "fgmacedo@gmail.com"
7-
__version__ = "2.5.0"
7+
__version__ = "2.6.0"
88

99
__all__ = ["StateMachine", "State", "Event"]
Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,35 @@
1-
# This file is distributed under the same license as the PROJECT project.
1+
# This file is distributed under the same license as the project.
22
# Fernando Macedo <fgmacedo@gmail.com>, 2024.
33
#
44
msgid ""
55
msgstr ""
6-
"Project-Id-Version: 2.4.0\n"
6+
"Project-Id-Version: 2.4.0\n"
77
"Report-Msgid-Bugs-To: fgmacedo@gmail.com\n"
8-
"POT-Creation-Date: 2023-03-04 16:10-0300\n"
8+
"POT-Creation-Date: 2026-02-13 18:28-0300\n"
99
"PO-Revision-Date: 2024-06-07 17:41-0300\n"
1010
"Last-Translator: Fernando Macedo <fgmacedo@gmail.com>\n"
11+
"Language: en\n"
12+
"Language-Team: en <LL@li.org>\n"
13+
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
1114
"MIME-Version: 1.0\n"
1215
"Content-Type: text/plain; charset=utf-8\n"
1316
"Content-Transfer-Encoding: 8bit\n"
14-
"Generated-By: Babel 2.12.1\n"
17+
"Generated-By: Babel 2.16.0\n"
1518

16-
#: statemachine/callbacks.py:165
19+
#: statemachine/callbacks.py:349 statemachine/callbacks.py:354
20+
msgid "Did not found name '{}' from model or statemachine"
21+
msgstr ""
22+
23+
#: statemachine/dispatcher.py:126
1724
msgid "Failed to parse boolean expression '{}'"
1825
msgstr ""
1926

20-
#: statemachine/callbacks.py:407 statemachine/callbacks.py:412
21-
msgid "Did not found name '{}' from model or statemachine"
27+
#: statemachine/event.py:90
28+
msgid "Cannot add callback '{}' to an event with no transitions."
29+
msgstr ""
30+
31+
#: statemachine/event.py:123
32+
msgid "Event {} cannot be called without a SM instance"
2233
msgstr ""
2334

2435
#: statemachine/exceptions.py:24
@@ -29,64 +40,68 @@ msgstr ""
2940
msgid "Can't {} when in {}."
3041
msgstr ""
3142

32-
#: statemachine/factory.py:74
43+
#: statemachine/factory.py:73
3344
msgid "There are no states."
3445
msgstr ""
3546

36-
#: statemachine/factory.py:77
47+
#: statemachine/factory.py:76
3748
msgid "There are no events."
3849
msgstr ""
3950

40-
#: statemachine/factory.py:89
51+
#: statemachine/factory.py:88
4152
msgid ""
4253
"There should be one and only one initial state. You currently have these:"
4354
" {!r}"
4455
msgstr ""
4556

46-
#: statemachine/factory.py:102
57+
#: statemachine/factory.py:101
4758
msgid "Cannot declare transitions from final state. Invalid state(s): {}"
4859
msgstr ""
4960

50-
#: statemachine/factory.py:110
61+
#: statemachine/factory.py:109
5162
msgid ""
5263
"All non-final states should have at least one outgoing transition. These "
5364
"states have no outgoing transition: {!r}"
5465
msgstr ""
5566

56-
#: statemachine/factory.py:124
67+
#: statemachine/factory.py:123
5768
msgid ""
5869
"All non-final states should have at least one path to a final state. "
5970
"These states have no path to a final state: {!r}"
6071
msgstr ""
6172

62-
#: statemachine/factory.py:148
73+
#: statemachine/factory.py:147
6374
msgid ""
6475
"There are unreachable states. The statemachine graph should have a single"
6576
" component. Disconnected states: {}"
6677
msgstr ""
6778

68-
#: statemachine/factory.py:257
79+
#: statemachine/factory.py:253
6980
msgid "An event in the '{}' has no id."
7081
msgstr ""
7182

72-
#: statemachine/mixins.py:26
83+
#: statemachine/mixins.py:28
7384
msgid "{!r} is not a valid state machine name."
7485
msgstr ""
7586

76-
#: statemachine/state.py:155
87+
#: statemachine/state.py:194
7788
msgid "State overriding is not allowed. Trying to add '{}' to {}"
7889
msgstr ""
7990

80-
#: statemachine/statemachine.py:94
91+
#: statemachine/statemachine.py:89
8192
msgid "There are no states or transitions."
8293
msgstr ""
8394

84-
#: statemachine/statemachine.py:285
95+
#: statemachine/statemachine.py:277
8596
msgid ""
8697
"There's no current state set. In async code, did you activate the initial"
8798
" state? (e.g., `await sm.activate_initial_state()`)"
8899
msgstr ""
89100

90-
#: statemachine/engines/async_.py:22
101+
#: statemachine/transition_mixin.py:15
102+
msgid "{} only supports the decorator syntax to register callbacks."
103+
msgstr ""
104+
105+
#: statemachine/engines/async_.py:18
91106
msgid "Only RTC is supported on async engine"
92107
msgstr ""

0 commit comments

Comments
 (0)