Skip to content

Commit 08d93ce

Browse files
authored
feat: Internal transition. Closes #78 (#328)
feat: Internal transitions.
1 parent e591f32 commit 08d93ce

File tree

11 files changed

+299
-77
lines changed

11 files changed

+299
-77
lines changed
8.61 KB
Loading

docs/releases/1.0.1.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Example:
5555
See {ref}`diagrams` for more details.
5656
```
5757

58-
### Unified dispatch mecanism for callbacks (actions and guards)
58+
### Unified dispatch mechanism for callbacks (actions and guards)
5959

6060
Every single callback, being {ref}`actions` or {ref}`guards`, is now handled equally by the library.
6161

@@ -81,7 +81,7 @@ See {ref}`dynamic-dispatch` for more details.
8181

8282
### Add observers to a running StateMachine
8383

84-
Observers are a way do generically add behaviour to a StateMachine without
84+
Observers are a way do generically add behavior to a StateMachine without
8585
changing it's internal implementation.
8686

8787
The `StateMachine` itself is registered as an observer, so by using `StateMachine.add_observer()`
@@ -162,7 +162,7 @@ See {ref}`State actions` for more details.
162162
Statemachine integrity checks are now performed at class declaration (import time) instead of on
163163
instance creation. This allows early feedback of invalid definitions.
164164

165-
This was the previous behaviour, you only got an error when trying to instantiate a StateMachine:
165+
This was the previous behavior, you only got an error when trying to instantiate a StateMachine:
166166

167167
```py
168168
class CampaignMachine(StateMachine):
@@ -206,7 +206,7 @@ with pytest.raises(exceptions.InvalidDefinition):
206206
called `TransitionList` that implements de `OR` operator. This turns a valid StateMachine
207207
traversal much easier: `[transition for state in machine.states for transition in state.transitions]`.
208208
- `StateMachine.get_transition` is removed. See {ref}`event`.
209-
- The previous excetions `MultipleStatesFound` and `MultipleTransitionCallbacksFound` are removed.
209+
- The previous exceptions `MultipleStatesFound` and `MultipleTransitionCallbacksFound` are removed.
210210
Since now you can have more than one callback defined to the same transition.
211211
- `on_enter_state` and `on_exit_state` now accepts any combination of parameters following the
212212
{ref}`dynamic-dispatch` rules. Previously it only accepted the `state` param.

