python-statemachine 2.1.2__py3-none-any.whl → 2.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_statemachine-2.1.2.dist-info → python_statemachine-2.3.0.dist-info}/METADATA +86 -25
- python_statemachine-2.3.0.dist-info/RECORD +28 -0
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +106 -76
- statemachine/contrib/diagram.py +20 -6
- statemachine/dispatcher.py +72 -37
- statemachine/event.py +21 -20
- statemachine/exceptions.py +9 -3
- statemachine/factory.py +72 -9
- statemachine/graph.py +6 -0
- statemachine/locale/en/LC_MESSAGES/statemachine.po +40 -26
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +60 -39
- statemachine/mixins.py +1 -3
- statemachine/signature.py +15 -8
- statemachine/state.py +22 -25
- statemachine/statemachine.py +125 -97
- statemachine/states.py +6 -4
- statemachine/transition.py +35 -26
- statemachine/utils.py +19 -0
- python_statemachine-2.1.2.dist-info/RECORD +0 -30
- statemachine/locale/en/LC_MESSAGES/statemachine.mo +0 -0
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.mo +0 -0
- {python_statemachine-2.1.2.dist-info → python_statemachine-2.3.0.dist-info}/LICENSE +0 -0
- {python_statemachine-2.1.2.dist-info → python_statemachine-2.3.0.dist-info}/WHEEL +0 -0
statemachine/mixins.py
CHANGED
|
@@ -20,9 +20,7 @@ class MachineMixin:
|
|
|
20
20
|
super().__init__(*args, **kwargs)
|
|
21
21
|
if not self.state_machine_name:
|
|
22
22
|
raise ValueError(
|
|
23
|
-
_("{!r} is not a valid state machine name.").format(
|
|
24
|
-
self.state_machine_name
|
|
25
|
-
)
|
|
23
|
+
_("{!r} is not a valid state machine name.").format(self.state_machine_name)
|
|
26
24
|
)
|
|
27
25
|
machine_cls = registry.get_machine_cls(self.state_machine_name)
|
|
28
26
|
setattr(
|
statemachine/signature.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import itertools
|
|
2
1
|
from functools import partial
|
|
3
2
|
from inspect import BoundArguments
|
|
4
3
|
from inspect import Parameter
|
|
5
4
|
from inspect import Signature
|
|
5
|
+
from inspect import iscoroutinefunction
|
|
6
|
+
from itertools import chain
|
|
6
7
|
from types import MethodType
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def _make_key(method):
|
|
11
12
|
method = method.func if isinstance(method, partial) else method
|
|
13
|
+
method = method.fget if isinstance(method, property) else method
|
|
12
14
|
if isinstance(method, MethodType):
|
|
13
15
|
return hash(
|
|
14
16
|
(
|
|
@@ -22,7 +24,6 @@ def _make_key(method):
|
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
def signature_cache(user_function):
|
|
25
|
-
|
|
26
27
|
cache = {}
|
|
27
28
|
cache_get = cache.get
|
|
28
29
|
|
|
@@ -51,9 +52,16 @@ class SignatureAdapter(Signature):
|
|
|
51
52
|
|
|
52
53
|
metadata_to_copy = method.func if isinstance(method, partial) else method
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
if iscoroutinefunction(method):
|
|
56
|
+
|
|
57
|
+
async def method_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
58
|
+
ba = sig_bind_expected(*args, **kwargs)
|
|
59
|
+
return await method(*ba.args, **ba.kwargs)
|
|
60
|
+
else:
|
|
61
|
+
|
|
62
|
+
async def method_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
63
|
+
ba = sig_bind_expected(*args, **kwargs)
|
|
64
|
+
return method(*ba.args, **ba.kwargs)
|
|
57
65
|
|
|
58
66
|
method_wrapper.__name__ = metadata_to_copy.__name__
|
|
59
67
|
|
|
@@ -113,8 +121,7 @@ class SignatureAdapter(Signature):
|
|
|
113
121
|
parameters_ex = (param,)
|
|
114
122
|
break
|
|
115
123
|
elif (
|
|
116
|
-
param.kind == Parameter.VAR_KEYWORD
|
|
117
|
-
or param.default is not Parameter.empty
|
|
124
|
+
param.kind == Parameter.VAR_KEYWORD or param.default is not Parameter.empty
|
|
118
125
|
):
|
|
119
126
|
# That's fine too - we have a default value for this
|
|
120
127
|
# parameter. So, lets start parsing `kwargs`, starting
|
|
@@ -161,7 +168,7 @@ class SignatureAdapter(Signature):
|
|
|
161
168
|
|
|
162
169
|
# Now, we iterate through the remaining parameters to process
|
|
163
170
|
# keyword arguments
|
|
164
|
-
for param in
|
|
171
|
+
for param in chain(parameters_ex, parameters):
|
|
165
172
|
if param.kind == Parameter.VAR_KEYWORD:
|
|
166
173
|
# Memorize that we have a '**kwargs'-like parameter
|
|
167
174
|
kwargs_param = param
|
statemachine/state.py
CHANGED
|
@@ -4,6 +4,7 @@ from typing import Dict
|
|
|
4
4
|
from weakref import ref
|
|
5
5
|
|
|
6
6
|
from .callbacks import CallbackMetaList
|
|
7
|
+
from .callbacks import CallbackPriority
|
|
7
8
|
from .exceptions import StateMachineError
|
|
8
9
|
from .i18n import _
|
|
9
10
|
from .transition import Transition
|
|
@@ -107,31 +108,30 @@ class State:
|
|
|
107
108
|
self._final = final
|
|
108
109
|
self._id: str = ""
|
|
109
110
|
self.transitions = TransitionList()
|
|
110
|
-
self.enter = CallbackMetaList().add(enter)
|
|
111
|
-
self.exit = CallbackMetaList().add(exit)
|
|
111
|
+
self.enter = CallbackMetaList().add(enter, priority=CallbackPriority.INLINE)
|
|
112
|
+
self.exit = CallbackMetaList().add(exit, priority=CallbackPriority.INLINE)
|
|
112
113
|
|
|
113
114
|
def __eq__(self, other):
|
|
114
|
-
return (
|
|
115
|
-
isinstance(other, State) and self.name == other.name and self.id == other.id
|
|
116
|
-
)
|
|
115
|
+
return isinstance(other, State) and self.name == other.name and self.id == other.id
|
|
117
116
|
|
|
118
117
|
def __hash__(self):
|
|
119
118
|
return hash(repr(self))
|
|
120
119
|
|
|
121
|
-
def _setup(self
|
|
120
|
+
def _setup(self):
|
|
121
|
+
self.enter.add("on_enter_state", priority=CallbackPriority.GENERIC, suppress_errors=True)
|
|
122
|
+
self.enter.add(
|
|
123
|
+
f"on_enter_{self.id}", priority=CallbackPriority.NAMING, suppress_errors=True
|
|
124
|
+
)
|
|
125
|
+
self.exit.add("on_exit_state", priority=CallbackPriority.GENERIC, suppress_errors=True)
|
|
126
|
+
self.exit.add(f"on_exit_{self.id}", priority=CallbackPriority.NAMING, suppress_errors=True)
|
|
127
|
+
|
|
128
|
+
def _add_observer(self, register):
|
|
122
129
|
register(self.enter)
|
|
123
130
|
register(self.exit)
|
|
124
|
-
return self
|
|
125
131
|
|
|
126
|
-
def
|
|
127
|
-
self.enter
|
|
128
|
-
|
|
129
|
-
)
|
|
130
|
-
self.enter.add(f"on_enter_{self.id}", registry=registry, suppress_errors=True)
|
|
131
|
-
self.exit.add(
|
|
132
|
-
"on_exit_state", registry=registry, prepend=True, suppress_errors=True
|
|
133
|
-
)
|
|
134
|
-
self.exit.add(f"on_exit_{self.id}", registry=registry, suppress_errors=True)
|
|
132
|
+
def _check_callbacks(self, check):
|
|
133
|
+
check(self.enter)
|
|
134
|
+
check(self.exit)
|
|
135
135
|
|
|
136
136
|
def __repr__(self):
|
|
137
137
|
return (
|
|
@@ -139,6 +139,9 @@ class State:
|
|
|
139
139
|
f"initial={self.initial!r}, final={self.final!r})"
|
|
140
140
|
)
|
|
141
141
|
|
|
142
|
+
def __str__(self):
|
|
143
|
+
return self.name
|
|
144
|
+
|
|
142
145
|
def __get__(self, machine, owner):
|
|
143
146
|
if machine is None:
|
|
144
147
|
return self
|
|
@@ -146,14 +149,10 @@ class State:
|
|
|
146
149
|
|
|
147
150
|
def __set__(self, instance, value):
|
|
148
151
|
raise StateMachineError(
|
|
149
|
-
_("State overriding is not allowed. Trying to add '{}' to {}").format(
|
|
150
|
-
value, self.id
|
|
151
|
-
)
|
|
152
|
+
_("State overriding is not allowed. Trying to add '{}' to {}").format(value, self.id)
|
|
152
153
|
)
|
|
153
154
|
|
|
154
|
-
def for_instance(
|
|
155
|
-
self, machine: "StateMachine", cache: Dict["State", "State"]
|
|
156
|
-
) -> "State":
|
|
155
|
+
def for_instance(self, machine: "StateMachine", cache: Dict["State", "State"]) -> "State":
|
|
157
156
|
if self not in cache:
|
|
158
157
|
cache[self] = InstanceState(self, machine)
|
|
159
158
|
|
|
@@ -171,9 +170,7 @@ class State:
|
|
|
171
170
|
self.name = self._id.replace("_", " ").capitalize()
|
|
172
171
|
|
|
173
172
|
def _to_(self, *states: "State", **kwargs):
|
|
174
|
-
transitions = TransitionList(
|
|
175
|
-
Transition(self, state, **kwargs) for state in states
|
|
176
|
-
)
|
|
173
|
+
transitions = TransitionList(Transition(self, state, **kwargs) for state in states)
|
|
177
174
|
self.transitions.add_transitions(transitions)
|
|
178
175
|
return transitions
|
|
179
176
|
|
statemachine/statemachine.py
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
from collections import deque
|
|
2
|
+
from copy import deepcopy
|
|
2
3
|
from functools import partial
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
from typing import Any
|
|
5
6
|
from typing import Dict
|
|
6
7
|
|
|
8
|
+
from statemachine.graph import iterate_states_and_transitions
|
|
9
|
+
from statemachine.utils import run_async_from_sync
|
|
10
|
+
|
|
11
|
+
from .callbacks import CallbackMetaList
|
|
12
|
+
from .callbacks import CallbacksExecutor
|
|
7
13
|
from .callbacks import CallbacksRegistry
|
|
8
14
|
from .dispatcher import ObjectConfig
|
|
9
15
|
from .dispatcher import resolver_factory
|
|
@@ -68,27 +74,32 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
68
74
|
self.state_field = state_field
|
|
69
75
|
self.start_value = start_value
|
|
70
76
|
self.allow_event_without_transition = allow_event_without_transition
|
|
77
|
+
self.__initialized = False
|
|
71
78
|
self.__rtc = rtc
|
|
72
79
|
self.__processing: bool = False
|
|
73
80
|
self._external_queue: deque = deque()
|
|
74
81
|
self._callbacks_registry = CallbacksRegistry()
|
|
75
|
-
self._states_for_instance: Dict[
|
|
82
|
+
self._states_for_instance: Dict[State, State] = {}
|
|
83
|
+
self._observers: Dict[Any, Any] = {}
|
|
76
84
|
|
|
77
|
-
assert hasattr(self, "_abstract")
|
|
78
85
|
if self._abstract:
|
|
79
86
|
raise InvalidDefinition(_("There are no states or transitions."))
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
self._register_callbacks()
|
|
89
|
+
|
|
90
|
+
# Activate the initial state, this only works if the outer scope is sync code.
|
|
91
|
+
# for async code, the user should manually call `await sm.activate_initial_state()`
|
|
92
|
+
# after state machine creation.
|
|
93
|
+
run_async_from_sync(self.activate_initial_state())
|
|
94
|
+
|
|
95
|
+
def __init_subclass__(cls, strict_states: bool = False):
|
|
96
|
+
cls._strict_states = strict_states
|
|
97
|
+
super().__init_subclass__()
|
|
86
98
|
|
|
87
99
|
if TYPE_CHECKING:
|
|
88
100
|
"""Makes mypy happy with dynamic created attributes"""
|
|
89
101
|
|
|
90
|
-
def __getattr__(self, attribute: str) -> Any:
|
|
91
|
-
...
|
|
102
|
+
def __getattr__(self, attribute: str) -> Any: ...
|
|
92
103
|
|
|
93
104
|
def __repr__(self):
|
|
94
105
|
current_state_id = self.current_state.id if self.current_state_value else None
|
|
@@ -97,19 +108,47 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
97
108
|
f"current_state={current_state_id!r})"
|
|
98
109
|
)
|
|
99
110
|
|
|
111
|
+
def __deepcopy__(self, memo):
|
|
112
|
+
deepcopy_method = self.__deepcopy__
|
|
113
|
+
self.__deepcopy__ = None
|
|
114
|
+
try:
|
|
115
|
+
cp = deepcopy(self, memo)
|
|
116
|
+
finally:
|
|
117
|
+
self.__deepcopy__ = deepcopy_method
|
|
118
|
+
cp.__deepcopy__ = deepcopy_method
|
|
119
|
+
cp._callbacks_registry.clear()
|
|
120
|
+
cp._register_callbacks()
|
|
121
|
+
cp.add_observer(*cp._observers.keys())
|
|
122
|
+
return cp
|
|
123
|
+
|
|
100
124
|
def _get_initial_state(self):
|
|
101
|
-
current_state_value =
|
|
102
|
-
self.start_value if self.start_value else self.initial_state.value
|
|
103
|
-
)
|
|
125
|
+
current_state_value = self.start_value if self.start_value else self.initial_state.value
|
|
104
126
|
try:
|
|
105
127
|
return self.states_map[current_state_value]
|
|
106
128
|
except KeyError as err:
|
|
107
129
|
raise InvalidStateValue(current_state_value) from err
|
|
108
130
|
|
|
109
|
-
def
|
|
131
|
+
async def activate_initial_state(self):
|
|
132
|
+
"""
|
|
133
|
+
Activate the initial state.
|
|
134
|
+
|
|
135
|
+
Called automatically on state machine creation from sync code, but in
|
|
136
|
+
async code, the user must call this method explicitly.
|
|
137
|
+
|
|
138
|
+
Given how async works on python, there's no built-in way to activate the initial state that
|
|
139
|
+
may depend on async code from the StateMachine.__init__ method.
|
|
140
|
+
|
|
141
|
+
We do a `_ensure_is_initialized()` check before each event, but to check the current state
|
|
142
|
+
just before the state machine is created, the user must await the activation of the initial
|
|
143
|
+
state explicitly.
|
|
144
|
+
"""
|
|
145
|
+
if self.__initialized:
|
|
146
|
+
return
|
|
147
|
+
self.__initialized = True
|
|
110
148
|
if self.current_state_value is None:
|
|
111
149
|
# send an one-time event `__initial__` to enter the current state.
|
|
112
150
|
# current_state = self.current_state
|
|
151
|
+
initial_transition = Transition(None, self._get_initial_state(), event="__initial__")
|
|
113
152
|
initial_transition.before.clear()
|
|
114
153
|
initial_transition.on.clear()
|
|
115
154
|
initial_transition.after.clear()
|
|
@@ -121,65 +160,34 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
121
160
|
),
|
|
122
161
|
transition=initial_transition,
|
|
123
162
|
)
|
|
124
|
-
self._activate(event_data)
|
|
125
|
-
|
|
126
|
-
def _get_protected_attrs(self):
|
|
127
|
-
return {
|
|
128
|
-
"_abstract",
|
|
129
|
-
"model",
|
|
130
|
-
"state_field",
|
|
131
|
-
"start_value",
|
|
132
|
-
"initial_state",
|
|
133
|
-
"final_states",
|
|
134
|
-
"states",
|
|
135
|
-
"_events",
|
|
136
|
-
"states_map",
|
|
137
|
-
"send",
|
|
138
|
-
} | {s.id for s in self.states}
|
|
139
|
-
|
|
140
|
-
def _visit_states_and_transitions(self, visitor):
|
|
141
|
-
for state in self.states:
|
|
142
|
-
visitor(state)
|
|
143
|
-
for transition in state.transitions:
|
|
144
|
-
visitor(transition)
|
|
145
|
-
|
|
146
|
-
def _setup(self, initial_transition: Transition):
|
|
147
|
-
"""
|
|
148
|
-
Args:
|
|
149
|
-
initial_transition: A special :ref:`transition` that triggers the enter on the
|
|
150
|
-
`initial` :ref:`State`.
|
|
151
|
-
"""
|
|
152
|
-
machine = ObjectConfig.from_obj(self, skip_attrs=self._get_protected_attrs())
|
|
153
|
-
model = ObjectConfig.from_obj(self.model, skip_attrs={self.state_field})
|
|
154
|
-
default_resolver = resolver_factory(machine, model)
|
|
163
|
+
await self._activate(event_data)
|
|
155
164
|
|
|
156
|
-
|
|
165
|
+
async def _ensure_is_initialized(self):
|
|
166
|
+
await self.activate_initial_state()
|
|
157
167
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def setup_visitor(visited):
|
|
161
|
-
visited._setup(register)
|
|
162
|
-
observer_visitor(visited)
|
|
163
|
-
|
|
164
|
-
self._visit_states_and_transitions(setup_visitor)
|
|
165
|
-
|
|
166
|
-
initial_transition._setup(register)
|
|
167
|
-
|
|
168
|
-
def _build_observers_visitor(self, *observers):
|
|
169
|
-
registry_callbacks = [
|
|
168
|
+
def _register_callbacks(self):
|
|
169
|
+
self._add_observer(
|
|
170
170
|
(
|
|
171
|
-
self.
|
|
172
|
-
|
|
173
|
-
)
|
|
171
|
+
ObjectConfig.from_obj(self, skip_attrs=self._protected_attrs),
|
|
172
|
+
ObjectConfig.from_obj(self.model, skip_attrs={self.state_field}),
|
|
174
173
|
)
|
|
175
|
-
|
|
176
|
-
]
|
|
174
|
+
)
|
|
177
175
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
check_callbacks = self._callbacks_registry.check
|
|
177
|
+
for visited in iterate_states_and_transitions(self.states):
|
|
178
|
+
try:
|
|
179
|
+
visited._check_callbacks(check_callbacks)
|
|
180
|
+
except Exception as err:
|
|
181
|
+
raise InvalidDefinition(
|
|
182
|
+
f"Error on {visited!s} when resolving callbacks: {err}"
|
|
183
|
+
) from err
|
|
181
184
|
|
|
182
|
-
|
|
185
|
+
def _add_observer(self, observers):
|
|
186
|
+
register = partial(self._callbacks_registry.register, resolver=resolver_factory(observers))
|
|
187
|
+
for visited in iterate_states_and_transitions(self.states):
|
|
188
|
+
visited._add_observer(register)
|
|
189
|
+
|
|
190
|
+
return self
|
|
183
191
|
|
|
184
192
|
def add_observer(self, *observers):
|
|
185
193
|
"""Add an observer.
|
|
@@ -191,10 +199,8 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
191
199
|
|
|
192
200
|
:ref:`observers`.
|
|
193
201
|
"""
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
self._visit_states_and_transitions(visitor)
|
|
197
|
-
return self
|
|
202
|
+
self._observers.update({o: None for o in observers})
|
|
203
|
+
return self._add_observer(tuple(ObjectConfig.from_obj(o) for o in observers))
|
|
198
204
|
|
|
199
205
|
def _repr_html_(self):
|
|
200
206
|
return f'<div class="statemachine">{self._repr_svg_()}</div>'
|
|
@@ -214,8 +220,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
214
220
|
This is a low level API, that can be used to assign any valid state value
|
|
215
221
|
completely bypassing all the hooks and validations.
|
|
216
222
|
"""
|
|
217
|
-
|
|
218
|
-
return value
|
|
223
|
+
return getattr(self.model, self.state_field, None)
|
|
219
224
|
|
|
220
225
|
@current_state_value.setter
|
|
221
226
|
def current_state_value(self, value):
|
|
@@ -231,11 +236,23 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
231
236
|
completely bypassing all the hooks and validations.
|
|
232
237
|
"""
|
|
233
238
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
try:
|
|
240
|
+
state: State = self.states_map[self.current_state_value].for_instance(
|
|
241
|
+
machine=self,
|
|
242
|
+
cache=self._states_for_instance,
|
|
243
|
+
)
|
|
244
|
+
return state
|
|
245
|
+
except KeyError as err:
|
|
246
|
+
if self.current_state_value is None:
|
|
247
|
+
raise InvalidStateValue(
|
|
248
|
+
self.current_state_value,
|
|
249
|
+
_(
|
|
250
|
+
"There's no current state set. In async code, "
|
|
251
|
+
"did you activate the initial state? "
|
|
252
|
+
"(e.g., `await sm.activate_initial_state()`)"
|
|
253
|
+
),
|
|
254
|
+
) from err
|
|
255
|
+
raise InvalidStateValue(self.current_state_value) from err
|
|
239
256
|
|
|
240
257
|
@current_state.setter
|
|
241
258
|
def current_state(self, value):
|
|
@@ -248,12 +265,9 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
248
265
|
@property
|
|
249
266
|
def allowed_events(self):
|
|
250
267
|
"""List of the current allowed events."""
|
|
251
|
-
return [
|
|
252
|
-
getattr(self, event)
|
|
253
|
-
for event in self.current_state.transitions.unique_events
|
|
254
|
-
]
|
|
268
|
+
return [getattr(self, event) for event in self.current_state.transitions.unique_events]
|
|
255
269
|
|
|
256
|
-
def _process(self, trigger):
|
|
270
|
+
async def _process(self, trigger):
|
|
257
271
|
"""Process event triggers.
|
|
258
272
|
|
|
259
273
|
The simplest implementation is the non-RTC (synchronous),
|
|
@@ -274,7 +288,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
274
288
|
"""
|
|
275
289
|
if not self.__rtc:
|
|
276
290
|
# The machine is in "synchronous" mode
|
|
277
|
-
return trigger()
|
|
291
|
+
return await trigger()
|
|
278
292
|
|
|
279
293
|
# The machine is in "queued" mode
|
|
280
294
|
# Add the trigger to queue and start processing in a loop.
|
|
@@ -285,9 +299,9 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
285
299
|
if self.__processing:
|
|
286
300
|
return
|
|
287
301
|
|
|
288
|
-
return self._processing_loop()
|
|
302
|
+
return await self._processing_loop()
|
|
289
303
|
|
|
290
|
-
def _processing_loop(self):
|
|
304
|
+
async def _processing_loop(self):
|
|
291
305
|
"""Execute the triggers in the queue in order until the queue is empty"""
|
|
292
306
|
self.__processing = True
|
|
293
307
|
|
|
@@ -300,7 +314,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
300
314
|
while self._external_queue:
|
|
301
315
|
trigger = self._external_queue.popleft()
|
|
302
316
|
try:
|
|
303
|
-
result = trigger()
|
|
317
|
+
result = await trigger()
|
|
304
318
|
if first_result is sentinel:
|
|
305
319
|
first_result = result
|
|
306
320
|
except Exception:
|
|
@@ -312,20 +326,18 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
312
326
|
self.__processing = False
|
|
313
327
|
return first_result if first_result is not sentinel else None
|
|
314
328
|
|
|
315
|
-
def _activate(self, event_data: EventData):
|
|
329
|
+
async def _activate(self, event_data: EventData):
|
|
316
330
|
transition = event_data.transition
|
|
317
331
|
source = event_data.state
|
|
318
332
|
target = transition.target
|
|
319
333
|
|
|
320
|
-
result = self.
|
|
334
|
+
result = await self._callbacks(transition.before).call(
|
|
321
335
|
*event_data.args, **event_data.extended_kwargs
|
|
322
336
|
)
|
|
323
337
|
if source is not None and not transition.internal:
|
|
324
|
-
self.
|
|
325
|
-
*event_data.args, **event_data.extended_kwargs
|
|
326
|
-
)
|
|
338
|
+
await self._callbacks(source.exit).call(*event_data.args, **event_data.extended_kwargs)
|
|
327
339
|
|
|
328
|
-
result += self.
|
|
340
|
+
result += await self._callbacks(transition.on).call(
|
|
329
341
|
*event_data.args, **event_data.extended_kwargs
|
|
330
342
|
)
|
|
331
343
|
|
|
@@ -333,10 +345,10 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
333
345
|
event_data.state = target
|
|
334
346
|
|
|
335
347
|
if not transition.internal:
|
|
336
|
-
self.
|
|
348
|
+
await self._callbacks(target.enter).call(
|
|
337
349
|
*event_data.args, **event_data.extended_kwargs
|
|
338
350
|
)
|
|
339
|
-
self.
|
|
351
|
+
await self._callbacks(transition.after).call(
|
|
340
352
|
*event_data.args, **event_data.extended_kwargs
|
|
341
353
|
)
|
|
342
354
|
|
|
@@ -347,7 +359,20 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
347
359
|
|
|
348
360
|
return result
|
|
349
361
|
|
|
350
|
-
def send(self, event, *args, **kwargs):
|
|
362
|
+
def send(self, event: str, *args, **kwargs):
|
|
363
|
+
"""Send an :ref:`Event` to the state machine.
|
|
364
|
+
|
|
365
|
+
This is a thin wrapper around :meth:`async_send` to allow synchronous
|
|
366
|
+
code to send events.
|
|
367
|
+
|
|
368
|
+
.. seealso::
|
|
369
|
+
|
|
370
|
+
See: :ref:`triggering events`.
|
|
371
|
+
|
|
372
|
+
"""
|
|
373
|
+
return run_async_from_sync(self.async_send(event, *args, **kwargs))
|
|
374
|
+
|
|
375
|
+
async def async_send(self, event: str, *args, **kwargs):
|
|
351
376
|
"""Send an :ref:`Event` to the state machine.
|
|
352
377
|
|
|
353
378
|
.. seealso::
|
|
@@ -355,5 +380,8 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
355
380
|
See: :ref:`triggering events`.
|
|
356
381
|
|
|
357
382
|
"""
|
|
358
|
-
|
|
359
|
-
return
|
|
383
|
+
event_instance: Event = Event(event)
|
|
384
|
+
return await event_instance.trigger(self, *args, **kwargs)
|
|
385
|
+
|
|
386
|
+
def _callbacks(self, meta_list: CallbackMetaList) -> CallbacksExecutor:
|
|
387
|
+
return self._callbacks_registry[meta_list]
|
statemachine/states.py
CHANGED
|
@@ -15,21 +15,23 @@ class States:
|
|
|
15
15
|
Helps creating :ref:`StateMachine`'s :ref:`state` definitions from other
|
|
16
16
|
sources, like an ``Enum`` class, using :meth:`States.from_enum`.
|
|
17
17
|
|
|
18
|
+
>>> states_def = [('open', {'initial': True}), ('closed', {'final': True})]
|
|
19
|
+
|
|
18
20
|
>>> from statemachine import StateMachine
|
|
19
21
|
>>> class SM(StateMachine):
|
|
20
22
|
...
|
|
21
23
|
... states = States({
|
|
22
|
-
... name: State(
|
|
24
|
+
... name: State(**params) for name, params in states_def
|
|
23
25
|
... })
|
|
24
26
|
...
|
|
25
|
-
...
|
|
27
|
+
... close = states.open.to(states.closed)
|
|
26
28
|
|
|
27
29
|
And states can be used as usual.
|
|
28
30
|
|
|
29
31
|
>>> sm = SM()
|
|
30
|
-
>>> sm.send("
|
|
32
|
+
>>> sm.send("close")
|
|
31
33
|
>>> sm.current_state.id
|
|
32
|
-
'
|
|
34
|
+
'closed'
|
|
33
35
|
|
|
34
36
|
"""
|
|
35
37
|
|