Skip to content

Commit c5ad450

Browse files
committed
Merge tag 'v2.4.0' into develop
*November 5, 2024* This release introduces powerful new features for the `StateMachine` library: {ref}`Condition expressions` and explicit definition of {ref}`Events`. These updates make it easier to define complex transition conditions and enhance performance, especially in workflows with nested or recursive event structures. StateMachine 2.4.0 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, and 3.13. This release introduces support for conditionals with Boolean algebra. You can now use expressions like `or`, `and`, and `not` directly within transition conditions, simplifying the definition of complex state transitions. This allows for more flexible and readable condition setups in your state machine configurations. Example (with a spoiler of the next highlight): ```py >>> from statemachine import StateMachine, State, Event >>> class AnyConditionSM(StateMachine): ... start = State(initial=True) ... end = State(final=True) ... ... submit = Event( ... start.to(end, cond="used_money or used_credit"), ... name="finish order", ... ) ... ... used_money: bool = False ... used_credit: bool = False >>> sm = AnyConditionSM() >>> sm.submit() Traceback (most recent call last): TransitionNotAllowed: Can't finish order when in Start. >>> sm.used_credit = True >>> sm.submit() >>> sm.current_state.id 'end' ``` ```{seealso} See {ref}`Condition expressions` for more details or take a look at the {ref}`sphx_glr_auto_examples_lor_machine.py` example. ``` Now you can explicit declare {ref}`Events` using the {ref}`event` class. This allows custom naming, translations, and also helps your IDE to know that events are callable. ```py >>> from statemachine import StateMachine, State, Event >>> class StartMachine(StateMachine): ... created = State(initial=True) ... started = State(final=True) ... ... start = Event(created.to(started), name="Launch the machine") ... >>> [e.id for e in StartMachine.events] ['start'] >>> [e.name for e in StartMachine.events] ['Launch the machine'] >>> StartMachine.start.name 'Launch the machine' ``` ```{seealso} See {ref}`Events` for more details. ``` We removed a note from the docs saying to avoid recursion loops. Since the {ref}`StateMachine 2.0.0` release we've turned the RTC model enabled by default, allowing nested events to occour as all events are put on an internal queue before being executed. ```{seealso} See {ref}`sphx_glr_auto_examples_recursive_event_machine.py` for an example of an infinite loop state machine declaration using `after` action callback to call the same event over and over again. ``` - Fixes [#484](#484) issue where nested events inside loops could leak memory by incorrectly referencing previous `event_data` when queuing the next event. This fix improves performance and stability in event-heavy workflows.
2 parents 9cadf84 + 9434716 commit c5ad450

13 files changed

Lines changed: 352 additions & 61 deletions

File tree

docs/actions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ It's also possible to use an event name as action.
160160

161161
## Transition actions
162162

163-
For each {ref}`event`, you can register `before`, `on`, and `after` callbacks.
163+
For each {ref}`events`, you can register `before`, `on`, and `after` callbacks.
164164

165165
### Declare transition actions by naming convention
166166

docs/guards.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,13 @@ Boolean Expression:
104104
```
105105

106106
```{seealso}
107-
See {ref}`sphx_glr_auto_examples_air_conditioner_machine.py` for an example of
108-
combining multiple transitions to the same event.
107+
See {ref}`sphx_glr_auto_examples_lor_machine.py` for an example of
108+
using boolean algebra in conditions.
109109
```
110110

111111
```{seealso}
112-
See {ref}`sphx_glr_auto_examples_lor_machine.py` for an example of
113-
using boolean algebra in conditions.
112+
See {ref}`sphx_glr_auto_examples_air_conditioner_machine.py` for an example of
113+
combining multiple transitions to the same event.
114114
```
115115

116116
```{hint}