docs/releases/2.0.0.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# StateMachine 2.0.0
2+
3+
*Not released yet*
4+
5+
Welcome to StateMachine 2.0.0!
6+
7+
This version is the first to take advantage of the Python3 improvements. We're on our way
8+
to implement features following the SCXML specs. We hope that
9+
you enjoy.
10+
11+
These release notes cover the [](#whats-new-in-20), as well as
12+
some [backwards incompatible changes](#backwards-incompatible-changes-in-20) you'll
13+
want to be aware of when upgrading from StateMachine 1.*. We've
14+
[begun the deprecation process for some features](#deprecated-features-in-20).
15+
16+
17+
## Python compatibility in 2.0
18+
19+
StateMachine 2.0 supports Python 3.7, 3.8, 3.9, 3.10, and 3.11.
20+
21+
22+
## What's new in 2.0
23+
24+
25+
### Internal transitions
26+
27+
An internal transition is like a {ref}`self transition`, but in contrast, no entry or exit actions
28+
are ever executed as a result of an internal transition.
29+
30+
```py
31+
>>> from statemachine import StateMachine, State
32+
33+
>>> class TestStateMachine(StateMachine):
34+
... initial = State("initial", initial=True)
35+
...
36+
... loop = initial.to.itself(internal=True)
37+
38+
```
39+
40+
```{seealso}
41+
See {ref}`internal transition` for more details.
42+
```
43+
44+
45+
## Minor features in 2.0
46+
47+
- Modernization of the development stack to use linters.
48+
49+
50+
## Backwards incompatible changes in 2.0
51+
52+
- TODO
53+
54+
### Other backwards incompatible changes in 2.0
55+
56+
- TODO
57+
58+
## Deprecated features in 2.0
59+
60+
- TODO

docs/releases/index.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,20 @@ Each release note will tell you what's new in each version, and will also descri
99

1010
Below are release notes through StateMachine and its patch releases.
1111

12+
### 2.0 release
13+
14+
```{toctree}
15+
:maxdepth: 1
16+
17+
2.0.0
18+
19+
```
20+
1221

1322
### 1.0 release
1423

24+
This is the last release series to support Python 2.X series.
25+
1526
```{toctree}
1627
:maxdepth: 1
1728

docs/transitions.md

Lines changed: 123 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,63 @@
1010

1111
# Transitions and events
1212

13-
A machine moves from state to state through transitions. These transitions are
14-
caused by events.
13+
A state machine is typically composed of a set of {ref}`state`, {ref}`transition`, {ref}`event`,
14+
and {ref}`actions`. A state is a representation of the system's current condition or behavior.
15+
A transition represents the change in the system's state in response to an event or condition.
16+
An event is a trigger that causes the system to transition from one state to another, and action
17+
is any side-effect, which is the way a StateMachine can cause things to happen in the
18+
outside world.
1519

1620

17-
## Event
21+
Consider this traffic light machine as an example:
1822

19-
An event is an external signal that something has happened.
20-
They are send to a state machine and allow the state machine to react.
23+
![TrafficLightMachine](images/traffic_light_machine.png)
2124

22-
An event starts a {ref}`transition`, can be thought of as a "cause" that
23-
initiates a change in the state of the system.
2425

25-
In python-statemachine, an event is specified as an attribute of the
26-
statemachine class declaration or directly on the {ref}`event` parameter on
27-
a {ref}`transition`.
26+
There're three transitions, one starting from green to yellow, another from
27+
yellow to red, and another from red back to green. All these transitions
28+
are triggered by the same {ref}`event` called `cycle`.
2829

30+
This state machine could be expressed in `python-statemachine` as:
31+
32+
```{literalinclude} ../tests/examples/traffic_light_machine.py
33+
:language: python
34+
:linenos:
35+
:emphasize-lines: 18
36+
```
37+
38+
In line 18, you can say that this code defines three transitions:
39+
40+
* `green.to(yellow)`
41+
* `yellow.to(red)`
42+
* `red.to(green)`
43+
44+
And these transitions are assigned to the {ref}`event` `cycle` defined at the class level.
2945

3046
## Transition
3147

32-
In an executing state machine, a transition is the instantaneous transfer
33-
from one state to another. In a state machine, a transition tells us what
34-
happens when an {ref}`event` occurs.
48+
In an executing state machine, a transition is a transfer from one state to another. In a state machine, a transition tells us what happens when an {ref}`event` occurs.
3549

36-
A self transition is a transition that goes from and to the same state.
3750

38-
A transition can define actions that will be executed whenever that transition
51+
```{tip}
52+
A transition can define {ref}`actions` that will be executed whenever that transition
3953
is executed.
4054
55+
An action associated with an event (before, on, after), will be assigned to all transitions
56+
bounded that uses the event as trigger.
57+
```
58+
4159
```{eval-rst}
4260
.. autoclass:: statemachine.transition.Transition
4361
:members:
4462
```
4563

64+
```{hint}
65+
Usually you don't need to import and use a {ref}`transition` class directly in your code,
66+
one of the most powerful features of this library is now transitions and events can be expressed
67+
linking directly from/to {ref}`state` instances.
68+
```
69+
4670
(self-transition)=
4771

4872
### Self transition
@@ -59,49 +83,104 @@ TransitionList([Transition(State('Draft', ...
5983

6084
```
6185

62-
### Example
86+
### Internal transition
6387

64-
Consider this traffic light machine as example:
88+
It's like a {ref}`self transition`.
6589

66-
![TrafficLightMachine](images/traffic_light_machine.png)
90+
But in contrast to a self-transition, no entry or exit actions are ever executed as a result of an internal transition.
6791

6892

69-
There're three transitions, one starting from green to yellow, another from
70-
yellow to red, and another from red back to green. All these transitions
71-
are triggered by the same {ref}`event` called `cycle`.
93+
Syntax:
7294

73-
This statemachine could be expressed in python-statemachine as:
95+
```py
96+
>>> draft = State("Draft")
7497

98+
>>> draft.to.itself(internal=True)
99+
TransitionList([Transition(State('Draft', ...
75100

76-
```{literalinclude} ../tests/examples/traffic_light_machine.py
77-
:language: python
78-
:linenos:
79-
:emphasize-lines: 18
80101
```
81102

82-
At line 18, you can say that this code defines three transitions:
103+
Example:
83104

84-
* `green.to(yellow)`
85-
* `yellow.to(red)`
86-
* `red.to(green)`
105+
```py
106+
>>> class TestStateMachine(StateMachine):
107+
... initial = State("initial", initial=True)
108+
...
109+
... external_loop = initial.to.itself(on="do_something")
110+
... internal_loop = initial.to.itself(internal=True, on="do_something")
111+
...
112+
... def __init__(self):
113+
... self.calls = []
114+
... super().__init__()
115+
...
116+
... def do_something(self):
117+
... self.calls.append("do_something")
118+
...
119+
... def on_exit_initial(self):
120+
... self.calls.append("on_exit_initial")
121+
...
122+
... def on_enter_initial(self):
123+
... self.calls.append("on_enter_initial")
124+
125+
```
126+
Usage:
127+
128+
```py
129+
>>> sm = TestStateMachine()
130+
131+
>>> sm._graph().write_png("docs/images/test_state_machine_internal.png")
132+
133+
>>> sm.calls.clear()
134+
135+
>>> sm.external_loop()
136+
137+
>>> sm.calls
138+
['on_exit_initial', 'do_something', 'on_enter_initial']
139+
140+
>>> sm.calls.clear()
141+
142+
>>> sm.internal_loop()
87143

88-
And these transitions are assigned to the {ref}`event` `cycle` defined at
89-
class level.
144+
>>> sm.calls
145+
['do_something']
90146

91-
When an {ref}`event` is send to a statemachine:
147+
```
148+
149+
![TestStateMachine](images/test_state_machine_internal.png)
150+
151+
```{note}
152+
153+
The internal transition is represented like an entry/exit action, where
154+
the event name is used to describe the transition.
155+
156+
```
157+
158+
159+
## Event
160+
161+
An event is an external signal that something has happened.
162+
They are sent to a state machine and allow the state machine to react.
163+
164+
An event starts a {ref}`transition`, which can be thought of as a "cause" that
165+
initiates a change in the state of the system.
166+
167+
In `python-statemachine`, an event is specified as an attribute of the state machine class declaration or directly on the {ref}`event` parameter on a {ref}`transition`.
92168

93-
1. Uses the current {ref}`state` to check for available transitions.
94-
1. For each possible transition, it checks for those that matches the received {ref}`event`.
95-
1. The target state, if the transition succeeds, is determined by a transition
96-
that an event matches and;
97-
1. All {ref}`validators-and-guards`, including {ref}`actions`
98-
attached to the `on_<event>` and `before_<event>` callbacks.
169+
### Triggering events
99170

171+
Triggering an event on a state machine means invoking or sending a signal, initiating the
172+
process that may result in executing a transition.
100173

101-
## Triggering events
174+
This process usually involves checking the current state, evaluating any guard conditions
175+
associated with the transition, executing any actions associated with the transition and states,
176+
and finally updating the current state.
177+
178+
```{seealso}
179+
See {ref}`actions` and {ref}`validators-and-guards`.
180+
```
102181

103182

104-
By direct calling the event:
183+
You can invoke the event in an imperative syntax:
105184

106185
```py
107186
>>> machine = TrafficLightMachine()
@@ -114,7 +193,7 @@ By direct calling the event:
114193

115194
```
116195

117-
In a running (interpreted) machine, events are `send`:
196+
Or in an event-oriented style, events are `send`:
118197

119198
```py
120199
>>> machine.send("cycle")
@@ -126,8 +205,8 @@ In a running (interpreted) machine, events are `send`:
126205
```
127206

128207
You can also pass positional and keyword arguments, that will be propagated
129-
to the actions. On this example, the :code:`TrafficLightMachine` implements
130-
an action that `echoes` back the params informed.
208+
to the actions and guards. In this example, the :code:`TrafficLightMachine` implements
209+
an action that `echoes` back the parameters informed.
131210

132211
```{literalinclude} ../tests/examples/traffic_light_machine.py
133212
:language: python

statemachine/callbacks.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ def __init__(self, func, suppress_errors=False):
2222
def __repr__(self):
2323
return "{}({!r})".format(type(self).__name__, self.func)
2424

25+
def __str__(self):
26+
return getattr(self.func, "__name__", self.func)
27+
2528
def __eq__(self, other):
2629
return self.func == getattr(other, "func", other)
2730

@@ -60,6 +63,10 @@ def __init__(self, func, suppress_errors=False, expected_value=True):
6063
super().__init__(func, suppress_errors)
6164
self.expected_value = expected_value
6265

66+
def __str__(self):
67+
name = super().__str__()
68+
return name if self.expected_value else "!{}".format(name)
69+
6370
def __call__(self, *args, **kwargs):
6471
return super().__call__(*args, **kwargs) == self.expected_value
6572

@@ -75,6 +82,9 @@ def __repr__(self):
7582
type(self).__name__, self.items, self.factory
7683
)
7784

85+
def __str__(self):
86+
return ", ".join(str(c) for c in self)
87+
7888
def setup(self, resolver):
7989
"""Validate configuracions"""
8090
self._resolver = resolver

0 commit comments

Comments
 (0)