Skip to content

Commit b89f2b5

Browse files
authored
Merge pull request #86 from simonsobs/dev
Replace a factory method of FSM with a config dict
2 parents 28ed47d + faaf416 commit b89f2b5

File tree

5 files changed

+202
-255
lines changed

5 files changed

+202
-255
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'''The configuration of the finite state machine of the auto mode states.
2+
3+
The package "transitions" is used: https://github.com/pytransitions/transitions
4+
5+
State Diagram:
6+
7+
.-------------.
8+
| Created |
9+
'-------------'
10+
| start()
11+
|
12+
v
13+
.-------------.
14+
.-------------->| Off |<--------------.
15+
| '-------------' |
16+
| | turn_on() |
17+
turn_off() | on_raised()
18+
| | |
19+
| | |
20+
| .------------------+------------------. |
21+
| | Auto | | |
22+
| | v | |
23+
| | .-------------. | |
24+
| | | Waiting | | |
25+
| | '-------------' | |
26+
| | | on_initialized() | |
27+
| | | on_finished() | |
28+
| | v | |
29+
| | .-------------. | |
30+
| | | Pulling | | |
31+
'---| '-------------' |---'
32+
| run() | ^ |
33+
| | | |
34+
| v | on_finished() |
35+
| .-------------. |
36+
| | Running | |
37+
| '-------------' |
38+
| |
39+
'-------------------------------------'
40+
41+
>>> class Model:
42+
... def on_enter_auto_waiting(self):
43+
... print('enter the waiting state')
44+
... self.on_finished()
45+
...
46+
... def on_exit_auto_waiting(self):
47+
... print('exit the waiting state')
48+
...
49+
... def on_enter_auto_pulling(self):
50+
... print('enter the pulling state')
51+
52+
>>> from transitions.extensions import HierarchicalMachine
53+
54+
>>> model = Model()
55+
>>> machine = HierarchicalMachine(model=model, **CONFIG)
56+
>>> model.state
57+
'created'
58+
59+
>>> _ = model.start()
60+
>>> model.state
61+
'off'
62+
63+
>>> _ = model.turn_on()
64+
enter the waiting state
65+
exit the waiting state
66+
enter the pulling state
67+
68+
>>> model.state
69+
'auto_pulling'
70+
'''
71+
72+
73+
_AUTO_SUB_STATE_CONFIG = {
74+
'name': 'auto',
75+
'children': ['waiting', 'pulling', 'running'],
76+
'initial': 'waiting',
77+
'transitions': [
78+
['on_initialized', 'waiting', 'pulling'],
79+
['on_finished', 'waiting', 'pulling'],
80+
['run', 'pulling', 'running'],
81+
['on_finished', 'running', 'pulling'],
82+
],
83+
}
84+
85+
CONFIG = {
86+
'name': 'global',
87+
'states': ['created', 'off', _AUTO_SUB_STATE_CONFIG],
88+
'transitions': [
89+
['start', 'created', 'off'],
90+
['turn_on', 'off', 'auto'],
91+
['on_raised', 'auto', 'off'],
92+
{
93+
'trigger': 'turn_off',
94+
'source': 'auto',
95+
'dest': 'off',
96+
'before': 'cancel_task',
97+
},
98+
],
99+
'initial': 'created',
100+
'queued': True,
101+
'ignore_invalid_triggers': True,
102+
}

src/nextline_schedule/auto/state_machine/factory.py

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

src/nextline_schedule/auto/state_machine/machine.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
from typing import Any, Protocol
55

66
from nextline.utils import pubsub
7+
from transitions.extensions.asyncio import HierarchicalAsyncMachine
78

8-
from .factory import build_state_machine
9+
from .config import CONFIG
910

1011

1112
class CallbackType(Protocol):
@@ -31,7 +32,7 @@ def __init__(self, callback: CallbackType):
3132
self._pubsub_state = pubsub.PubSubItem[str]()
3233
self._logger = getLogger(__name__)
3334

34-
machine = build_state_machine(model=self)
35+
machine = HierarchicalAsyncMachine(model=self, **CONFIG) # type: ignore
3536
machine.after_state_change = [self.after_state_change.__name__]
3637