docs/releases/2.4.0.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# StateMachine 2.4.0
2+
3+
*November 5, 2024*
4+
5+
## What's new in 2.4.0
6+
7+
This release introduces powerful new features for the `StateMachine` library: {ref}`Condition expressions` and explicit definition of {ref}`Events`. These updates make it easier to define complex transition conditions and enhance performance, especially in workflows with nested or recursive event structures.
8+
9+
### Python compatibility in 2.4.0
10+
11+
StateMachine 2.4.0 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, and 3.13.
12+
13+
### Conditions expressions in 2.4.0
14+
15+
This release introduces support for conditionals with Boolean algebra. You can now use expressions like `or`, `and`, and `not` directly within transition conditions, simplifying the definition of complex state transitions. This allows for more flexible and readable condition setups in your state machine configurations.
16+
17+
Example (with a spoiler of the next highlight):
18+
19+
```py
20+
>>> from statemachine import StateMachine, State, Event
21+
22+
>>> class AnyConditionSM(StateMachine):
23+
... start = State(initial=True)
24+
... end = State(final=True)
25+
...
26+
... submit = Event(
27+
... start.to(end, cond="used_money or used_credit"),
28+
... name="finish order",
29+
... )
30+
...
31+
... used_money: bool = False
32+
... used_credit: bool = False
33+
34+
>>> sm = AnyConditionSM()
35+
>>> sm.submit()
36+
Traceback (most recent call last):
37+
TransitionNotAllowed: Can't finish order when in Start.
38+
39+
>>> sm.used_credit = True
40+
>>> sm.submit()
41+
>>> sm.current_state.id
42+
'end'
43+
44+
```
45+
46+
```{seealso}
47+
See {ref}`Condition expressions` for more details or take a look at the {ref}`sphx_glr_auto_examples_lor_machine.py` example.
48+
```
49+
50+
### Explicit event creation in 2.4.0
51+
52+
Now you can explicit declare {ref}`Events` using the {ref}`event` class. This allows custom naming, translations, and also helps your IDE to know that events are callable.
53+
54+
```py
55+
>>> from statemachine import StateMachine, State, Event
56+
57+
>>> class StartMachine(StateMachine):
58+
... created = State(initial=True)
59+
... started = State(final=True)
60+
...
61+
... start = Event(created.to(started), name="Launch the machine")
62+
...
63+
>>> [e.id for e in StartMachine.events]
64+
['start']
65+
>>> [e.name for e in StartMachine.events]
66+
['Launch the machine']
67+
>>> StartMachine.start.name
68+
'Launch the machine'
69+
70+
```
71+
72+
```{seealso}
73+
See {ref}`Events` for more details.
74+
```
75+
76+
### Recursive state machines (infinite loop)
77+
78+
We removed a note from the docs saying to avoid recursion loops. Since the {ref}`StateMachine 2.0.0` release we've turned the RTC model enabled by default, allowing nested events to occour as all events are put on an internal queue before being executed.
79+
80+
```{seealso}
81+
See {ref}`sphx_glr_auto_examples_recursive_event_machine.py` for an example of an infinite loop state machine declaration using `after` action callback to call the same event over and over again.
82+
83+
```
84+
85+
86+
## Bugfixes in 2.4.0
87+
88+
- Fixes [#484](https://github.com/fgmacedo/python-statemachine/issues/484) issue where nested events inside loops could leak memory by incorrectly
89+
referencing previous `event_data` when queuing the next event. This fix improves performance and stability in event-heavy workflows.

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.4.0
1819
2.3.6
1920
2.3.5
2021
2.3.4

docs/transitions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ An {ref}`Event` declared as string will have its `name` set equal to its `id`. T
313313

314314
In the next major release, `Event.name` will default to a capitalized version of `id` (i.e., `Event.id.replace("_", " ").capitalize()`).
315315

316-
Starting from version 2.3.7, use `Event.id` to check for event identifiers instead of `Event.name`.
316+
Starting from version 2.4.0, use `Event.id` to check for event identifiers instead of `Event.name`.
317317

318318
```
319319

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "python-statemachine"
3-
version = "2.3.6"
3+
version = "2.4.0"
44
description = "Python Finite State Machines made easy."
55
authors = ["Fernando Macedo <fgmacedo@gmail.com>"]
66
maintainers = ["Fernando Macedo <fgmacedo@gmail.com>"]

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.3.6"
7+
__version__ = "2.4.0"
88

99
__all__ = ["StateMachine", "State", "Event"]

statemachine/exceptions.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .i18n import _
44

55
if TYPE_CHECKING:
6+
from .event import Event
67
from .state import State
78

89

@@ -31,8 +32,8 @@ class AttrNotFound(InvalidDefinition):
3132
class TransitionNotAllowed(StateMachineError):
3233
"Raised when there's no transition that can run from the current :ref:`state`."
3334

34-
def __init__(self, event: str, state: "State"):
35+
def __init__(self, event: "Event", state: "State"):
3536
self.event = event
3637
self.state = state
37-
msg = _("Can't {} when in {}.").format(self.event, self.state.name)
38+
msg = _("Can't {} when in {}.").format(self.event.name, self.state.name)
3839
super().__init__(msg)

statemachine/locale/en/LC_MESSAGES/statemachine.po

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44
msgid ""
55
msgstr ""
6-
"Project-Id-Version: 2.3.0\n"
6+
"Project-Id-Version: 2.4.0\n"
77
"Report-Msgid-Bugs-To: fgmacedo@gmail.com\n"
88
"POT-Creation-Date: 2023-03-04 16:10-0300\n"
99
"PO-Revision-Date: 2024-06-07 17:41-0300\n"
@@ -13,68 +13,80 @@ msgstr ""
1313
"Content-Transfer-Encoding: 8bit\n"
1414
"Generated-By: Babel 2.12.1\n"
1515

16-
#: statemachine/callbacks.py:289
16+
#: statemachine/callbacks.py:165
17+
msgid "Failed to parse boolean expression '{}'"
18+
msgstr ""
19+
20+
#: statemachine/callbacks.py:407 statemachine/callbacks.py:412
1721
msgid "Did not found name '{}' from model or statemachine"
1822
msgstr ""
1923

20-
#: statemachine/exceptions.py:23
24+
#: statemachine/exceptions.py:24
2125
msgid "{!r} is not a valid state value."
2226
msgstr ""
2327

24-
#: statemachine/exceptions.py:37
28+
#: statemachine/exceptions.py:38
2529
msgid "Can't {} when in {}."
2630
msgstr ""
2731

28-
#: statemachine/factory.py:73
32+
#: statemachine/factory.py:74
2933
msgid "There are no states."
3034
msgstr ""
3135

32-
#: statemachine/factory.py:76
36+
#: statemachine/factory.py:77
3337
msgid "There are no events."
3438
msgstr ""
3539

36-
#: statemachine/factory.py:88
40+
#: statemachine/factory.py:89
3741
msgid ""
3842
"There should be one and only one initial state. You currently have these:"
3943
" {!r}"
4044
msgstr ""
4145

42-
#: statemachine/factory.py:101
46+
#: statemachine/factory.py:102
4347
msgid "Cannot declare transitions from final state. Invalid state(s): {}"
4448
msgstr ""
4549

46-
#: statemachine/factory.py:109
50+
#: statemachine/factory.py:110
4751
msgid ""
4852
"All non-final states should have at least one outgoing transition. These "
4953
"states have no outgoing transition: {!r}"
5054
msgstr ""
5155

52-
#: statemachine/factory.py:123
56+
#: statemachine/factory.py:124
5357
msgid ""
5458
"All non-final states should have at least one path to a final state. "
5559
"These states have no path to a final state: {!r}"
5660
msgstr ""
5761

58-
#: statemachine/factory.py:147
62+
#: statemachine/factory.py:148
5963
msgid ""
6064
"There are unreachable states. The statemachine graph should have a single"
6165
" component. Disconnected states: {}"
6266
msgstr ""
6367

64-
#: statemachine/mixins.py:23
68+
#: statemachine/factory.py:257
69+
msgid "An event in the '{}' has no id."
70+
msgstr ""
71+
72+
#: statemachine/mixins.py:26
6573
msgid "{!r} is not a valid state machine name."
6674
msgstr ""
6775

68-
#: statemachine/state.py:152
76+
#: statemachine/state.py:155
6977
msgid "State overriding is not allowed. Trying to add '{}' to {}"
7078
msgstr ""
7179

72-
#: statemachine/statemachine.py:86
80+
#: statemachine/statemachine.py:94
7381
msgid "There are no states or transitions."
7482
msgstr ""
7583

76-
#: statemachine/statemachine.py:249
84+
#: statemachine/statemachine.py:285
7785
msgid ""
7886
"There's no current state set. In async code, did you activate the initial"
7987
" state? (e.g., `await sm.activate_initial_state()`)"
8088
msgstr ""
89+
90+
#: statemachine/engines/async_.py:22
91+
msgid "Only RTC is supported on async engine"
92+
msgstr ""
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# This file is distributed under the same license as the project.
2+
# Fernando Macedo <fgmacedo@gmail.com>, 2024.
3+
#
4+
msgid ""
5+
msgstr ""
6+
"Project-Id-Version: 2.4.0\n"
7+
"Report-Msgid-Bugs-To: fgmacedo@gmail.com\n"
8+
"POT-Creation-Date: 2023-03-04 16:10-0300\n"
9+
"PO-Revision-Date: 2024-06-07 17:41-0300\n"
10+
"Last-Translator: Fernando Macedo <fgmacedo@gmail.com>\n"
11+
"Language-Team: LANGUAGE <LL@li.org>\n"
12+
"MIME-Version: 1.0\n"
13+
"Content-Type: text/plain; charset=utf-8\n"
14+
"Content-Transfer-Encoding: 8bit\n"
15+
"Generated-By: Babel 2.14.0\n"
16+
17+
#: statemachine/callbacks.py:165
18+
msgid "Failed to parse boolean expression '{}'"
19+
msgstr "बूलियन अभिव्यक्ति '{}' को पार्स करने में विफल रहा"
20+
21+
#: statemachine/callbacks.py:407 statemachine/callbacks.py:412
22+
msgid "Did not found name '{}' from model or statemachine"
23+
msgstr "मॉडल या स्टेटमशीन में नाम '{}' नहीं मिला"
24+
25+
#: statemachine/exceptions.py:24
26+
msgid "{!r} is not a valid state value."
27+
msgstr "{!r} एक मान्य स्टेट मान नहीं है।"
28+
29+
#: statemachine/exceptions.py:38
30+
msgid "Can't {} when in {}."
31+
msgstr "{} स्थिति में {} नहीं कर सकते।"
32+
33+
#: statemachine/factory.py:74
34+
msgid "There are no states."
35+
msgstr "कोई स्टेट नहीं है।"
36+
37+
#: statemachine/factory.py:77
38+
msgid "There are no events."
39+
msgstr "कोई इवेंट नहीं है।"
40+
41+
#: statemachine/factory.py:89
42+
msgid ""
43+
"There should be one and only one initial state. You currently have these:"
44+
" {!r}"
45+
msgstr "एक और केवल एक प्रारंभिक स्टेट होना चाहिए। वर्तमान में आपके पास ये हैं: {!r}"
46+
47+
#: statemachine/factory.py:102
48+
msgid "Cannot declare transitions from final state. Invalid state(s): {}"
49+
msgstr "अंतिम स्टेट से ट्रांज़िशन घोषित नहीं कर सकते। अमान्य स्टेट: {}"
50+
51+
#: statemachine/factory.py:110
52+
msgid ""
53+
"All non-final states should have at least one outgoing transition. These "
54+
"states have no outgoing transition: {!r}"
55+
msgstr "सभी गैर-अंतिम स्टेट में कम से कम एक आउटगोइंग ट्रांज़िशन होना चाहिए। इन स्टेट में कोई आउटगोइंग ट्रांज़िशन नहीं है: {!r}"
56+
57+
#: statemachine/factory.py:124
58+
msgid ""
59+
"All non-final states should have at least one path to a final state. "
60+
"These states have no path to a final state: {!r}"
61+
msgstr "सभी गैर-अंतिम स्टेट में अंतिम स्टेट तक कम से कम एक पथ होना चाहिए। इन स्टेट में अंतिम स्टेट तक कोई पथ नहीं है: {!r}"
62+
63+
#: statemachine/factory.py:148
64+
msgid ""
65+
"There are unreachable states. The statemachine graph should have a single"
66+
" component. Disconnected states: {}"
67+
msgstr "कुछ स्टेट पहुंच योग्य नहीं हैं। स्टेटमशीन ग्राफ में एक ही घटक होना चाहिए। डिस्कनेक्टेड स्टेट: {}"
68+
69+
#: statemachine/factory.py:257
70+
msgid "An event in the '{}' has no id."
71+
msgstr "'{}' में एक इवेंट का आईडी नहीं है।"
72+
73+
#: statemachine/mixins.py:26
74+
msgid "{!r} is not a valid state machine name."
75+
msgstr "{!r} एक मान्य स्टेटमशीन नाम नहीं है।"
76+
77+
#: statemachine/state.py:155
78+
msgid "State overriding is not allowed. Trying to add '{}' to {}"
79+
msgstr "स्टेट ओवरराइड करना अनुमति नहीं है। '{}' को {} में जोड़ने की कोशिश कर रहे हैं"
80+
81+
#: statemachine/statemachine.py:94
82+
msgid "There are no states or transitions."
83+
msgstr "कोई स्टेट या ट्रांज़िशन नहीं हैं।"
84+
85+
#: statemachine/statemachine.py:285
86+
msgid ""
87+
"There's no current state set. In async code, did you activate the initial"
88+
" state? (e.g., `await sm.activate_initial_state()`)"
89+
msgstr "कोई वर्तमान स्टेट सेट नहीं है। असिंक्रोनस कोड में, क्या आपने प्रारंभिक स्टेट को सक्रिय किया? (उदाहरण: `await sm.activate_initial_state()`)"
90+
91+
#: statemachine/engines/async_.py:22
92+
msgid "Only RTC is supported on async engine"
93+
msgstr "असिंक्रोनस इंजन पर केवल RTC समर्थित है"

0 commit comments

Comments
 (0)