Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9434716

Browse filesBrowse files
committed
Merge branch 'release/2.4.0'
2 parents 0610c9d + 90b78fc commit 9434716
Copy full SHA for 9434716
Expand file treeCollapse file tree

38 files changed

+1631
-346
lines changed

‎.github/workflows/python-package.yml

Copy file name to clipboardExpand all lines: .github/workflows/python-package.yml
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ jobs:
4242
- name: Install dependencies
4343
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
4444
run: poetry install --no-interaction --no-root --all-extras
45+
- name: Install old pydot for 3.7 only
46+
if: matrix.python-version == 3.7
47+
run: |
48+
source .venv/bin/activate
49+
pip install pydot==2.0.0
4550
#----------------------------------------------
4651
# run ruff
4752
#----------------------------------------------

‎README.md

Copy file name to clipboardExpand all lines: README.md
+7-7Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -377,25 +377,25 @@ There's a lot more to cover, please take a look at our docs:
377377
https://python-statemachine.readthedocs.io.
378378

379379

380-
## Contributing to the project
380+
## Contributing
381381

382382
* <a class="github-button" href="https://github.com/fgmacedo/python-statemachine" data-icon="octicon-star" aria-label="Star fgmacedo/python-statemachine on GitHub">Star this project</a>
383383
* <a class="github-button" href="https://github.com/fgmacedo/python-statemachine/issues" data-icon="octicon-issue-opened" aria-label="Issue fgmacedo/python-statemachine on GitHub">Open an Issue</a>
384384
* <a class="github-button" href="https://github.com/fgmacedo/python-statemachine/fork" data-icon="octicon-repo-forked" aria-label="Fork fgmacedo/python-statemachine on GitHub">Fork</a>
385385

386386
- If you found this project helpful, please consider giving it a star on GitHub.
387387

388-
- **Contribute code**: If you would like to contribute code to this project, please submit a pull
388+
- **Contribute code**: If you would like to contribute code, please submit a pull
389389
request. For more information on how to contribute, please see our [contributing.md](contributing.md) file.
390390

391-
- **Report bugs**: If you find any bugs in this project, please report them by opening an issue
391+
- **Report bugs**: If you find any bugs, please report them by opening an issue
392392
on our GitHub issue tracker.
393393

394-
- **Suggest features**: If you have a great idea for a new feature, please let us know by opening
395-
an issue on our GitHub issue tracker.
394+
- **Suggest features**: If you have an idea for a new feature, of feels something being harder than it should be,
395+
please let us know by opening an issue on our GitHub issue tracker.
396396

397-
- **Documentation**: Help improve this project's documentation by submitting pull requests.
397+
- **Documentation**: Help improve documentation by submitting pull requests.
398398

