Skip to content

Commit 762b1a9

Browse files
authored
feat: StateMachine observers (#312)
* feat: StateMachine.add_observer
1 parent 71d88bc commit 762b1a9

13 files changed

Lines changed: 177 additions & 74 deletions

File tree

README.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,6 @@ True
307307
>>> control.completed.is_active
308308
True
309309

310+
311+
There's a lot more to cover, please take a look at our docs:
312+
https://python-statemachine.readthedocs.io.

docs/actions.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -332,21 +332,34 @@ you'll be fine, if you declare an expected parameter, you'll also be covered.
332332

333333
For your convenience, all these parameters are available for you on any Action or Guard:
334334

335-
- `*args`: All positional arguments provided on the {ref}`Event`.
336335

337-
- `**kwargs`: All keyword arguments provided on the {ref}`Event`.
336+
`*args`
337+
: All positional arguments provided on the {ref}`Event`.
338338

339-
- `event_data`: A reference to `EventData` instance.
339+
`**kwargs`
340+
: All keyword arguments provided on the {ref}`Event`.
340341

341-
- `event`: The {ref}`Event` that was triggered.
342+
`event_data`
343+
: A reference to `EventData` instance.
342344

343-
- `source`: The {ref}`State` the statemachine was when the {ref}`Event` started.
345+
`event`
346+
: The {ref}`Event` that was triggered.
344347

345-
- `state`: The current {ref}`State` of the statemachine.
348+
`source`
349+
: The {ref}`State` the statemachine was when the {ref}`Event` started.
346350

347-
- `model`: A reference to the underlying model that holds the current {ref}`State`.
351+
`state`
352+
: The current {ref}`State` of the statemachine.
353+
354+
`target`
355+
: The destination {ref}`State` of the transition.
356+
357+
`model`
358+
: A reference to the underlying model that holds the current {ref}`State`.
359+
360+
`transition`
361+
: The {ref}`Transition` instance that was activated by the {ref}`Event`.
348362

349-
- `transition`: The {ref}`Transition` instance that was activated by the {ref}`Event`.
350363

351364
So, you can implement Actions and Guards like these, but this list is not exaustive, it's only
352365
to give you a few examples... any combination of parameters will work, including extra parameters

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Contents:
1212
transitions
1313
actions
1414
guards
15+
observers
1516
mixins
1617
integrations
1718
diagram

docs/observers.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
2+
# Observers
3+
4+
Observers are a way do generically add behavior to a StateMachine without
5+
changing its internal implementation.
6+
7+
One possible use case is to add an observer that prints a log message when the SM runs a
8+
transition or enters a new state.
9+
10+
Giving the {ref}`sphx_glr_auto_examples_traffic_light_machine.py` as example:
11+
12+
13+
```py
14+
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
15+
16+
>>> class LogObserver(object):
17+
... def __init__(self, name):
18+
... self.name = name
19+
...
20+
... def after_transition(self, event, source, target):
21+
... print("{} after: {}--({})-->{}".format(self.name, source.id, event, target.id))
22+
...
23+
... def on_enter_state(self, target, event):
24+
... print("{} enter: {} from {}".format(self.name, target.id, event))
25+
26+
27+
>>> sm = TrafficLightMachine()
28+
29+
>>> sm.add_observer(LogObserver("Paulista Avenue")) # doctest: +ELLIPSIS
30+
TrafficLightMachine...
31+
32+
>>> sm.cycle()
33+
Paulista Avenue enter: yellow from cycle
34+
Paulista Avenue after: green--(cycle)-->yellow
35+
'Running cycle from green to yellow'
36+
37+
```
38+
39+
```{hint}
40+
The `StateMachine` itself is registered as an observer, so by using `.add_observer()` an
41+
external object can have the same level of functionalities provided to the built-in class.
42+
```
43+
44+
```{seealso}
45+
See {ref}`actions`, {ref}`validators-and-guards` for a list of possible callbacks.
46+
47+
And also {ref}`dynamic-dispatch` to know more about how the lib calls methods to match
48+
their signature.
49+
```

docs/releases/1.0.0.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ def action_or_guard_method_name(self, *args, event_data, event, source, state, m
7979
See {ref}`dynamic-dispatch` for more details.
8080
```
8181

82+
### Add observers to a running StateMachine
83+
84+
Observers are a way do generically add behaviour to a StateMachine without
85+
changing it's internal implementation.
86+
87+
The `StateMachine` itself is registered as an observer, so by using `StateMachine.add_observer()`
88+
an external object can have the same level of functionalities provided to the built-in class.
89+
90+
```{seealso}
91+
See {ref}`observers` for more details.
92+
```
93+
8294
## Minor features in 1.0
8395

8496
- Fixed mypy complaining about incorrect type for ``StateMachine`` class.

statemachine/callbacks.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,14 @@ def clear(self):
128128
def call(self, *args, **kwargs):
129129
return [callback(*args, **kwargs) for callback in self.items]
130130

131-
def _add(self, func, prepend=False, **kwargs):
131+
def _add(self, func, resolver=None, prepend=False, **kwargs):
132132
if func in self.items:
133133
return
134134

135+
resolver = resolver or self._resolver
136+
135137
callback = self.factory(func, **kwargs)
136-
if self._resolver is not None and not callback.setup(self._resolver):
138+
if resolver is not None and not callback.setup(resolver):
137139
return
138140

139141
if prepend:

statemachine/contrib/diagram.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import pydot
33
import importlib
44

5-
from ..factory import StateMachineMetaclass
65
from ..statemachine import BaseStateMachine
76

87

@@ -29,13 +28,10 @@ def __init__(self, machine):
2928

3029
def _get_graph(self):
3130
machine = self.machine
32-
sm_class = (
33-
machine if isinstance(machine, StateMachineMetaclass) else machine.__class__
34-
)
3531
return pydot.Dot(
3632
"list",
3733
graph_type="digraph",
38-
label=sm_class.__name__,
34+
label=machine.name,
3935
fontsize=self.state_font_size,
4036
rankdir=self.graph_rankdir,
4137
)

statemachine/dispatcher.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ class ObjectConfig(namedtuple("ObjectConfig", "obj skip_attrs")):
1818

1919
@classmethod
2020
def from_obj(cls, obj):
21-
if isinstance(obj, (tuple, list)):
22-
return cls(*obj)
21+
if isinstance(obj, ObjectConfig):
22+
return obj
2323
else:
2424
return cls(obj, set())
2525

statemachine/factory.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def __init__(cls, name, bases, attrs):
1717
super(StateMachineMetaclass, cls).__init__(name, bases, attrs)
1818
registry.register(cls)
1919
cls._abstract = True
20+
cls.name = cls.__name__
2021
cls.states = []
2122
cls._events = OrderedDict()
2223
cls.states_map = {}

statemachine/state.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,13 @@ def __init__(
9191
def _setup(self, resolver):
9292
self.enter.setup(resolver)
9393
self.exit.setup(resolver)
94-
self.enter.add("on_enter_state", prepend=True, suppress_errors=True)
95-
self.enter.add("on_enter_{}".format(self.id), suppress_errors=True)
96-
self.exit.add("on_exit_state", prepend=True, suppress_errors=True)
97-
self.exit.add("on_exit_{}".format(self.id), suppress_errors=True)
94+
95+
def _add_observer(self, *resolvers):
96+
for r in resolvers:
97+
self.enter.add("on_enter_state", resolver=r, prepend=True, suppress_errors=True)
98+
self.enter.add("on_enter_{}".format(self.id), resolver=r, suppress_errors=True)
99+
self.exit.add("on_exit_state", resolver=r, prepend=True, suppress_errors=True)
100+
self.exit.add("on_exit_{}".format(self.id), resolver=r, suppress_errors=True)
98101

99102
def __repr__(self):
100103
return "{}({!r}, id={!r}, value={!r}, initial={!r}, final={!r})".format(

0 commit comments

Comments
 (0)