Skip to content

Commit 0070f48

Browse files
authored
chore: Observers rebranded to listeners (#448)
* chore: Observers rebranded to listeners * tests: Fixing warnings * chore: updating deps
1 parent ee0be69 commit 0070f48

19 files changed

Lines changed: 344 additions & 185 deletions

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ transitions
1212
actions
1313
guards
1414
models
15-
observers
15+
listeners
1616
async
1717
mixins
1818
integrations

docs/listeners.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
2+
(observers)=
3+
# Listeners
4+
5+
Listeners are a way to generically add behavior to a state machine without
6+
changing its internal implementation.
7+
8+
One possible use case is to add a listener that prints a log message when the SM runs a
9+
transition or enters a new state.
10+
11+
Giving the {ref}`sphx_glr_auto_examples_traffic_light_machine.py` as example:
12+
13+
14+
```py
15+
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
16+
17+
>>> class LogListener(object):
18+
... def __init__(self, name):
19+
... self.name = name
20+
...
21+
... def after_transition(self, event, source, target):
22+
... print(f"{self.name} after: {source.id}--({event})-->{target.id}")
23+
...
24+
... def on_enter_state(self, target, event):
25+
... print(f"{self.name} enter: {target.id} from {event}")
26+
27+
28+
>>> sm = TrafficLightMachine(listeners=[LogListener("Paulista Avenue")])
29+
Paulista Avenue enter: green from __initial__
30+
31+
>>> sm.cycle()
32+
Paulista Avenue enter: yellow from cycle
33+
Paulista Avenue after: green--(cycle)-->yellow
34+
'Running cycle from green to yellow'
35+
36+
```
37+
38+
## Adding listeners to an instance
39+
40+
Attach listeners to an already running state machine instance using `add_listener`.
41+
42+
Exploring our example, imagine that you can implement the LED panel as a listener, that
43+
reacts to state changes and turn on/off automatically.
44+
45+
46+
``` py
47+
>>> class LedPanel:
48+
...
49+
... def __init__(self, color: str):
50+
... self.color = color
51+
...
52+
... def on_enter_state(self, target: State):
53+
... if target.id == self.color:
54+
... print(f"{self.color} turning on")
55+
...
56+
... def on_exit_state(self, source: State):
57+
... if source.id == self.color:
58+
... print(f"{self.color} turning off")
59+
60+
```
61+
62+
Adding a listener for each traffic light indicator
63+
64+
```
65+
>>> sm.add_listener(LedPanel("green"), LedPanel("yellow"), LedPanel("red")) # doctest: +ELLIPSIS
66+
TrafficLightMachine...
67+
68+
```
69+
70+
Now each "LED panel" reacts to changes in state from the state machine:
71+
72+
```py
73+
>>> sm.cycle()
74+
yellow turning off
75+
Paulista Avenue enter: red from cycle
76+
red turning on
77+
Don't move.
78+
Paulista Avenue after: yellow--(cycle)-->red
79+
'Running cycle from yellow to red'
80+
81+
>>> sm.cycle()
82+
red turning off
83+
Go ahead!
84+
Paulista Avenue enter: green from cycle
85+
green turning on
86+
Paulista Avenue after: red--(cycle)-->green
87+
'Running cycle from red to green'
88+
89+
```
90+
91+
92+
```{hint}
93+
The `StateMachine` itself is registered as a listener, so by using `listeners` an
94+
external object can have the same level of functionalities provided to the built-in class.
95+
```
96+
97+
```{tip}
98+
{ref}`domain models` are also registered as a listener.
99+
```
100+
101+
102+
```{seealso}
103+
See {ref}`actions`, {ref}`validators and guards` for a list of possible callbacks.
104+
105+
And also {ref}`dynamic-dispatch` to know more about how the lib calls methods to match
106+
their signature.
107+
```

docs/models.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ domain object to hold attributes and methods to be used on the `StateMachine` de
2020
```
2121

2222
```{hint}
23-
Domain models are registered as {ref}`observers`, so you can have the same level of functionalities
23+
Domain models are registered as {ref}`listeners`, so you can have the same level of functionalities
2424
provided to the built-in {ref}`StateMachine`, such as implementing all {ref}`actions` and
2525
{ref}`guards` on your domain model and keeping only the definition of {ref}`states` and
2626
{ref}`transitions` on the {ref}`StateMachine`.

docs/observers.md

Lines changed: 0 additions & 54 deletions
This file was deleted.

docs/releases/2.3.2.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# StateMachine 2.3.2
2+
3+
*Not released yet*
4+
5+
## What's new in 2.3.2
6+
7+
Observers are now rebranded to {ref}`listeners`. With expanted support for adding listeners when
8+
instantiating a state machine. This allows covering more use cases.
9+
10+
### Listeners at class initialization
11+
12+
Listeners are a way to generically add behavior to a state machine without changing its internal implementation.
13+
14+
Example:
15+
16+
```py
17+
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
18+
19+
>>> class LogListener(object):
20+
... def __init__(self, name):
21+
... self.name = name
22+
...
23+
... def after_transition(self, event, source, target):
24+
... print(f"{self.name} after: {source.id}--({event})-->{target.id}")
25+
...
26+
... def on_enter_state(self, target, event):
27+
... print(f"{self.name} enter: {target.id} from {event}")
28+
29+
30+
>>> sm = TrafficLightMachine(listeners=[LogListener("Paulista Avenue")])
31+
Paulista Avenue enter: green from __initial__
32+
33+
>>> sm.cycle()
34+
Paulista Avenue enter: yellow from cycle
35+
Paulista Avenue after: green--(cycle)-->yellow
36+
'Running cycle from green to yellow'
37+
38+
```
39+
40+
```{seealso}
41+
See {ref}`listeners` for more details.
42+
```
43+
44+
## Bugfixes in 2.3.2
45+
46+
-
47+
48+
## Deprecation notes
49+
50+
### Statemachine class
51+
52+
- `StateMachine.add_observer` is deprecated in favor of `StateMachine.add_listener`.

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.3.2
1819
2.3.1
1920
2.3.0
2021
2.2.0

poetry.lock

Lines changed: 26 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

statemachine/dispatcher.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919

2020
@dataclass
21-
class ObjectConfig:
22-
"""Configuration for objects passed to resolver_factory.
21+
class Listener:
22+
"""Object reference that provides attributes to be used as callbacks.
2323
2424
Args:
2525
obj: Any object that will serve as lookup for attributes.
@@ -31,8 +31,8 @@ class ObjectConfig:
3131
resolver_id: str
3232

3333
@classmethod
34-
def from_obj(cls, obj, skip_attrs=None) -> "ObjectConfig":
35-
if isinstance(obj, ObjectConfig):
34+
def from_obj(cls, obj, skip_attrs=None) -> "Listener":
35+
if isinstance(obj, Listener):
3636
return obj
3737
else:
3838
if skip_attrs is None:
@@ -42,17 +42,17 @@ def from_obj(cls, obj, skip_attrs=None) -> "ObjectConfig":
4242

4343

4444
@dataclass
45-
class ObjectConfigs:
46-
"""Configuration for objects passed to resolver_factory."""
45+
class Listeners:
46+
"""Listeners that provides attributes to be used as callbacks."""
4747

48-
items: Tuple[ObjectConfig, ...]
48+
items: Tuple[Listener, ...]
4949
all_attrs: Set[str]
5050

5151
@classmethod
52-
def from_configs(cls, configs: Iterable["ObjectConfig"]) -> "ObjectConfigs":
53-
configs = tuple(configs)
54-
all_attrs = set().union(*(config.all_attrs for config in configs))
55-
return cls(configs, all_attrs)
52+
def from_listeners(cls, listeners: Iterable["Listener"]) -> "Listeners":
53+
listeners = tuple(listeners)
54+
all_attrs = set().union(*(listener.all_attrs for listener in listeners))
55+
return cls(listeners, all_attrs)
5656

5757
def resolve(self, specs: "CallbackSpecList", registry):
5858
found_convention_specs = specs.conventional_specs & self.all_attrs
@@ -155,4 +155,4 @@ async def method(*args, **kwargs):
155155

156156

157157
def resolver_factory_from_objects(*objects: Tuple[Any, ...]):
158-
return ObjectConfigs.from_configs(ObjectConfig.from_obj(o) for o in objects)
158+
return Listeners.from_listeners(Listener.from_obj(o) for o in objects)

statemachine/state.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,6 @@ def _setup(self):
129129
self.exit.add("on_exit_state", priority=CallbackPriority.GENERIC, is_convention=True)
130130
self.exit.add(f"on_exit_{self.id}", priority=CallbackPriority.NAMING, is_convention=True)
131131

132-
def _add_observer(self, register):
133-
register(self._specs)
134-
135-
def _check_callbacks(self, check):
136-
check(self._specs)
137-
138132
def __repr__(self):
139133
return (
140134
f"{type(self).__name__}({self.name!r}, id={self.id!r}, value={self.value!r}, "

0 commit comments

Comments
 (0)