python-statemachine 2.1.2__py3-none-any.whl → 2.2.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.2.0.dist-info}/METADATA +4 -4
- python_statemachine-2.2.0.dist-info/RECORD +28 -0
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +98 -73
- statemachine/contrib/diagram.py +20 -6
- statemachine/dispatcher.py +63 -31
- statemachine/factory.py +71 -7
- statemachine/graph.py +6 -0
- statemachine/mixins.py +1 -3
- statemachine/signature.py +2 -3
- statemachine/state.py +22 -25
- statemachine/statemachine.py +59 -84
- statemachine/states.py +6 -4
- statemachine/transition.py +31 -22
- statemachine/utils.py +4 -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.2.0.dist-info}/LICENSE +0 -0
- {python_statemachine-2.1.2.dist-info → python_statemachine-2.2.0.dist-info}/WHEEL +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-statemachine
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Python Finite State Machines made easy.
|
|
5
5
|
Home-page: https://github.com/fgmacedo/python-statemachine
|
|
6
6
|
License: MIT
|
|
@@ -8,17 +8,17 @@ Author: Fernando Macedo
|
|
|
8
8
|
Author-email: fgmacedo@gmail.com
|
|
9
9
|
Maintainer: Fernando Macedo
|
|
10
10
|
Maintainer-email: fgmacedo@gmail.com
|
|
11
|
-
Requires-Python: >=3.
|
|
11
|
+
Requires-Python: >=3.9,<3.13
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
14
|
Classifier: Natural Language :: English
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
18
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
19
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
22
22
|
Classifier: Topic :: Software Development :: Libraries
|
|
23
23
|
Provides-Extra: diagrams
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
statemachine/__init__.py,sha256=nhqSQwRfWBVrcn4h-aLlxLbzeSsjt6lE_SSFnFWK0FY,192
|
|
2
|
+
statemachine/callbacks.py,sha256=QMh564YuNVNV74VxlIyvlF_eW6njsmV9VHauQH9JR1U,8704
|
|
3
|
+
statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
statemachine/contrib/diagram.py,sha256=CTlNRtAHEzCCzRUlxkiyzJOCOp1CohihdD0_7pHrx4c,7004
|
|
5
|
+
statemachine/dispatcher.py,sha256=i9ChhdomQpVbTg9vNS3xoCHUAw3Z32B0UPzK47nb1-A,4924
|
|
6
|
+
statemachine/event.py,sha256=fvWxyFcrbPVxNW94p66rGJ5I1UZUK9mDJz68udkz2JI,2106
|
|
7
|
+
statemachine/event_data.py,sha256=nxOfypngK-78A77gj4pS-oxLx9zl1S9wnSt_rTiCyZA,2257
|
|
8
|
+
statemachine/events.py,sha256=rUbiTX-QGoo7zzmQMEeWeLrIVnp9zhicdRIm34CoAVI,731
|
|
9
|
+
statemachine/exceptions.py,sha256=uzvDbHkzCZkmFI_02L0yC9gd6d0SBn9GmsI8ekc0Hjk,952
|
|
10
|
+
statemachine/factory.py,sha256=m2TeJ19eDIymoGADDp0G0ItrrcCRgIJoJYFJ5lzqmWE,8015
|
|
11
|
+
statemachine/graph.py,sha256=KtwB1CYckaLjTgQD9tEeuaEzJje9q3fPVpBViW5TgSk,487
|
|
12
|
+
statemachine/i18n.py,sha256=NLvGseaORmQ0G-V_J8tkjoxh_piWMOm2CI6mBQpLamc,362
|
|
13
|
+
statemachine/locale/en/LC_MESSAGES/statemachine.po,sha256=MfODCwzML6DzhOpHbno2Pdhsb26AWboR5Ka1JCNvJIA,1685
|
|
14
|
+
statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=KsR9rlnhw17EZG-IbVx3F0wJAJSK0B-OByGmUmDHDUM,2375
|
|
15
|
+
statemachine/mixins.py,sha256=B8WB3EGyZpMWAQg4Nw3kUMCfswgmLCChDpOQdxR28eE,1017
|
|
16
|
+
statemachine/model.py,sha256=OylI3FjMiHpYyDl9mtK1zEJMeSvemaN4giQDonpc8kI,211
|
|
17
|
+
statemachine/registry.py,sha256=RnVBRS3I_6Tm2OMgXMB_ewX7zQaslqEfhXFOhbqIkG4,959
|
|
18
|
+
statemachine/signature.py,sha256=9KItptchQlP2VpNmBiIFXLNAWNWY3Bs_ymUd7OOgDD4,7507
|
|
19
|
+
statemachine/state.py,sha256=bZki0DyemZa-aVAfollQCmpXxesWONzUmE28eDfqz3o,8315
|
|
20
|
+
statemachine/statemachine.py,sha256=U0rLIUhvsCivth1uNb0pt2xWVUnDbzhXF982yJH-bg0,11659
|
|
21
|
+
statemachine/states.py,sha256=gdGemJYF9k-cifs9Tk0Pe_-1u1Lanf3l08o0t8wAMgg,4203
|
|
22
|
+
statemachine/transition.py,sha256=ebRhJsjOoLya7oYZVjRAKvjq1EsC9ImpExgHANekVMA,5388
|
|
23
|
+
statemachine/transition_list.py,sha256=DatsmMWgK0YK30Nrj-josVvlTgeGapKutzYur9-puF8,5949
|
|
24
|
+
statemachine/utils.py,sha256=fV-Hz1gnyl4tAs9nPr2u9I9zfxq62mokPV8mUpIJc1k,458
|
|
25
|
+
python_statemachine-2.2.0.dist-info/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
|
|
26
|
+
python_statemachine-2.2.0.dist-info/METADATA,sha256=qH4SxonFNJStkKVocZABu-RLiYD54DcON2gNGjBPGGQ,11454
|
|
27
|
+
python_statemachine-2.2.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
|
28
|
+
python_statemachine-2.2.0.dist-info/RECORD,,
|
statemachine/__init__.py
CHANGED
statemachine/callbacks.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
from bisect import insort
|
|
1
2
|
from collections import defaultdict
|
|
2
3
|
from collections import deque
|
|
4
|
+
from enum import IntEnum
|
|
3
5
|
from typing import Callable
|
|
4
6
|
from typing import Dict
|
|
7
|
+
from typing import Generator
|
|
5
8
|
from typing import List
|
|
6
9
|
|
|
7
10
|
from .exceptions import AttrNotFound
|
|
@@ -9,27 +12,42 @@ from .i18n import _
|
|
|
9
12
|
from .utils import ensure_iterable
|
|
10
13
|
|
|
11
14
|
|
|
15
|
+
class CallbackPriority(IntEnum):
|
|
16
|
+
GENERIC = 0
|
|
17
|
+
INLINE = 10
|
|
18
|
+
DECORATOR = 20
|
|
19
|
+
NAMING = 30
|
|
20
|
+
AFTER = 40
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def allways_true(*args, **kwargs):
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
|
|
12
27
|
class CallbackWrapper:
|
|
13
28
|
def __init__(
|
|
14
29
|
self,
|
|
15
30
|
callback: Callable,
|
|
16
31
|
condition: Callable,
|
|
32
|
+
meta: "CallbackMeta",
|
|
17
33
|
unique_key: str,
|
|
18
|
-
expected_value: "bool | None" = None,
|
|
19
34
|
) -> None:
|
|
20
35
|
self._callback = callback
|
|
21
36
|
self.condition = condition
|
|
37
|
+
self.meta = meta
|
|
22
38
|
self.unique_key = unique_key
|
|
23
|
-
self.expected_value = expected_value
|
|
24
39
|
|
|
25
40
|
def __repr__(self):
|
|
26
41
|
return f"{type(self).__name__}({self.unique_key})"
|
|
27
42
|
|
|
43
|
+
def __str__(self):
|
|
44
|
+
return str(self.meta)
|
|
45
|
+
|
|
46
|
+
def __lt__(self, other):
|
|
47
|
+
return self.meta.priority < other.meta.priority
|
|
48
|
+
|
|
28
49
|
def __call__(self, *args, **kwargs):
|
|
29
|
-
|
|
30
|
-
if self.expected_value is not None:
|
|
31
|
-
return bool(result) == self.expected_value
|
|
32
|
-
return result
|
|
50
|
+
return self._callback(*args, **kwargs)
|
|
33
51
|
|
|
34
52
|
|
|
35
53
|
class CallbackMeta:
|
|
@@ -42,14 +60,22 @@ class CallbackMeta:
|
|
|
42
60
|
call is performed, to allow the proper callback resolution.
|
|
43
61
|
"""
|
|
44
62
|
|
|
45
|
-
def __init__(
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
func,
|
|
66
|
+
suppress_errors=False,
|
|
67
|
+
cond=None,
|
|
68
|
+
priority: CallbackPriority = CallbackPriority.NAMING,
|
|
69
|
+
expected_value=None,
|
|
70
|
+
):
|
|
46
71
|
self.func = func
|
|
47
72
|
self.suppress_errors = suppress_errors
|
|
48
|
-
self.cond =
|
|
73
|
+
self.cond = cond
|
|
49
74
|
self.expected_value = expected_value
|
|
75
|
+
self.priority = priority
|
|
50
76
|
|
|
51
77
|
def __repr__(self):
|
|
52
|
-
return f"{type(self).__name__}({self.func!r})"
|
|
78
|
+
return f"{type(self).__name__}({self.func!r}, suppress_errors={self.suppress_errors!r})"
|
|
53
79
|
|
|
54
80
|
def __str__(self):
|
|
55
81
|
return getattr(self.func, "__name__", self.func)
|
|
@@ -63,7 +89,10 @@ class CallbackMeta:
|
|
|
63
89
|
def _update_func(self, func):
|
|
64
90
|
self.func = func
|
|
65
91
|
|
|
66
|
-
def
|
|
92
|
+
def _wrap_callable(self, func, _expected_value):
|
|
93
|
+
return func
|
|
94
|
+
|
|
95
|
+
def build(self, resolver) -> Generator["CallbackWrapper", None, None]:
|
|
67
96
|
"""
|
|
68
97
|
Resolves the `func` into a usable callable.
|
|
69
98
|
|
|
@@ -71,25 +100,14 @@ class CallbackMeta:
|
|
|
71
100
|
resolver (callable): A method responsible to build and return a valid callable that
|
|
72
101
|
can receive arbitrary parameters like `*args, **kwargs`.
|
|
73
102
|
"""
|
|
74
|
-
callback
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
callback=callback,
|
|
81
|
-
condition=conditions.all,
|
|
103
|
+
for callback in resolver(self.func):
|
|
104
|
+
condition = next(resolver(self.cond)) if self.cond is not None else allways_true
|
|
105
|
+
yield CallbackWrapper(
|
|
106
|
+
callback=self._wrap_callable(callback, self.expected_value),
|
|
107
|
+
condition=condition,
|
|
108
|
+
meta=self,
|
|
82
109
|
unique_key=callback.unique_key,
|
|
83
|
-
expected_value=self.expected_value,
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
if not self.suppress_errors:
|
|
87
|
-
raise AttrNotFound(
|
|
88
|
-
_("Did not found name '{}' from model or statemachine").format(
|
|
89
|
-
self.func
|
|
90
|
-
)
|
|
91
110
|
)
|
|
92
|
-
return None
|
|
93
111
|
|
|
94
112
|
|
|
95
113
|
class BoolCallbackMeta(CallbackMeta):
|
|
@@ -102,18 +120,32 @@ class BoolCallbackMeta(CallbackMeta):
|
|
|
102
120
|
call is performed, to allow the proper callback resolution.
|
|
103
121
|
"""
|
|
104
122
|
|
|
105
|
-
def __init__(
|
|
106
|
-
self
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
func,
|
|
126
|
+
suppress_errors=False,
|
|
127
|
+
cond=None,
|
|
128
|
+
priority: CallbackPriority = CallbackPriority.NAMING,
|
|
129
|
+
expected_value=True,
|
|
130
|
+
):
|
|
131
|
+
super().__init__(
|
|
132
|
+
func, suppress_errors, cond, priority=priority, expected_value=expected_value
|
|
133
|
+
)
|
|
110
134
|
|
|
111
135
|
def __str__(self):
|
|
112
136
|
name = super().__str__()
|
|
113
137
|
return name if self.expected_value else f"!{name}"
|
|
114
138
|
|
|
139
|
+
def _wrap_callable(self, func, expected_value):
|
|
140
|
+
def bool_wrapper(*args, **kwargs):
|
|
141
|
+
return bool(func(*args, **kwargs)) == expected_value
|
|
142
|
+
|
|
143
|
+
return bool_wrapper
|
|
144
|
+
|
|
115
145
|
|
|
116
146
|
class CallbackMetaList:
|
|
147
|
+
"""List of `CallbackMeta` instances"""
|
|
148
|
+
|
|
117
149
|
def __init__(self, factory=CallbackMeta):
|
|
118
150
|
self.items: List[CallbackMeta] = []
|
|
119
151
|
self.factory = factory
|
|
@@ -157,6 +189,7 @@ class CallbackMetaList:
|
|
|
157
189
|
return func
|
|
158
190
|
|
|
159
191
|
def __call__(self, callback):
|
|
192
|
+
"""Allows usage of the callback list as a decorator."""
|
|
160
193
|
return self._add_unbounded_callback(callback)
|
|
161
194
|
|
|
162
195
|
def __iter__(self):
|
|
@@ -165,18 +198,13 @@ class CallbackMetaList:
|
|
|
165
198
|
def clear(self):
|
|
166
199
|
self.items = []
|
|
167
200
|
|
|
168
|
-
def _add(self, func,
|
|
201
|
+
def _add(self, func, **kwargs):
|
|
169
202
|
meta = self.factory(func, **kwargs)
|
|
170
|
-
if registry is not None and not registry(self, meta, prepend=prepend):
|
|
171
|
-
return
|
|
172
203
|
|
|
173
204
|
if meta in self.items:
|
|
174
205
|
return
|
|
175
206
|
|
|
176
|
-
|
|
177
|
-
self.items.insert(0, meta)
|
|
178
|
-
else:
|
|
179
|
-
self.items.append(meta)
|
|
207
|
+
self.items.append(meta)
|
|
180
208
|
return meta
|
|
181
209
|
|
|
182
210
|
def add(self, callbacks, **kwargs):
|
|
@@ -191,6 +219,8 @@ class CallbackMetaList:
|
|
|
191
219
|
|
|
192
220
|
|
|
193
221
|
class CallbacksExecutor:
|
|
222
|
+
"""A list of callbacks that can be executed in order."""
|
|
223
|
+
|
|
194
224
|
def __init__(self):
|
|
195
225
|
self.items: List[CallbackWrapper] = deque()
|
|
196
226
|
self.items_already_seen = set()
|
|
@@ -201,34 +231,26 @@ class CallbacksExecutor:
|
|
|
201
231
|
def __repr__(self):
|
|
202
232
|
return f"{type(self).__name__}({self.items!r})"
|
|
203
233
|
|
|
204
|
-
def
|
|
205
|
-
|
|
206
|
-
) -> "CallbackWrapper | None":
|
|
207
|
-
callback = callback_info.build(resolver)
|
|
208
|
-
if callback is None:
|
|
209
|
-
return None
|
|
234
|
+
def __str__(self):
|
|
235
|
+
return ", ".join(str(c) for c in self)
|
|
210
236
|
|
|
211
|
-
|
|
212
|
-
|
|
237
|
+
def _add(self, callback_meta: CallbackMeta, resolver: Callable):
|
|
238
|
+
for callback in callback_meta.build(resolver):
|
|
239
|
+
if callback.unique_key in self.items_already_seen:
|
|
240
|
+
continue
|
|
213
241
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
self.items.insert(0, callback)
|
|
217
|
-
else:
|
|
218
|
-
self.items.append(callback)
|
|
219
|
-
return callback
|
|
242
|
+
self.items_already_seen.add(callback.unique_key)
|
|
243
|
+
insort(self.items, callback)
|
|
220
244
|
|
|
221
245
|
def add(self, items: CallbackMetaList, resolver: Callable):
|
|
222
246
|
"""Validate configurations"""
|
|
223
247
|
for item in items:
|
|
224
|
-
self.
|
|
248
|
+
self._add(item, resolver)
|
|
225
249
|
return self
|
|
226
250
|
|
|
227
251
|
def call(self, *args, **kwargs):
|
|
228
252
|
return [
|
|
229
|
-
callback(*args, **kwargs)
|
|
230
|
-
for callback in self
|
|
231
|
-
if callback.condition(*args, **kwargs)
|
|
253
|
+
callback(*args, **kwargs) for callback in self if callback.condition(*args, **kwargs)
|
|
232
254
|
]
|
|
233
255
|
|
|
234
256
|
def all(self, *args, **kwargs):
|
|
@@ -237,24 +259,27 @@ class CallbacksExecutor:
|
|
|
237
259
|
|
|
238
260
|
class CallbacksRegistry:
|
|
239
261
|
def __init__(self) -> None:
|
|
240
|
-
self._registry: Dict[CallbackMetaList, CallbacksExecutor] = defaultdict(
|
|
241
|
-
CallbacksExecutor
|
|
242
|
-
)
|
|
262
|
+
self._registry: Dict[CallbackMetaList, CallbacksExecutor] = defaultdict(CallbacksExecutor)
|
|
243
263
|
|
|
244
|
-
def register(self,
|
|
245
|
-
executor_list = self[
|
|
246
|
-
executor_list.add(
|
|
264
|
+
def register(self, meta_list: CallbackMetaList, resolver):
|
|
265
|
+
executor_list = self[meta_list]
|
|
266
|
+
executor_list.add(meta_list, resolver)
|
|
247
267
|
return executor_list
|
|
248
268
|
|
|
249
|
-
def
|
|
250
|
-
|
|
269
|
+
def clear(self):
|
|
270
|
+
self._registry.clear()
|
|
271
|
+
|
|
272
|
+
def __getitem__(self, meta_list: CallbackMetaList) -> CallbacksExecutor:
|
|
273
|
+
return self._registry[meta_list]
|
|
251
274
|
|
|
252
|
-
def
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
meta:
|
|
256
|
-
|
|
257
|
-
):
|
|
258
|
-
return self[meta_list].add_one(meta, resolver, prepend=prepend)
|
|
275
|
+
def check(self, meta_list: CallbackMetaList):
|
|
276
|
+
executor = self[meta_list]
|
|
277
|
+
for meta in meta_list:
|
|
278
|
+
if meta.suppress_errors:
|
|
279
|
+
continue
|
|
259
280
|
|
|
260
|
-
|
|
281
|
+
if any(callback for callback in executor if callback.meta == meta):
|
|
282
|
+
continue
|
|
283
|
+
raise AttrNotFound(
|
|
284
|
+
_("Did not found name '{}' from model or statemachine").format(meta.func)
|
|
285
|
+
)
|
statemachine/contrib/diagram.py
CHANGED
|
@@ -66,21 +66,35 @@ class DotGraphMachine:
|
|
|
66
66
|
fontsize=self.transition_font_size,
|
|
67
67
|
)
|
|
68
68
|
|
|
69
|
+
def _actions_getter(self):
|
|
70
|
+
if isinstance(self.machine, StateMachine):
|
|
71
|
+
|
|
72
|
+
def getter(x):
|
|
73
|
+
return self.machine._callbacks(x)
|
|
74
|
+
else:
|
|
75
|
+
|
|
76
|
+
def getter(x):
|
|
77
|
+
return x
|
|
78
|
+
|
|
79
|
+
return getter
|
|
80
|
+
|
|
69
81
|
def _state_actions(self, state):
|
|
70
|
-
|
|
71
|
-
|
|
82
|
+
getter = self._actions_getter()
|
|
83
|
+
|
|
84
|
+
entry = str(getter(state.enter))
|
|
85
|
+
exit_ = str(getter(state.exit))
|
|
72
86
|
internal = ", ".join(
|
|
73
|
-
f"{transition.event} / {transition.on
|
|
87
|
+
f"{transition.event} / {str(getter(transition.on))}"
|
|
74
88
|
for transition in state.transitions
|
|
75
89
|
if transition.internal
|
|
76
90
|
)
|
|
77
91
|
|
|
78
92
|
if entry:
|
|
79
93
|
entry = f"entry / {entry}"
|
|
80
|
-
if
|
|
81
|
-
|
|
94
|
+
if exit_:
|
|
95
|
+
exit_ = f"exit / {exit_}"
|
|
82
96
|
|
|
83
|
-
actions = "\n".join(x for x in [entry,
|
|
97
|
+
actions = "\n".join(x for x in [entry, exit_, internal] if x)
|
|
84
98
|
|
|
85
99
|
if actions:
|
|
86
100
|
actions = f"\n{actions}"
|
statemachine/dispatcher.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from collections import namedtuple
|
|
2
2
|
from operator import attrgetter
|
|
3
3
|
from typing import Any
|
|
4
|
+
from typing import Generator
|
|
4
5
|
|
|
5
6
|
from .signature import SignatureAdapter
|
|
6
7
|
|
|
@@ -18,17 +19,10 @@ class ObjectConfig(namedtuple("ObjectConfig", "obj skip_attrs resolver_id")):
|
|
|
18
19
|
if isinstance(obj, ObjectConfig):
|
|
19
20
|
return obj
|
|
20
21
|
else:
|
|
21
|
-
return cls(obj,
|
|
22
|
-
|
|
23
|
-
def getattr(self, attr):
|
|
24
|
-
if attr in self.skip_attrs:
|
|
25
|
-
return
|
|
26
|
-
return getattr(self.obj, attr, None)
|
|
22
|
+
return cls(obj, skip_attrs or set(), str(id(obj)))
|
|
27
23
|
|
|
28
24
|
|
|
29
25
|
class WrapSearchResult:
|
|
30
|
-
is_empty = False
|
|
31
|
-
|
|
32
26
|
def __init__(self, attribute, resolver_id) -> None:
|
|
33
27
|
self.attribute = attribute
|
|
34
28
|
self.resolver_id = resolver_id
|
|
@@ -48,10 +42,6 @@ class WrapSearchResult:
|
|
|
48
42
|
return self._cache(*args, **kwds)
|
|
49
43
|
|
|
50
44
|
|
|
51
|
-
class EmptyWrapSearchResult(WrapSearchResult):
|
|
52
|
-
is_empty = True
|
|
53
|
-
|
|
54
|
-
|
|
55
45
|
class CallableSearchResult(WrapSearchResult):
|
|
56
46
|
def __init__(self, attribute, a_callable, resolver_id) -> None:
|
|
57
47
|
self.a_callable = a_callable
|
|
@@ -93,32 +83,74 @@ class EventSearchResult(WrapSearchResult):
|
|
|
93
83
|
return wrapper
|
|
94
84
|
|
|
95
85
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
86
|
+
def _search_callable_attr_is_property(
|
|
87
|
+
attr, configs: tuple[ObjectConfig]
|
|
88
|
+
) -> "WrapSearchResult | None":
|
|
89
|
+
# if the attr is a property, we'll try to find the object that has the
|
|
90
|
+
# property on the configs
|
|
91
|
+
attr_name = attr.fget.__name__
|
|
92
|
+
for obj, _skip_attrs, resolver_id in configs:
|
|
93
|
+
func = getattr(type(obj), attr_name, None)
|
|
94
|
+
if func is not None and func is attr:
|
|
95
|
+
return AttributeCallableSearchResult(attr_name, obj, resolver_id)
|
|
96
|
+
return None
|
|
99
97
|
|
|
100
|
-
for config in configs:
|
|
101
|
-
func = config.getattr(attr)
|
|
102
|
-
if func is not None:
|
|
103
|
-
if not callable(func):
|
|
104
|
-
return AttributeCallableSearchResult(
|
|
105
|
-
attr, config.obj, config.resolver_id
|
|
106
|
-
)
|
|
107
98
|
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
def _search_callable_attr_is_callable(attr, configs: tuple[ObjectConfig]) -> WrapSearchResult:
|
|
100
|
+
# if the attr is an unbounded method, we'll try to find the bounded method
|
|
101
|
+
# on the configs
|
|
102
|
+
if not hasattr(attr, "__self__"):
|
|
103
|
+
for obj, _skip_attrs, resolver_id in configs:
|
|
104
|
+
func = getattr(obj, attr.__name__, None)
|
|
105
|
+
if func is not None and func.__func__ is attr:
|
|
106
|
+
return CallableSearchResult(attr.__name__, func, resolver_id)
|
|
110
107
|
|
|
111
|
-
|
|
108
|
+
return CallableSearchResult(attr, attr, None)
|
|
112
109
|
|
|
113
|
-
return EmptyWrapSearchResult(attr, None)
|
|
114
110
|
|
|
111
|
+
def _search_callable_in_configs(
|
|
112
|
+
attr, configs: tuple[ObjectConfig]
|
|
113
|
+
) -> Generator[WrapSearchResult, None, None]:
|
|
114
|
+
for obj, skip_attrs, resolver_id in configs:
|
|
115
|
+
if attr in skip_attrs:
|
|
116
|
+
continue
|
|
115
117
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
if not hasattr(obj, attr):
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
func = getattr(obj, attr)
|
|
122
|
+
if not callable(func):
|
|
123
|
+
yield AttributeCallableSearchResult(attr, obj, resolver_id)
|
|
124
|
+
|
|
125
|
+
if getattr(func, "_is_sm_event", False):
|
|
126
|
+
yield EventSearchResult(attr, func, resolver_id)
|
|
127
|
+
|
|
128
|
+
yield CallableSearchResult(attr, func, resolver_id)
|
|
118
129
|
|
|
119
|
-
objects = [ObjectConfig.from_obj(obj) for obj in objects]
|
|
120
130
|
|
|
121
|
-
|
|
122
|
-
|
|
131
|
+
def search_callable(attr, configs: tuple) -> Generator[WrapSearchResult, None, None]: # noqa: C901
|
|
132
|
+
if isinstance(attr, property):
|
|
133
|
+
result = _search_callable_attr_is_property(attr, configs)
|
|
134
|
+
if result is not None:
|
|
135
|
+
yield result
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if callable(attr):
|
|
139
|
+
yield _search_callable_attr_is_callable(attr, configs)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
yield from _search_callable_in_configs(attr, configs)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def resolver_factory(objects: tuple[ObjectConfig]):
|
|
146
|
+
"""Factory that returns a configured resolver."""
|
|
147
|
+
|
|
148
|
+
def wrapper(attr) -> Generator[WrapSearchResult, None, None]:
|
|
149
|
+
yield from search_callable(attr, objects)
|
|
123
150
|
|
|
124
151
|
return wrapper
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def resolver_factory_from_objects(*objects: tuple[Any]):
|
|
155
|
+
configs = tuple(ObjectConfig.from_obj(o) for o in objects)
|
|
156
|
+
return resolver_factory(configs)
|
statemachine/factory.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import warnings
|
|
1
2
|
from typing import TYPE_CHECKING
|
|
2
3
|
from typing import Any
|
|
3
4
|
from typing import Dict
|
|
@@ -9,6 +10,7 @@ from . import registry
|
|
|
9
10
|
from .event import Event
|
|
10
11
|
from .event import trigger_event_factory
|
|
11
12
|
from .exceptions import InvalidDefinition
|
|
13
|
+
from .graph import iterate_states_and_transitions
|
|
12
14
|
from .graph import visit_connected_states
|
|
13
15
|
from .i18n import _
|
|
14
16
|
from .state import State
|
|
@@ -18,7 +20,15 @@ from .transition_list import TransitionList
|
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
class StateMachineMetaclass(type):
|
|
21
|
-
|
|
23
|
+
"Metaclass for constructing StateMachine classes"
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
cls,
|
|
27
|
+
name: str,
|
|
28
|
+
bases: Tuple[type],
|
|
29
|
+
attrs: Dict[str, Any],
|
|
30
|
+
strict_states: bool = False,
|
|
31
|
+
) -> None:
|
|
22
32
|
super().__init__(name, bases, attrs)
|
|
23
33
|
registry.register(cls)
|
|
24
34
|
cls.name = cls.__name__
|
|
@@ -27,7 +37,9 @@ class StateMachineMetaclass(type):
|
|
|
27
37
|
"""Map of ``state.value`` to the corresponding :ref:`state`."""
|
|
28
38
|
|
|
29
39
|
cls._abstract = True
|
|
40
|
+
cls._strict_states = strict_states
|
|
30
41
|
cls._events: Dict[str, Event] = {}
|
|
42
|
+
cls._protected_attrs: set = set()
|
|
31
43
|
|
|
32
44
|
cls.add_inherited(bases)
|
|
33
45
|
cls.add_from_attributes(attrs)
|
|
@@ -40,12 +52,12 @@ class StateMachineMetaclass(type):
|
|
|
40
52
|
cls.final_states: List[State] = [state for state in cls.states if state.final]
|
|
41
53
|
|
|
42
54
|
cls._check()
|
|
55
|
+
cls._setup()
|
|
43
56
|
|
|
44
57
|
if TYPE_CHECKING:
|
|
45
58
|
"""Makes mypy happy with dynamic created attributes"""
|
|
46
59
|
|
|
47
|
-
def __getattr__(self, attribute: str) -> Any:
|
|
48
|
-
...
|
|
60
|
+
def __getattr__(self, attribute: str) -> Any: ...
|
|
49
61
|
|
|
50
62
|
def _check(cls):
|
|
51
63
|
has_states = bool(cls.states)
|
|
@@ -66,6 +78,8 @@ class StateMachineMetaclass(type):
|
|
|
66
78
|
cls._check_initial_state()
|
|
67
79
|
cls._check_final_states()
|
|
68
80
|
cls._check_disconnected_state()
|
|
81
|
+
cls._check_trap_states()
|
|
82
|
+
cls._check_reachable_final_states()
|
|
69
83
|
|
|
70
84
|
def _check_initial_state(cls):
|
|
71
85
|
initials = [s for s in cls.states if s.initial]
|
|
@@ -73,7 +87,7 @@ class StateMachineMetaclass(type):
|
|
|
73
87
|
raise InvalidDefinition(
|
|
74
88
|
_(
|
|
75
89
|
"There should be one and only one initial state. "
|
|
76
|
-
"
|
|
90
|
+
"You currently have these: {!r}"
|
|
77
91
|
).format([s.id for s in initials])
|
|
78
92
|
)
|
|
79
93
|
|
|
@@ -84,11 +98,44 @@ class StateMachineMetaclass(type):
|
|
|
84
98
|
|
|
85
99
|
if final_state_with_invalid_transitions:
|
|
86
100
|
raise InvalidDefinition(
|
|
87
|
-
_(
|
|
88
|
-
|
|
89
|
-
)
|
|
101
|
+
_("Cannot declare transitions from final state. Invalid state(s): {}").format(
|
|
102
|
+
[s.id for s in final_state_with_invalid_transitions]
|
|
103
|
+
)
|
|
90
104
|
)
|
|
91
105
|
|
|
106
|
+
def _check_trap_states(cls):
|
|
107
|
+
trap_states = [s for s in cls.states if not s.final and not s.transitions]
|
|
108
|
+
if trap_states:
|
|
109
|
+
message = _(
|
|
110
|
+
"All non-final states should have at least one outgoing transition. "
|
|
111
|
+
"These states have no outgoing transition: {!r}"
|
|
112
|
+
).format([s.id for s in trap_states])
|
|
113
|
+
if cls._strict_states:
|
|
114
|
+
raise InvalidDefinition(message)
|
|
115
|
+
else:
|
|
116
|
+
warnings.warn(message, UserWarning, stacklevel=4)
|
|
117
|
+
|
|
118
|
+
def _check_reachable_final_states(cls):
|
|
119
|
+
if not any(s.final for s in cls.states):
|
|
120
|
+
return # No need to check final reachability
|
|
121
|
+
disconnected_states = cls._states_without_path_to_final_states()
|
|
122
|
+
if disconnected_states:
|
|
123
|
+
message = _(
|
|
124
|
+
"All non-final states should have at least one path to a final state. "
|
|
125
|
+
"These states have no path to a final state: {!r}"
|
|
126
|
+
).format([s.id for s in disconnected_states])
|
|
127
|
+
if cls._strict_states:
|
|
128
|
+
raise InvalidDefinition(message)
|
|
129
|
+
else:
|
|
130
|
+
warnings.warn(message, UserWarning, stacklevel=1)
|
|
131
|
+
|
|
132
|
+
def _states_without_path_to_final_states(cls):
|
|
133
|
+
return [
|
|
134
|
+
state
|
|
135
|
+
for state in cls.states
|
|
136
|
+
if not state.final and not any(s.final for s in visit_connected_states(state))
|
|
137
|
+
]
|
|
138
|
+
|
|
92
139
|
def _disconnected_states(cls, starting_state):
|
|
93
140
|
visitable_states = set(visit_connected_states(starting_state))
|
|
94
141
|
return set(cls.states) - visitable_states
|
|
@@ -104,6 +151,23 @@ class StateMachineMetaclass(type):
|
|
|
104
151
|
).format([s.id for s in disconnected_states])
|
|
105
152
|
)
|
|
106
153
|
|
|
154
|
+
def _setup(cls):
|
|
155
|
+
for visited in iterate_states_and_transitions(cls.states):
|
|
156
|
+
visited._setup()
|
|
157
|
+
|
|
158
|
+
cls._protected_attrs = {
|
|
159
|
+
"_abstract",
|
|
160
|
+
"model",
|
|
161
|
+
"state_field",
|
|
162
|
+
"start_value",
|
|
163
|
+
"initial_state",
|
|
164
|
+
"final_states",
|
|
165
|
+
"states",
|
|
166
|
+
"_events",
|
|
167
|
+
"states_map",
|
|
168
|
+
"send",
|
|
169
|
+
} | {s.id for s in cls.states}
|
|
170
|
+
|
|
107
171
|
def add_inherited(cls, bases):
|
|
108
172
|
for base in bases:
|
|
109
173
|
for state in getattr(base, "states", []):
|
statemachine/graph.py
CHANGED
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
|
@@ -9,6 +9,7 @@ from typing import Any
|
|
|
9
9
|
|
|
10
10
|
def _make_key(method):
|
|
11
11
|
method = method.func if isinstance(method, partial) else method
|
|
12
|
+
method = method.fget if isinstance(method, property) else method
|
|
12
13
|
if isinstance(method, MethodType):
|
|
13
14
|
return hash(
|
|
14
15
|
(
|
|
@@ -22,7 +23,6 @@ def _make_key(method):
|
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
def signature_cache(user_function):
|
|
25
|
-
|
|
26
26
|
cache = {}
|
|
27
27
|
cache_get = cache.get
|
|
28
28
|
|
|
@@ -113,8 +113,7 @@ class SignatureAdapter(Signature):
|
|
|
113
113
|
parameters_ex = (param,)
|
|
114
114
|
break
|
|
115
115
|
elif (
|
|
116
|
-
param.kind == Parameter.VAR_KEYWORD
|
|
117
|
-
or param.default is not Parameter.empty
|
|
116
|
+
param.kind == Parameter.VAR_KEYWORD or param.default is not Parameter.empty
|
|
118
117
|
):
|
|
119
118
|
# That's fine too - we have a default value for this
|
|
120
119
|
# parameter. So, lets start parsing `kwargs`, starting
|
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,14 @@
|
|
|
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
|
+
|
|
10
|
+
from .callbacks import CallbackMetaList
|
|
11
|
+
from .callbacks import CallbacksExecutor
|
|
7
12
|
from .callbacks import CallbacksRegistry
|
|
8
13
|
from .dispatcher import ObjectConfig
|
|
9
14
|
from .dispatcher import resolver_factory
|
|
@@ -73,22 +78,22 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
73
78
|
self._external_queue: deque = deque()
|
|
74
79
|
self._callbacks_registry = CallbacksRegistry()
|
|
75
80
|
self._states_for_instance: Dict["State", "State"] = {}
|
|
81
|
+
self._observers: Dict[Any, Any] = {}
|
|
76
82
|
|
|
77
|
-
assert hasattr(self, "_abstract")
|
|
78
83
|
if self._abstract:
|
|
79
84
|
raise InvalidDefinition(_("There are no states or transitions."))
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
self._register_callbacks()
|
|
87
|
+
self._activate_initial_state()
|
|
88
|
+
|
|
89
|
+
def __init_subclass__(cls, strict_states: bool = False):
|
|
90
|
+
cls._strict_states = strict_states
|
|
91
|
+
super().__init_subclass__()
|
|
86
92
|
|
|
87
93
|
if TYPE_CHECKING:
|
|
88
94
|
"""Makes mypy happy with dynamic created attributes"""
|
|
89
95
|
|
|
90
|
-
def __getattr__(self, attribute: str) -> Any:
|
|
91
|
-
...
|
|
96
|
+
def __getattr__(self, attribute: str) -> Any: ...
|
|
92
97
|
|
|
93
98
|
def __repr__(self):
|
|
94
99
|
current_state_id = self.current_state.id if self.current_state_value else None
|
|
@@ -97,19 +102,31 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
97
102
|
f"current_state={current_state_id!r})"
|
|
98
103
|
)
|
|
99
104
|
|
|
105
|
+
def __deepcopy__(self, memo):
|
|
106
|
+
deepcopy_method = self.__deepcopy__
|
|
107
|
+
self.__deepcopy__ = None
|
|
108
|
+
try:
|
|
109
|
+
cp = deepcopy(self, memo)
|
|
110
|
+
finally:
|
|
111
|
+
self.__deepcopy__ = deepcopy_method
|
|
112
|
+
cp.__deepcopy__ = deepcopy_method
|
|
113
|
+
cp._callbacks_registry.clear()
|
|
114
|
+
cp._register_callbacks()
|
|
115
|
+
cp.add_observer(*cp._observers.keys())
|
|
116
|
+
return cp
|
|
117
|
+
|
|
100
118
|
def _get_initial_state(self):
|
|
101
|
-
current_state_value =
|
|
102
|
-
self.start_value if self.start_value else self.initial_state.value
|
|
103
|
-
)
|
|
119
|
+
current_state_value = self.start_value if self.start_value else self.initial_state.value
|
|
104
120
|
try:
|
|
105
121
|
return self.states_map[current_state_value]
|
|
106
122
|
except KeyError as err:
|
|
107
123
|
raise InvalidStateValue(current_state_value) from err
|
|
108
124
|
|
|
109
|
-
def _activate_initial_state(self
|
|
125
|
+
def _activate_initial_state(self):
|
|
110
126
|
if self.current_state_value is None:
|
|
111
127
|
# send an one-time event `__initial__` to enter the current state.
|
|
112
128
|
# current_state = self.current_state
|
|
129
|
+
initial_transition = Transition(None, self._get_initial_state(), event="__initial__")
|
|
113
130
|
initial_transition.before.clear()
|
|
114
131
|
initial_transition.on.clear()
|
|
115
132
|
initial_transition.after.clear()
|
|
@@ -123,63 +140,29 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
123
140
|
)
|
|
124
141
|
self._activate(event_data)
|
|
125
142
|
|
|
126
|
-
def
|
|
127
|
-
|
|
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)
|
|
155
|
-
|
|
156
|
-
register = partial(self._callbacks_registry.register, resolver=default_resolver)
|
|
157
|
-
|
|
158
|
-
observer_visitor = self._build_observers_visitor(machine, model)
|
|
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 = [
|
|
143
|
+
def _register_callbacks(self):
|
|
144
|
+
self._add_observer(
|
|
170
145
|
(
|
|
171
|
-
self.
|
|
172
|
-
|
|
173
|
-
)
|
|
146
|
+
ObjectConfig.from_obj(self, skip_attrs=self._protected_attrs),
|
|
147
|
+
ObjectConfig.from_obj(self.model, skip_attrs={self.state_field}),
|
|
174
148
|
)
|
|
175
|
-
|
|
176
|
-
]
|
|
149
|
+
)
|
|
177
150
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
151
|
+
check_callbacks = self._callbacks_registry.check
|
|
152
|
+
for visited in iterate_states_and_transitions(self.states):
|
|
153
|
+
try:
|
|
154
|
+
visited._check_callbacks(check_callbacks)
|
|
155
|
+
except Exception as err:
|
|
156
|
+
raise InvalidDefinition(
|
|
157
|
+
f"Error on {visited!s} when resolving callbacks: {err}"
|
|
158
|
+
) from err
|
|
181
159
|
|
|
182
|
-
|
|
160
|
+
def _add_observer(self, observers):
|
|
161
|
+
register = partial(self._callbacks_registry.register, resolver=resolver_factory(observers))
|
|
162
|
+
for visited in iterate_states_and_transitions(self.states):
|
|
163
|
+
visited._add_observer(register)
|
|
164
|
+
|
|
165
|
+
return self
|
|
183
166
|
|
|
184
167
|
def add_observer(self, *observers):
|
|
185
168
|
"""Add an observer.
|
|
@@ -191,10 +174,8 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
191
174
|
|
|
192
175
|
:ref:`observers`.
|
|
193
176
|
"""
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
self._visit_states_and_transitions(visitor)
|
|
197
|
-
return self
|
|
177
|
+
self._observers.update({o: None for o in observers})
|
|
178
|
+
return self._add_observer(tuple(ObjectConfig.from_obj(o) for o in observers))
|
|
198
179
|
|
|
199
180
|
def _repr_html_(self):
|
|
200
181
|
return f'<div class="statemachine">{self._repr_svg_()}</div>'
|
|
@@ -248,10 +229,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
248
229
|
@property
|
|
249
230
|
def allowed_events(self):
|
|
250
231
|
"""List of the current allowed events."""
|
|
251
|
-
return [
|
|
252
|
-
getattr(self, event)
|
|
253
|
-
for event in self.current_state.transitions.unique_events
|
|
254
|
-
]
|
|
232
|
+
return [getattr(self, event) for event in self.current_state.transitions.unique_events]
|
|
255
233
|
|
|
256
234
|
def _process(self, trigger):
|
|
257
235
|
"""Process event triggers.
|
|
@@ -317,15 +295,13 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
317
295
|
source = event_data.state
|
|
318
296
|
target = transition.target
|
|
319
297
|
|
|
320
|
-
result = self.
|
|
298
|
+
result = self._callbacks(transition.before).call(
|
|
321
299
|
*event_data.args, **event_data.extended_kwargs
|
|
322
300
|
)
|
|
323
301
|
if source is not None and not transition.internal:
|
|
324
|
-
self.
|
|
325
|
-
*event_data.args, **event_data.extended_kwargs
|
|
326
|
-
)
|
|
302
|
+
self._callbacks(source.exit).call(*event_data.args, **event_data.extended_kwargs)
|
|
327
303
|
|
|
328
|
-
result += self.
|
|
304
|
+
result += self._callbacks(transition.on).call(
|
|
329
305
|
*event_data.args, **event_data.extended_kwargs
|
|
330
306
|
)
|
|
331
307
|
|
|
@@ -333,12 +309,8 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
333
309
|
event_data.state = target
|
|
334
310
|
|
|
335
311
|
if not transition.internal:
|
|
336
|
-
self.
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
self._callbacks_registry[transition.after].call(
|
|
340
|
-
*event_data.args, **event_data.extended_kwargs
|
|
341
|
-
)
|
|
312
|
+
self._callbacks(target.enter).call(*event_data.args, **event_data.extended_kwargs)
|
|
313
|
+
self._callbacks(transition.after).call(*event_data.args, **event_data.extended_kwargs)
|
|
342
314
|
|
|
343
315
|
if len(result) == 0:
|
|
344
316
|
result = None
|
|
@@ -357,3 +329,6 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
357
329
|
"""
|
|
358
330
|
event = Event(event)
|
|
359
331
|
return event.trigger(self, *args, **kwargs)
|
|
332
|
+
|
|
333
|
+
def _callbacks(self, meta_list: CallbackMetaList) -> CallbacksExecutor:
|
|
334
|
+
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
|
|
statemachine/transition.py
CHANGED
|
@@ -2,6 +2,7 @@ from typing import TYPE_CHECKING
|
|
|
2
2
|
|
|
3
3
|
from .callbacks import BoolCallbackMeta
|
|
4
4
|
from .callbacks import CallbackMetaList
|
|
5
|
+
from .callbacks import CallbackPriority
|
|
5
6
|
from .event import same_event_cond_builder
|
|
6
7
|
from .events import Events
|
|
7
8
|
from .exceptions import InvalidDefinition
|
|
@@ -48,7 +49,6 @@ class Transition:
|
|
|
48
49
|
before=None,
|
|
49
50
|
after=None,
|
|
50
51
|
):
|
|
51
|
-
|
|
52
52
|
self.source = source
|
|
53
53
|
self.target = target
|
|
54
54
|
self.internal = internal
|
|
@@ -57,14 +57,14 @@ class Transition:
|
|
|
57
57
|
raise InvalidDefinition("Internal transitions should be self-transitions.")
|
|
58
58
|
|
|
59
59
|
self._events = Events().add(event)
|
|
60
|
-
self.validators = CallbackMetaList().add(validators)
|
|
61
|
-
self.before = CallbackMetaList().add(before)
|
|
62
|
-
self.on = CallbackMetaList().add(on)
|
|
63
|
-
self.after = CallbackMetaList().add(after)
|
|
60
|
+
self.validators = CallbackMetaList().add(validators, priority=CallbackPriority.INLINE)
|
|
61
|
+
self.before = CallbackMetaList().add(before, priority=CallbackPriority.INLINE)
|
|
62
|
+
self.on = CallbackMetaList().add(on, priority=CallbackPriority.INLINE)
|
|
63
|
+
self.after = CallbackMetaList().add(after, priority=CallbackPriority.INLINE)
|
|
64
64
|
self.cond = (
|
|
65
65
|
CallbackMetaList(factory=BoolCallbackMeta)
|
|
66
|
-
.add(cond)
|
|
67
|
-
.add(unless, expected_value=False)
|
|
66
|
+
.add(cond, priority=CallbackPriority.INLINE)
|
|
67
|
+
.add(unless, priority=CallbackPriority.INLINE, expected_value=False)
|
|
68
68
|
)
|
|
69
69
|
|
|
70
70
|
def __repr__(self):
|
|
@@ -73,49 +73,58 @@ class Transition:
|
|
|
73
73
|
f"internal={self.internal!r})"
|
|
74
74
|
)
|
|
75
75
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
-
register(self.cond)
|
|
79
|
-
register(self.before)
|
|
80
|
-
register(self.on)
|
|
81
|
-
register(self.after)
|
|
76
|
+
def __str__(self):
|
|
77
|
+
return f"transition {self.event!s} from {self.source!s} to {self.target!s}"
|
|
82
78
|
|
|
83
|
-
def
|
|
79
|
+
def _setup(self):
|
|
84
80
|
before = self.before.add
|
|
85
81
|
on = self.on.add
|
|
86
82
|
after = self.after.add
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
)
|
|
90
|
-
on("on_transition", registry=registry, suppress_errors=True, prepend=True)
|
|
83
|
+
|
|
84
|
+
before("before_transition", priority=CallbackPriority.GENERIC, suppress_errors=True)
|
|
85
|
+
on("on_transition", priority=CallbackPriority.GENERIC, suppress_errors=True)
|
|
91
86
|
|
|
92
87
|
for event in self._events:
|
|
93
88
|
same_event_cond = same_event_cond_builder(event)
|
|
94
89
|
before(
|
|
95
90
|
f"before_{event}",
|
|
96
|
-
|
|
91
|
+
priority=CallbackPriority.NAMING,
|
|
97
92
|
suppress_errors=True,
|
|
98
93
|
cond=same_event_cond,
|
|
99
94
|
)
|
|
100
95
|
on(
|
|
101
96
|
f"on_{event}",
|
|
102
|
-
|
|
97
|
+
priority=CallbackPriority.NAMING,
|
|
103
98
|
suppress_errors=True,
|
|
104
99
|
cond=same_event_cond,
|
|
105
100
|
)
|
|
106
101
|
after(
|
|
107
102
|
f"after_{event}",
|
|
108
|
-
|
|
103
|
+
priority=CallbackPriority.NAMING,
|
|
109
104
|
suppress_errors=True,
|
|
110
105
|
cond=same_event_cond,
|
|
111
106
|
)
|
|
112
107
|
|
|
113
108
|
after(
|
|
114
109
|
"after_transition",
|
|
115
|
-
|
|
110
|
+
priority=CallbackPriority.AFTER,
|
|
116
111
|
suppress_errors=True,
|
|
117
112
|
)
|
|
118
113
|
|
|
114
|
+
def _add_observer(self, register):
|
|
115
|
+
register(self.validators)
|
|
116
|
+
register(self.cond)
|
|
117
|
+
register(self.before)
|
|
118
|
+
register(self.on)
|
|
119
|
+
register(self.after)
|
|
120
|
+
|
|
121
|
+
def _check_callbacks(self, check):
|
|
122
|
+
check(self.validators)
|
|
123
|
+
check(self.cond)
|
|
124
|
+
check(self.before)
|
|
125
|
+
check(self.on)
|
|
126
|
+
check(self.after)
|
|
127
|
+
|
|
119
128
|
def match(self, event):
|
|
120
129
|
return self._events.match(event)
|
|
121
130
|
|
statemachine/utils.py
CHANGED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
statemachine/__init__.py,sha256=Bx88FyKa4Gm2RZxCWmYSxlefw0hVvOJMdWcqpsEiCgc,192
|
|
2
|
-
statemachine/callbacks.py,sha256=_tmx5AZlhn_f3zhtUNAKBwfkkOKQLPBfSByhDUdFCeo,8265
|
|
3
|
-
statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
statemachine/contrib/diagram.py,sha256=_UxkX0ycOhzCAuvV_b3hrvZ6V_VOUFmbBigI3oz4PCA,6748
|
|
5
|
-
statemachine/dispatcher.py,sha256=GMAwivNTz_m8X9szPqKD9O0cV2XoWhPkBE8d3btsFHY,3598
|
|
6
|
-
statemachine/event.py,sha256=fvWxyFcrbPVxNW94p66rGJ5I1UZUK9mDJz68udkz2JI,2106
|
|
7
|
-
statemachine/event_data.py,sha256=nxOfypngK-78A77gj4pS-oxLx9zl1S9wnSt_rTiCyZA,2257
|
|
8
|
-
statemachine/events.py,sha256=rUbiTX-QGoo7zzmQMEeWeLrIVnp9zhicdRIm34CoAVI,731
|
|
9
|
-
statemachine/exceptions.py,sha256=uzvDbHkzCZkmFI_02L0yC9gd6d0SBn9GmsI8ekc0Hjk,952
|
|
10
|
-
statemachine/factory.py,sha256=KGAa3rSnxTFyPSD1UXDgkGtBpg95A5vFISq3YCyvccQ,5779
|
|
11
|
-
statemachine/graph.py,sha256=QTNlEmEYCq6NfX6yiKrjfEY8bpbGASgV-M1g5TNR7YM,359
|
|
12
|
-
statemachine/i18n.py,sha256=NLvGseaORmQ0G-V_J8tkjoxh_piWMOm2CI6mBQpLamc,362
|
|
13
|
-
statemachine/locale/en/LC_MESSAGES/statemachine.mo,sha256=aGIvF2G_OISexcLv3ZsudPfAkVIvL33zaEGeXJM47-I,452
|
|
14
|
-
statemachine/locale/en/LC_MESSAGES/statemachine.po,sha256=MfODCwzML6DzhOpHbno2Pdhsb26AWboR5Ka1JCNvJIA,1685
|
|
15
|
-
statemachine/locale/pt_BR/LC_MESSAGES/statemachine.mo,sha256=wTh7ge52cALzWScvS1wdjT-QO5ZhrsvodiI-dscYZhI,1914
|
|
16
|
-
statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=KsR9rlnhw17EZG-IbVx3F0wJAJSK0B-OByGmUmDHDUM,2375
|
|
17
|
-
statemachine/mixins.py,sha256=54aOvLZh9TyAIrqwfufGNmo1K4gaJR_dzhPdzisGPMk,1055
|
|
18
|
-
statemachine/model.py,sha256=OylI3FjMiHpYyDl9mtK1zEJMeSvemaN4giQDonpc8kI,211
|
|
19
|
-
statemachine/registry.py,sha256=RnVBRS3I_6Tm2OMgXMB_ewX7zQaslqEfhXFOhbqIkG4,959
|
|
20
|
-
statemachine/signature.py,sha256=o8HLzhysMSnH_yy8qWG7aW5jVMOWdJM47OMnMyGVfC4,7463
|
|
21
|
-
statemachine/state.py,sha256=YJ6NxsYi6KYxMTH8wp_jf4yZL2FSYN_HZmcqJXmch1w,8175
|
|
22
|
-
statemachine/statemachine.py,sha256=rol4RKXG7R0hsibLCkcQMs7v3__v069IJc7xtFZuhRc,11998
|
|
23
|
-
statemachine/states.py,sha256=gK-_csTDatvX4DYJcHUB45Ealv4FnfVz1Bj7IAS5nn0,4151
|
|
24
|
-
statemachine/transition.py,sha256=7UO6Kjb73tsF7nHfaYM94J-FsByZJN81k_KtCBB_1vY,4833
|
|
25
|
-
statemachine/transition_list.py,sha256=DatsmMWgK0YK30Nrj-josVvlTgeGapKutzYur9-puF8,5949
|
|
26
|
-
statemachine/utils.py,sha256=hY72gKE7VT9dn3xW5ffjKZosVblGbga8G9sKLEh5ZFg,317
|
|
27
|
-
python_statemachine-2.1.2.dist-info/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
|
|
28
|
-
python_statemachine-2.1.2.dist-info/METADATA,sha256=3KLowdmDsCjT4zMeeFpan_7g-HVlLGoCgiiLzwzRybA,11454
|
|
29
|
-
python_statemachine-2.1.2.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
|
30
|
-
python_statemachine-2.1.2.dist-info/RECORD,,
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|