3738
self.state: str # attached by machine
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from copy import deepcopy
2+
from pathlib import Path
3+
from unittest.mock import AsyncMock
4+
5+
import pytest
6+
from hypothesis import given, settings
7+
from hypothesis import strategies as st
8+
from transitions import Machine
9+
from transitions.extensions import HierarchicalAsyncGraphMachine
10+
from transitions.extensions.asyncio import HierarchicalAsyncMachine
11+
from transitions.extensions.markup import HierarchicalMarkupMachine
12+
13+
from nextline_schedule.auto.state_machine.config import CONFIG
14+
15+
SELF_LITERAL = Machine.self_literal
16+
17+
18+
def test_model_default() -> None:
19+
machine = HierarchicalAsyncMachine(model=None, **CONFIG) # type: ignore
20+
assert not machine.models
21+
22+
23+
def test_model_self_literal() -> None:
24+
machine = HierarchicalAsyncMachine(model=SELF_LITERAL, **CONFIG) # type: ignore
25+
assert machine.models[0] is machine
26+
assert len(machine.models) == 1
27+
28+
29+
def test_restore_from_markup() -> None:
30+
machine = HierarchicalMarkupMachine(model=None, **CONFIG) # type: ignore
31+
assert isinstance(machine.markup, dict)
32+
markup = deepcopy(machine.markup)
33+
del markup['models'] # type: ignore
34+
rebuild = HierarchicalMarkupMachine(model=None, **markup) # type: ignore
35+
assert rebuild.markup == machine.markup
36+
37+
38+
@pytest.mark.skip
39+
def test_graph(tmp_path: Path) -> None: # pragma: no cover
40+
FILE_NAME = 'states.png'
41+
path = tmp_path / FILE_NAME
42+
# print(f'Saving the state diagram to {path}...')
43+
machine = HierarchicalAsyncGraphMachine(model=SELF_LITERAL, **CONFIG) # type: ignore
44+
machine.get_graph().draw(path, prog='dot')
45+
46+
47+
STATE_MAP = {
48+
'created': {
49+
'start': {'dest': 'off'},
50+
},
51+
'off': {
52+
'turn_on': {'dest': 'auto_waiting'},
53+
},
54+
'auto_waiting': {
55+
'turn_off': {'dest': 'off', 'before': 'cancel_task'},
56+
'on_initialized': {'dest': 'auto_pulling'},
57+
'on_finished': {'dest': 'auto_pulling'},
58+
'on_raised': {'dest': 'off'},
59+
},
60+
'auto_pulling': {
61+
'run': {'dest': 'auto_running'},
62+
'turn_off': {'dest': 'off', 'before': 'cancel_task'},
63+
'on_raised': {'dest': 'off'},
64+
},
65+
'auto_running': {
66+
'on_finished': {'dest': 'auto_pulling'},
67+
'turn_off': {'dest': 'off', 'before': 'cancel_task'},
68+
'on_raised': {'dest': 'off'},
69+
},
70+
}
71+
72+
TRIGGERS = list({trigger for v in STATE_MAP.values() for trigger in v.keys()})
73+
74+
75+
@settings(max_examples=200)
76+
@given(triggers=st.lists(st.sampled_from(TRIGGERS)))
77+
async def test_transitions(triggers: list[str]) -> None:
78+
machine = HierarchicalAsyncMachine(model=SELF_LITERAL, **CONFIG) # type: ignore
79+
assert machine.is_created()
80+
81+
for trigger in triggers:
82+
prev = machine.state
83+
if (map_ := STATE_MAP[prev].get(trigger)) is None:
84+
await getattr(machine, trigger)()
85+
assert machine.state == prev
86+
continue
87+
88+
if before := map_.get('before'):
89+
setattr(machine, before, AsyncMock())
90+
91+
assert await getattr(machine, trigger)() is True
92+
dest = map_['dest']
93+
assert getattr(machine, f'is_{dest}')()
94+
95+
if before:
96+
assert getattr(machine, before).call_count == 1
97+
assert getattr(machine, before).await_count == 1

0 commit comments

Comments
 (0)