399-
- **Promote the project**: Help spread the word about this project by sharing it on social media,
399+
- **Promote the project**: Help spread the word by sharing on social media,
400400
writing a blog post, or giving a talk about it. Tag me on Twitter
401401
[@fgmacedo](https://twitter.com/fgmacedo) so I can share it too!

‎docs/actions.md

Copy file name to clipboardExpand all lines: docs/actions.md
+1-5Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,6 @@ Use the `enter` or `exit` params available on the `State` constructor.
135135

136136
```{hint}
137137
It's also possible to use an event name as action.
138-
139-
**Be careful to not introduce recursion errors** that will raise `RecursionError` exception.
140138
```
141139

142140
### Bind state actions using decorator syntax
@@ -162,7 +160,7 @@ It's also possible to use an event name as action.
162160

163161
## Transition actions
164162

165-
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.
166164

167165
### Declare transition actions by naming convention
168166

@@ -221,8 +219,6 @@ using the patterns:
221219

222220
```{hint}
223221
It's also possible to use an event name as action to chain transitions.
224-
225-
**Be careful to not introduce recursion errors**, like `loop = initial.to.itself(after="loop")`, that will raise `RecursionError` exception.
226222
```
227223

228224
### Bind transition actions using decorator syntax

‎docs/api.md

Copy file name to clipboardExpand all lines: docs/api.md
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@
6666
:members:
6767
```
6868

69+
## Event
70+
71+
```{eval-rst}
72+
.. autoclass:: statemachine.event.Event
73+
:members: id, name, __call__
74+
```
75+
6976
## EventData
7077

7178
```{eval-rst}

‎docs/async.md

Copy file name to clipboardExpand all lines: docs/async.md
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
Support for async code was added!
55
```
66

7-
The {ref}`StateMachine` fully supports asynchronous code. You can write async {ref}`actions`, {ref}`guards`, and {ref}`event` triggers, while maintaining the same external API for both synchronous and asynchronous codebases.
7+
The {ref}`StateMachine` fully supports asynchronous code. You can write async {ref}`actions`, {ref}`guards`, and {ref}`events` triggers, while maintaining the same external API for both synchronous and asynchronous codebases.
88

9-
This is achieved through a new concept called "engine," an internal strategy pattern abstraction that manages transitions and callbacks.
9+
This is achieved through a new concept called **engine**, an internal strategy pattern abstraction that manages transitions and callbacks.
1010

1111
There are two engines, {ref}`SyncEngine` and {ref}`AsyncEngine`.
1212

‎docs/guards.md

Copy file name to clipboardExpand all lines: docs/guards.md
+71-5Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,84 @@ A condition is generally a boolean function, property, or attribute, and must no
3030

3131
There are two variations of Guard clauses available:
3232

33-
3433
cond
35-
: A list of conditions, acting like predicates. A transition is only allowed to occur if
34+
: A list of condition expressions, acting like predicates. A transition is only allowed to occur if
3635
all conditions evaluate to ``True``.
37-
* Single condition: `cond="condition"`
38-
* Multiple conditions: `cond=["condition1", "condition2"]`
36+
* Single condition expression: `cond="condition"` / `cond="<condition expression>"`
37+
* Multiple condition expressions: `cond=["condition1", "condition2"]`
3938

4039
unless
4140
: Same as `cond`, but the transition is only allowed if all conditions evaluate to ``False``.
42-
* Single condition: `unless="condition"`
41+
* Single condition: `unless="condition"` / `unless="<condition expression>"`
4342
* Multiple conditions: `unless=["condition1", "condition2"]`
4443

44+
### Condition expressions
45+
46+
This library supports a mini-language for boolean expressions in conditions, allowing the definition of guards that control transitions based on specified criteria. It includes basic [boolean algebra](https://en.wikipedia.org/wiki/Boolean_algebra) operators, parentheses for controlling precedence, and **names** that refer to attributes on the state machine, its associated model, or registered {ref}`Listeners`.
47+
48+
```{tip}
49+
All condition expressions are evaluated when the State Machine is instantiated. This is by design to help you catch any invalid definitions early, rather than when your state machine is running.
50+
```
51+
52+
The mini-language is based on Python's built-in language and the [`ast`](https://docs.python.org/3/library/ast.html) parser, so there are no surprises if you’re familiar with Python. Below is a formal specification to clarify the structure.
53+
54+
#### Syntax elements
55+
56+
1. **Names**:
57+
- Names refer to attributes on the state machine instance, its model or listeners, used directly in expressions to evaluate conditions.
58+
- Names must consist of alphanumeric characters and underscores (`_`) and cannot begin with a digit (e.g., `is_active`, `count`, `has_permission`).
59+
- Any property name used in the expression must exist as an attribute on the state machine, model instance, or listeners, otherwise, an `InvalidDefinition` error is raised.
60+
- Names can be pointed to `properties`, `attributes` or `methods`. If pointed to `attributes`, the library will create a
61+
wrapper get method so each time the expression is evaluated the current value will be retrieved.
62+
63+
2. **Boolean operators and precedence**:
64+
- The following Boolean operators are supported, listed from highest to lowest precedence:
65+
1. `not` / `!` — Logical negation
66+
2. `and` / `^` — Logical conjunction
67+
3. `or` / `v` — Logical disjunction
68+
- These operators are case-sensitive (e.g., `NOT` and `Not` are not equivalent to `not` and will raise syntax errors).
69+
- Both formats can be used interchangeably, so `!sauron_alive` and `not sauron_alive` are equivalent.
70+
71+
3. **Parentheses for precedence**:
72+
- When operators with the same precedence appear in the expression, evaluation proceeds from left to right, unless parentheses specify a different order.
73+
- Parentheses `(` and `)` are supported to control the order of evaluation in expressions.
74+
- Expressions within parentheses are evaluated first, allowing explicit precedence control (e.g., `(is_admin or is_moderator) and has_permission`).
75+
76+
#### Expression Examples
77+
78+
Examples of valid boolean expressions include:
79+
- `is_logged_in and has_permission`
80+
- `not is_active or is_admin`
81+
- `!(is_guest ^ has_access)`
82+
- `(is_admin or is_moderator) and !is_banned`
83+
- `has_account and (verified or trusted)`
84+
- `frodo_has_ring and gandalf_present or !sauron_alive`
85+
86+
Being used on a transition definition:
87+
88+
```python
89+
start.to(end, cond="frodo_has_ring and gandalf_present or !sauron_alive")
90+
```
91+
92+
#### Summary of grammar rules
93+
94+
The mini-language is formally specified as follows:
95+
96+
```
97+
Name: [A-Za-z_][A-Za-z0-9_]*
98+
Boolean Expression:
99+
100+
<boolean_expr> ::= <term> | <boolean_expr> 'or' <term> | <boolean_expr> 'v' <term>
101+
<term> ::= <factor> | <term> 'and' <factor> | <term> '^' <factor>
102+
<factor> ::= 'not' <factor> | '!' <factor> | '(' <boolean_expr> ')' | <name>
103+
104+
```
105+
106+
```{seealso}
107+
See {ref}`sphx_glr_auto_examples_lor_machine.py` for an example of
108+
using boolean algebra in conditions.
109+
```
110+
45111
```{seealso}
46112
See {ref}`sphx_glr_auto_examples_air_conditioner_machine.py` for an example of
47113
combining multiple transitions to the same event.

‎docs/installation.md

Copy file name to clipboardExpand all lines: docs/installation.md
+7-1Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33

44
## Latest release
55

6-
To install Python State Machine using [poetry](https://python-poetry.org/):
6+
To install using [uv](https://docs.astral.sh/uv):
7+
8+
```shell
9+
uv add python-statemachine
10+
```
11+
12+
To install using [poetry](https://python-poetry.org/):
713

814
```shell
915
poetry add python-statemachine

‎docs/releases/2.4.0.md

Copy file name to clipboard
+89Lines 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

Copy file name to clipboardExpand all lines: docs/releases/index.md
+1Lines 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

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.