python-statemachine 2.1.1__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.1.dist-info → python_statemachine-2.2.0.dist-info}/METADATA +3 -7
- python_statemachine-2.2.0.dist-info/RECORD +28 -0
- {python_statemachine-2.1.1.dist-info → python_statemachine-2.2.0.dist-info}/WHEEL +1 -1
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +176 -63
- statemachine/contrib/diagram.py +20 -6
- statemachine/dispatcher.py +114 -59
- statemachine/factory.py +71 -7
- statemachine/graph.py +6 -0
- statemachine/mixins.py +1 -3
- statemachine/signature.py +47 -13
- statemachine/state.py +93 -38
- statemachine/statemachine.py +74 -74
- statemachine/states.py +6 -4
- statemachine/transition.py +63 -40
- statemachine/utils.py +5 -5
- python_statemachine-2.1.1.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.1.dist-info → python_statemachine-2.2.0.dist-info}/LICENSE +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,21 +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
|
-
Classifier: Programming Language :: Python :: 3.
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
20
|
Classifier: Programming Language :: Python :: 3.7
|
|
24
21
|
Classifier: Programming Language :: Python :: 3.8
|
|
25
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
26
22
|
Classifier: Topic :: Software Development :: Libraries
|
|
27
23
|
Provides-Extra: diagrams
|
|
28
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,11 +1,57 @@
|
|
|
1
|
+
from bisect import insort
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from collections import deque
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
from typing import Callable
|
|
6
|
+
from typing import Dict
|
|
7
|
+
from typing import Generator
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
1
10
|
from .exceptions import AttrNotFound
|
|
2
|
-
from .exceptions import InvalidDefinition
|
|
3
11
|
from .i18n import _
|
|
4
12
|
from .utils import ensure_iterable
|
|
5
13
|
|
|
6
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
|
+
|
|
7
27
|
class CallbackWrapper:
|
|
8
|
-
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
callback: Callable,
|
|
31
|
+
condition: Callable,
|
|
32
|
+
meta: "CallbackMeta",
|
|
33
|
+
unique_key: str,
|
|
34
|
+
) -> None:
|
|
35
|
+
self._callback = callback
|
|
36
|
+
self.condition = condition
|
|
37
|
+
self.meta = meta
|
|
38
|
+
self.unique_key = unique_key
|
|
39
|
+
|
|
40
|
+
def __repr__(self):
|
|
41
|
+
return f"{type(self).__name__}({self.unique_key})"
|
|
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
|
+
|
|
49
|
+
def __call__(self, *args, **kwargs):
|
|
50
|
+
return self._callback(*args, **kwargs)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CallbackMeta:
|
|
54
|
+
"""A thin wrapper that register info about actions and guards.
|
|
9
55
|
|
|
10
56
|
At first, `func` can be a string or a callable, and even if it's already
|
|
11
57
|
a callable, his signature can mismatch.
|
|
@@ -14,21 +60,28 @@ class CallbackWrapper:
|
|
|
14
60
|
call is performed, to allow the proper callback resolution.
|
|
15
61
|
"""
|
|
16
62
|
|
|
17
|
-
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
|
+
):
|
|
18
71
|
self.func = func
|
|
19
72
|
self.suppress_errors = suppress_errors
|
|
20
|
-
self.cond =
|
|
21
|
-
self.
|
|
22
|
-
self.
|
|
73
|
+
self.cond = cond
|
|
74
|
+
self.expected_value = expected_value
|
|
75
|
+
self.priority = priority
|
|
23
76
|
|
|
24
77
|
def __repr__(self):
|
|
25
|
-
return f"{type(self).__name__}({self.func!r})"
|
|
78
|
+
return f"{type(self).__name__}({self.func!r}, suppress_errors={self.suppress_errors!r})"
|
|
26
79
|
|
|
27
80
|
def __str__(self):
|
|
28
81
|
return getattr(self.func, "__name__", self.func)
|
|
29
82
|
|
|
30
83
|
def __eq__(self, other):
|
|
31
|
-
return self.func == other.func
|
|
84
|
+
return self.func == other.func
|
|
32
85
|
|
|
33
86
|
def __hash__(self):
|
|
34
87
|
return id(self)
|
|
@@ -36,7 +89,10 @@ class CallbackWrapper:
|
|
|
36
89
|
def _update_func(self, func):
|
|
37
90
|
self.func = func
|
|
38
91
|
|
|
39
|
-
def
|
|
92
|
+
def _wrap_callable(self, func, _expected_value):
|
|
93
|
+
return func
|
|
94
|
+
|
|
95
|
+
def build(self, resolver) -> Generator["CallbackWrapper", None, None]:
|
|
40
96
|
"""
|
|
41
97
|
Resolves the `func` into a usable callable.
|
|
42
98
|
|
|
@@ -44,41 +100,54 @@ class CallbackWrapper:
|
|
|
44
100
|
resolver (callable): A method responsible to build and return a valid callable that
|
|
45
101
|
can receive arbitrary parameters like `*args, **kwargs`.
|
|
46
102
|
"""
|
|
47
|
-
self.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
raise
|
|
55
|
-
return False
|
|
56
|
-
|
|
57
|
-
def __call__(self, *args, **kwargs):
|
|
58
|
-
if self._callback is None:
|
|
59
|
-
raise InvalidDefinition(
|
|
60
|
-
_("Callback {!r} not property configured.").format(self)
|
|
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,
|
|
109
|
+
unique_key=callback.unique_key,
|
|
61
110
|
)
|
|
62
|
-
return self._callback(*args, **kwargs)
|
|
63
111
|
|
|
64
112
|
|
|
65
|
-
class
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
113
|
+
class BoolCallbackMeta(CallbackMeta):
|
|
114
|
+
"""A thin wrapper that register info about actions and guards.
|
|
115
|
+
|
|
116
|
+
At first, `func` can be a string or a callable, and even if it's already
|
|
117
|
+
a callable, his signature can mismatch.
|
|
118
|
+
|
|
119
|
+
After instantiation, `.setup(resolver)` must be called before any real
|
|
120
|
+
call is performed, to allow the proper callback resolution.
|
|
121
|
+
"""
|
|
122
|
+
|
|
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
|
+
)
|
|
69
134
|
|
|
70
135
|
def __str__(self):
|
|
71
136
|
name = super().__str__()
|
|
72
137
|
return name if self.expected_value else f"!{name}"
|
|
73
138
|
|
|
74
|
-
def
|
|
75
|
-
|
|
139
|
+
def _wrap_callable(self, func, expected_value):
|
|
140
|
+
def bool_wrapper(*args, **kwargs):
|
|
141
|
+
return bool(func(*args, **kwargs)) == expected_value
|
|
76
142
|
|
|
143
|
+
return bool_wrapper
|
|
77
144
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
145
|
+
|
|
146
|
+
class CallbackMetaList:
|
|
147
|
+
"""List of `CallbackMeta` instances"""
|
|
148
|
+
|
|
149
|
+
def __init__(self, factory=CallbackMeta):
|
|
150
|
+
self.items: List[CallbackMeta] = []
|
|
82
151
|
self.factory = factory
|
|
83
152
|
|
|
84
153
|
def __repr__(self):
|
|
@@ -87,13 +156,6 @@ class Callbacks:
|
|
|
87
156
|
def __str__(self):
|
|
88
157
|
return ", ".join(str(c) for c in self)
|
|
89
158
|
|
|
90
|
-
def setup(self, resolver):
|
|
91
|
-
"""Validate configurations"""
|
|
92
|
-
self._resolver = resolver
|
|
93
|
-
self.items = [
|
|
94
|
-
callback for callback in self.items if callback.setup(self._resolver)
|
|
95
|
-
]
|
|
96
|
-
|
|
97
159
|
def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
|
|
98
160
|
"""This list was a target for adding a func using decorator
|
|
99
161
|
`@<state|event>[.on|before|after|enter|exit]` syntax.
|
|
@@ -127,6 +189,7 @@ class Callbacks:
|
|
|
127
189
|
return func
|
|
128
190
|
|
|
129
191
|
def __call__(self, callback):
|
|
192
|
+
"""Allows usage of the callback list as a decorator."""
|
|
130
193
|
return self._add_unbounded_callback(callback)
|
|
131
194
|
|
|
132
195
|
def __iter__(self):
|
|
@@ -135,31 +198,14 @@ class Callbacks:
|
|
|
135
198
|
def clear(self):
|
|
136
199
|
self.items = []
|
|
137
200
|
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
callback(*args, **kwargs)
|
|
141
|
-
for callback in self.items
|
|
142
|
-
if callback.cond.all(*args, **kwargs)
|
|
143
|
-
]
|
|
144
|
-
|
|
145
|
-
def all(self, *args, **kwargs):
|
|
146
|
-
return all(condition(*args, **kwargs) for condition in self)
|
|
147
|
-
|
|
148
|
-
def _add(self, func, resolver=None, prepend=False, **kwargs):
|
|
149
|
-
resolver = resolver or self._resolver
|
|
201
|
+
def _add(self, func, **kwargs):
|
|
202
|
+
meta = self.factory(func, **kwargs)
|
|
150
203
|
|
|
151
|
-
|
|
152
|
-
if resolver is not None and not callback.setup(resolver):
|
|
204
|
+
if meta in self.items:
|
|
153
205
|
return
|
|
154
206
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if prepend:
|
|
159
|
-
self.items.insert(0, callback)
|
|
160
|
-
else:
|
|
161
|
-
self.items.append(callback)
|
|
162
|
-
return callback
|
|
207
|
+
self.items.append(meta)
|
|
208
|
+
return meta
|
|
163
209
|
|
|
164
210
|
def add(self, callbacks, **kwargs):
|
|
165
211
|
if callbacks is None:
|
|
@@ -170,3 +216,70 @@ class Callbacks:
|
|
|
170
216
|
self._add(func, **kwargs)
|
|
171
217
|
|
|
172
218
|
return self
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class CallbacksExecutor:
|
|
222
|
+
"""A list of callbacks that can be executed in order."""
|
|
223
|
+
|
|
224
|
+
def __init__(self):
|
|
225
|
+
self.items: List[CallbackWrapper] = deque()
|
|
226
|
+
self.items_already_seen = set()
|
|
227
|
+
|
|
228
|
+
def __iter__(self):
|
|
229
|
+
return iter(self.items)
|
|
230
|
+
|
|
231
|
+
def __repr__(self):
|
|
232
|
+
return f"{type(self).__name__}({self.items!r})"
|
|
233
|
+
|
|
234
|
+
def __str__(self):
|
|
235
|
+
return ", ".join(str(c) for c in self)
|
|
236
|
+
|
|
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
|
|
241
|
+
|
|
242
|
+
self.items_already_seen.add(callback.unique_key)
|
|
243
|
+
insort(self.items, callback)
|
|
244
|
+
|
|
245
|
+
def add(self, items: CallbackMetaList, resolver: Callable):
|
|
246
|
+
"""Validate configurations"""
|
|
247
|
+
for item in items:
|
|
248
|
+
self._add(item, resolver)
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
def call(self, *args, **kwargs):
|
|
252
|
+
return [
|
|
253
|
+
callback(*args, **kwargs) for callback in self if callback.condition(*args, **kwargs)
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
def all(self, *args, **kwargs):
|
|
257
|
+
return all(condition(*args, **kwargs) for condition in self)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class CallbacksRegistry:
|
|
261
|
+
def __init__(self) -> None:
|
|
262
|
+
self._registry: Dict[CallbackMetaList, CallbacksExecutor] = defaultdict(CallbacksExecutor)
|
|
263
|
+
|
|
264
|
+
def register(self, meta_list: CallbackMetaList, resolver):
|
|
265
|
+
executor_list = self[meta_list]
|
|
266
|
+
executor_list.add(meta_list, resolver)
|
|
267
|
+
return executor_list
|
|
268
|
+
|
|
269
|
+
def clear(self):
|
|
270
|
+
self._registry.clear()
|
|
271
|
+
|
|
272
|
+
def __getitem__(self, meta_list: CallbackMetaList) -> CallbacksExecutor:
|
|
273
|
+
return self._registry[meta_list]
|
|
274
|
+
|
|
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
|
|
280
|
+
|
|
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,13 +1,12 @@
|
|
|
1
1
|
from collections import namedtuple
|
|
2
|
-
from functools import wraps
|
|
3
2
|
from operator import attrgetter
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import Generator
|
|
4
5
|
|
|
5
|
-
from .exceptions import AttrNotFound
|
|
6
|
-
from .i18n import _
|
|
7
6
|
from .signature import SignatureAdapter
|
|
8
7
|
|
|
9
8
|
|
|
10
|
-
class ObjectConfig(namedtuple("ObjectConfig", "obj skip_attrs")):
|
|
9
|
+
class ObjectConfig(namedtuple("ObjectConfig", "obj skip_attrs resolver_id")):
|
|
11
10
|
"""Configuration for objects passed to resolver_factory.
|
|
12
11
|
|
|
13
12
|
Args:
|
|
@@ -16,86 +15,142 @@ class ObjectConfig(namedtuple("ObjectConfig", "obj skip_attrs")):
|
|
|
16
15
|
"""
|
|
17
16
|
|
|
18
17
|
@classmethod
|
|
19
|
-
def from_obj(cls, obj):
|
|
18
|
+
def from_obj(cls, obj, skip_attrs=None):
|
|
20
19
|
if isinstance(obj, ObjectConfig):
|
|
21
20
|
return obj
|
|
22
21
|
else:
|
|
23
|
-
return cls(obj, set())
|
|
22
|
+
return cls(obj, skip_attrs or set(), str(id(obj)))
|
|
24
23
|
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
break
|
|
33
|
-
else:
|
|
34
|
-
raise AttrNotFound(
|
|
35
|
-
_("Did not found name '{}' from model or statemachine").format(attr)
|
|
36
|
-
)
|
|
37
|
-
return func, config.obj
|
|
25
|
+
class WrapSearchResult:
|
|
26
|
+
def __init__(self, attribute, resolver_id) -> None:
|
|
27
|
+
self.attribute = attribute
|
|
28
|
+
self.resolver_id = resolver_id
|
|
29
|
+
self._cache = None
|
|
30
|
+
self.unique_key = f"{attribute}@{resolver_id}"
|
|
38
31
|
|
|
32
|
+
def __repr__(self):
|
|
33
|
+
return f"{type(self).__name__}({self.unique_key})"
|
|
39
34
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
# so `func` contains it's current value.
|
|
43
|
-
# we'll build a method that get's the fresh value for each call
|
|
44
|
-
getter = attrgetter(attr)
|
|
35
|
+
def wrap(self): # pragma: no cover
|
|
36
|
+
pass
|
|
45
37
|
|
|
46
|
-
def
|
|
47
|
-
|
|
38
|
+
def __call__(self, *args: Any, **kwds: Any) -> Any:
|
|
39
|
+
if self._cache is None:
|
|
40
|
+
self._cache = self.wrap()
|
|
41
|
+
assert self._cache
|
|
42
|
+
return self._cache(*args, **kwds)
|
|
48
43
|
|
|
49
|
-
return wrapper
|
|
50
44
|
|
|
45
|
+
class CallableSearchResult(WrapSearchResult):
|
|
46
|
+
def __init__(self, attribute, a_callable, resolver_id) -> None:
|
|
47
|
+
self.a_callable = a_callable
|
|
48
|
+
super().__init__(attribute, resolver_id)
|
|
51
49
|
|
|
52
|
-
def
|
|
53
|
-
|
|
50
|
+
def wrap(self):
|
|
51
|
+
return SignatureAdapter.wrap(self.a_callable)
|
|
54
52
|
|
|
55
|
-
def wrapper(*args, **kwargs):
|
|
56
|
-
kwargs.pop("machine", None)
|
|
57
|
-
return func(*args, **kwargs)
|
|
58
53
|
|
|
59
|
-
|
|
54
|
+
class AttributeCallableSearchResult(WrapSearchResult):
|
|
55
|
+
def __init__(self, attribute, obj, resolver_id) -> None:
|
|
56
|
+
self.obj = obj
|
|
57
|
+
super().__init__(attribute, resolver_id)
|
|
60
58
|
|
|
59
|
+
def wrap(self):
|
|
60
|
+
# if `attr` is not callable, then it's an attribute or property,
|
|
61
|
+
# so `func` contains it's current value.
|
|
62
|
+
# we'll build a method that get's the fresh value for each call
|
|
63
|
+
getter = attrgetter(self.attribute)
|
|
61
64
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
`objects`.
|
|
65
|
+
def wrapper(*args, **kwargs):
|
|
66
|
+
return getter(self.obj)
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
attr (str or callable): A property/method name or a callable.
|
|
68
|
-
objects: A list of objects instances that will serve as lookup for the given attr.
|
|
69
|
-
The result `callable`, if any, will be a wrapper to the first object's attr that
|
|
70
|
-
has the given ``attr``.
|
|
71
|
-
"""
|
|
72
|
-
if callable(attr) or isinstance(attr, property):
|
|
73
|
-
return SignatureAdapter.wrap(attr)
|
|
68
|
+
return wrapper
|
|
74
69
|
|
|
75
|
-
# Setup configuration if not present to normalize the internal API
|
|
76
|
-
configs = [ObjectConfig.from_obj(obj) for obj in objects]
|
|
77
70
|
|
|
78
|
-
|
|
71
|
+
class EventSearchResult(WrapSearchResult):
|
|
72
|
+
def __init__(self, attribute, func, resolver_id) -> None:
|
|
73
|
+
self.func = func
|
|
74
|
+
super().__init__(attribute, resolver_id)
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
def wrap(self):
|
|
77
|
+
"Events already have the 'machine' parameter defined."
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
def wrapper(*args, **kwargs):
|
|
80
|
+
kwargs.pop("machine", None)
|
|
81
|
+
return self.func(*args, **kwargs)
|
|
85
82
|
|
|
86
|
-
|
|
83
|
+
return wrapper
|
|
87
84
|
|
|
88
85
|
|
|
89
|
-
def
|
|
90
|
-
|
|
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
|
|
97
|
+
|
|
98
|
+
|
|
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)
|
|
107
|
+
|
|
108
|
+
return CallableSearchResult(attr, attr, None)
|
|
109
|
+
|
|
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
|
|
91
117
|
|
|
92
|
-
|
|
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)
|
|
129
|
+
|
|
130
|
+
|
|
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
|
|
93
137
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return
|
|
138
|
+
if callable(attr):
|
|
139
|
+
yield _search_callable_attr_is_callable(attr, configs)
|
|
140
|
+
return
|
|
97
141
|
|
|
98
|
-
|
|
99
|
-
|
|
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)
|
|
100
150
|
|
|
101
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)
|