Skip to content

Commit 9cadf84

Browse files
committed
docs: Improving docs for events
1 parent f22bae0 commit 9cadf84

11 files changed

Lines changed: 111 additions & 37 deletions

File tree

README.md

Lines 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/api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,11 @@
6666
:members:
6767
```
6868

69-
## Event (class)
69+
## Event
7070

7171
```{eval-rst}
7272
.. autoclass:: statemachine.event.Event
73-
:members:
73+
:members: id, name, __call__
7474
```
7575

7676
## EventData

docs/async.md

Lines 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

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,32 +30,78 @@ 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

45-
Conditions also support [Boolean algebra](https://en.wikipedia.org/wiki/Boolean_algebra) expressions, allowing you to use compound logic within transition guards. You can use both standard Python logical operators (`not`, `and`, `or`) as well as classic Boolean algebra symbols:
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.
4670

47-
- `!` for `not`
48-
- `^` for `and`
49-
- `v` for `or`
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`).
5075

51-
For example:
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:
5287

5388
```python
5489
start.to(end, cond="frodo_has_ring and gandalf_present or !sauron_alive")
5590
```
5691

57-
Both formats can be used interchangeably, so `!sauron_alive` and `not sauron_alive` are equivalent.
92+
#### Summary of grammar rules
5893

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+
```
59105

60106
```{seealso}
61107
See {ref}`sphx_glr_auto_examples_air_conditioner_machine.py` for an example of

docs/installation.md

Lines 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/transitions.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,7 @@ the event name is used to describe the transition.
160160

161161
```
162162

163-
164-
## Event
163+
## Events
165164

166165
An event is an external signal that something has happened.
167166
They are send to a state machine and allow the state machine to react.
@@ -175,7 +174,7 @@ In `python-statemachine`, an event is specified as an attribute of the state mac
175174
### Declaring events
176175

177176
The simplest way to declare an {ref}`event` is by assiging a transitions list to a name at the
178-
State machine class level. The name will be converted to an {ref}`Event (class)`:
177+
State machine class level. The name will be converted to an {ref}`Event`:
179178

180179
```py
181180
>>> from statemachine import Event
@@ -197,7 +196,7 @@ True
197196
You can also explict declare an {ref}`Event` instance, this helps IDEs to know that the event is callable and also with transtation strings.
198197
```
199198

200-
To declare an explicit event you must also import the {ref}`Event (class)`:
199+
To declare an explicit event you must also import the {ref}`Event`:
201200

202201
```py
203202
>>> from statemachine import Event
@@ -219,7 +218,7 @@ To declare an explicit event you must also import the {ref}`Event (class)`:
219218

220219
```
221220

222-
An {ref}`Event (class)` instance or an event id string can also be used as the `event` parameter of a {ref}`transition`. So you can mix these options as you need.
221+
An {ref}`Event` instance or an event id string can also be used as the `event` parameter of a {ref}`transition`. So you can mix these options as you need.
223222

224223
```py
225224
>>> from statemachine import State, StateMachine, Event
@@ -293,22 +292,23 @@ An {ref}`Event (class)` instance or an event id string can also be used as the `
293292
```
294293

295294
```{tip}
296-
Avoid mixing these options within the same project; instead, choose the one that best serves your use case. Declaring events as strings has been the standard approach since the library’s inception and can be considered syntactic sugar, as the state machine metaclass will convert all events to {ref}`Event (class)` instances under the hood.
295+
Avoid mixing these options within the same project; instead, choose the one that best serves your use case. Declaring events as strings has been the standard approach since the library’s inception and can be considered syntactic sugar, as the state machine metaclass will convert all events to {ref}`Event` instances under the hood.
297296

298297
```
299298

300299
```{note}
301-
In order to allow the seamless upgrade from using strings to `Event` instances, the {ref}`Event (class)` inherits from `str`.
300+
In order to allow the seamless upgrade from using strings to `Event` instances, the {ref}`Event` inherits from `str`.
302301

303302
Note that this is just an implementation detail and can change in the future.
304303

305-
>>> isinstance(TrafficLightMachine.cycle, str)
306-
True
304+
>>> isinstance(TrafficLightMachine.cycle, str)
305+
True
307306

308307
```
309308

310309

311310
```{warning}
311+
312312
An {ref}`Event` declared as string will have its `name` set equal to its `id`. This is for backward compatibility when migrating from previous versions.
313313

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

statemachine/event.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@
2525

2626

2727
class Event(str):
28+
"""An event is triggers a signal that something has happened.
29+
30+
They are send to a state machine and allow the state machine to react.
31+
32+
An event starts a :ref:`Transition`, which can be thought of as a “cause” that initiates a
33+
change in the state of the system.
34+
35+
See also :ref:`events`.
36+
"""
37+
2838
id: str
2939
"""The event identifier."""
3040

@@ -84,7 +94,11 @@ def __get__(self, instance, owner):
8494
return BoundEvent(id=self.id, name=self.name, _sm=instance)
8595

8696
def __call__(self, *args, **kwargs):
87-
"""Send this event to the current state machine."""
97+
"""Send this event to the current state machine.
98+
99+
Triggering an event on a state machine means invoking or sending a signal, initiating the
100+
process that may result in executing a transition.
101+
"""
88102
# The `__call__` is declared here to help IDEs knowing that an `Event`
89103
# can be called as a method. But it is not meant to be called without
90104
# an SM instance. Such SM instance is provided by `__get__` method when

statemachine/event_data.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Any
55

66
if TYPE_CHECKING:
7+
from .event import Event
78
from .state import State
89
from .statemachine import StateMachine
910
from .transition import Transition
@@ -13,7 +14,7 @@
1314
class TriggerData:
1415
machine: "StateMachine"
1516

16-
event: str
17+
event: "Event"
1718
"""The Event that was triggered."""
1819

1920
model: Any = field(init=False)

statemachine/state.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ class State:
9292
>>> [(t.source.name, t.target.name) for t in transitions]
9393
[('Draft', 'Draft'), ('Draft', 'Producing'), ('Draft', 'Closed')]
9494
95+
Sometimes it's easier to use the :func:`State.from_` method:
96+
97+
>>> transitions = closed.from_(draft, producing, closed)
98+
99+
>>> [(t.source.name, t.target.name) for t in transitions]
100+
[('Draft', 'Closed'), ('Producing', 'Closed'), ('Closed', 'Closed')]
101+
95102
"""
96103

97104
def __init__(

statemachine/statemachine.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def __init__(
101101
if self.current_state_value is None:
102102
trigger_data = TriggerData(
103103
machine=self,
104-
event="__initial__",
104+
event=BoundEvent("__initial__", _sm=self),
105105
)
106106
self._put_nonblocking(trigger_data)
107107

0 commit comments

Comments
 (0)