python-statemachine 2.3.6__py3-none-any.whl → 2.4.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.3.6.dist-info → python_statemachine-2.4.0.dist-info}/METADATA +9 -9
- python_statemachine-2.4.0.dist-info/RECORD +35 -0
- statemachine/__init__.py +3 -2
- statemachine/callbacks.py +62 -10
- statemachine/contrib/diagram.py +0 -4
- statemachine/dispatcher.py +5 -3
- statemachine/event.py +105 -27
- statemachine/event_data.py +2 -1
- statemachine/events.py +15 -6
- statemachine/exceptions.py +3 -2
- statemachine/factory.py +47 -12
- statemachine/locale/en/LC_MESSAGES/statemachine.po +27 -15
- statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +93 -0
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +36 -34
- statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +93 -0
- statemachine/spec_parser.py +79 -0
- statemachine/state.py +7 -0
- statemachine/statemachine.py +10 -19
- statemachine/states.py +2 -2
- statemachine/transition.py +1 -2
- statemachine/transition_list.py +5 -1
- python_statemachine-2.3.6.dist-info/RECORD +0 -32
- {python_statemachine-2.3.6.dist-info → python_statemachine-2.4.0.dist-info}/LICENSE +0 -0
- {python_statemachine-2.3.6.dist-info → python_statemachine-2.4.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.4.0
|
|
4
4
|
Summary: Python Finite State Machines made easy.
|
|
5
5
|
Home-page: https://github.com/fgmacedo/python-statemachine
|
|
6
6
|
License: MIT
|
|
@@ -23,7 +23,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
24
24
|
Classifier: Topic :: Software Development :: Libraries
|
|
25
25
|
Provides-Extra: diagrams
|
|
26
|
-
Requires-Dist: pydot (>=2.0.0) ; extra == "diagrams"
|
|
26
|
+
Requires-Dist: pydot (>=2.0.0) ; (python_full_version > "3.8.0") and (extra == "diagrams")
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
29
29
|
# Python StateMachine
|
|
@@ -405,7 +405,7 @@ There's a lot more to cover, please take a look at our docs:
|
|
|
405
405
|
https://python-statemachine.readthedocs.io.
|
|
406
406
|
|
|
407
407
|
|
|
408
|
-
## Contributing
|
|
408
|
+
## Contributing
|
|
409
409
|
|
|
410
410
|
* <a class="github-button" href="https://github.com/fgmacedo/python-statemachine" data-icon="octicon-star" aria-label="Star fgmacedo/python-statemachine on GitHub">Star this project</a>
|
|
411
411
|
* <a class="github-button" href="https://github.com/fgmacedo/python-statemachine/issues" data-icon="octicon-issue-opened" aria-label="Issue fgmacedo/python-statemachine on GitHub">Open an Issue</a>
|
|
@@ -413,18 +413,18 @@ https://python-statemachine.readthedocs.io.
|
|
|
413
413
|
|
|
414
414
|
- If you found this project helpful, please consider giving it a star on GitHub.
|
|
415
415
|
|
|
416
|
-
- **Contribute code**: If you would like to contribute code
|
|
416
|
+
- **Contribute code**: If you would like to contribute code, please submit a pull
|
|
417
417
|
request. For more information on how to contribute, please see our [contributing.md](contributing.md) file.
|
|
418
418
|
|
|
419
|
-
- **Report bugs**: If you find any bugs
|
|
419
|
+
- **Report bugs**: If you find any bugs, please report them by opening an issue
|
|
420
420
|
on our GitHub issue tracker.
|
|
421
421
|
|
|
422
|
-
- **Suggest features**: If you have
|
|
423
|
-
an issue on our GitHub issue tracker.
|
|
422
|
+
- **Suggest features**: If you have an idea for a new feature, of feels something being harder than it should be,
|
|
423
|
+
please let us know by opening an issue on our GitHub issue tracker.
|
|
424
424
|
|
|
425
|
-
- **Documentation**: Help improve
|
|
425
|
+
- **Documentation**: Help improve documentation by submitting pull requests.
|
|
426
426
|
|
|
427
|
-
- **Promote the project**: Help spread the word
|
|
427
|
+
- **Promote the project**: Help spread the word by sharing on social media,
|
|
428
428
|
writing a blog post, or giving a talk about it. Tag me on Twitter
|
|
429
429
|
[@fgmacedo](https://twitter.com/fgmacedo) so I can share it too!
|
|
430
430
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
statemachine/__init__.py,sha256=NLqMYmezhWHlgs3OJHd_zmKBsVEQAA3jhLIHAf4bFGM,226
|
|
2
|
+
statemachine/callbacks.py,sha256=5Onm2HxhSfFiilL_1PCUxIsLdcmEOQD80CArurIx9rs,12974
|
|
3
|
+
statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
statemachine/contrib/diagram.py,sha256=1z0d_Ln0k4-jLaEYry8si5pK25lsViyPWMT7KkDeDSc,7131
|
|
5
|
+
statemachine/dispatcher.py,sha256=4ewPGs-nOVSPMrDMhcuwkkc0c_AqLyw2Pv495bpfjXU,5407
|
|
6
|
+
statemachine/engines/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
statemachine/engines/async_.py,sha256=deZ5oYeoIYX9zOsFEOCT4KXsz5V1F9fbaMDOBATnTkE,5572
|
|
8
|
+
statemachine/engines/sync.py,sha256=cGy23Ivx0Ul4NWSix8E8PNA1Ji4V9qvOd3K_yodD_UI,5523
|
|
9
|
+
statemachine/event.py,sha256=ZyU6YmoLlKndBSohcmMp6v0tGSY2RG711JtJZJ6JXGY,3861
|
|
10
|
+
statemachine/event_data.py,sha256=H9lp_XnvHSK9YErUOCvMK3ZBjWhC-xDSOJ2gZxtmrq8,2261
|
|
11
|
+
statemachine/events.py,sha256=QVx1SsoPMwDI6J5mcuk8SoqMDIrPvZgQLOx4m_MNghI,1031
|
|
12
|
+
statemachine/exceptions.py,sha256=vHVQPTTMMkVvySNbN6XZPciBryvpY608LDe3MCnmxFU,1124
|
|
13
|
+
statemachine/factory.py,sha256=hVCLJIITv5ccXVPpg5H5B7_tfGZMqk7zSALTCAiXiWk,9318
|
|
14
|
+
statemachine/graph.py,sha256=KtwB1CYckaLjTgQD9tEeuaEzJje9q3fPVpBViW5TgSk,487
|
|
15
|
+
statemachine/i18n.py,sha256=NLvGseaORmQ0G-V_J8tkjoxh_piWMOm2CI6mBQpLamc,362
|
|
16
|
+
statemachine/locale/en/LC_MESSAGES/statemachine.po,sha256=4Pk-h5nk7twOTHcRQ_Chanfdi5EtFi9aTAzGkVJuCo8,2424
|
|
17
|
+
statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po,sha256=Bs5bbIxDrYtODSNJKNl4FH8ZtjEJwPIidRwbAxMwu5E,4941
|
|
18
|
+
statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=gmnhc15-6YVDCLWYT0ZQL5jfXHgIOYq5p5rJLqNPaxE,3593
|
|
19
|
+
statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po,sha256=cDbRHDYpi3pwJkFmaSn79q5KmX9cGmHFHw1ndj37vRw,3325
|
|
20
|
+
statemachine/mixins.py,sha256=Y1fa52Cj20JaGkyNk3P7Gpqkt4cGTjJ0YyV_VQyCl0M,1231
|
|
21
|
+
statemachine/model.py,sha256=OylI3FjMiHpYyDl9mtK1zEJMeSvemaN4giQDonpc8kI,211
|
|
22
|
+
statemachine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
statemachine/registry.py,sha256=HmV9sUGkYVrNUZxJYoZo-trSUis7dIun_WcGktblgc8,922
|
|
24
|
+
statemachine/signature.py,sha256=HwuyWX3p_GZwKKrGbt9wk4-bHpymqs_pgqW7OlDgGRc,7877
|
|
25
|
+
statemachine/spec_parser.py,sha256=Mqi9VMnlwJqEAcDnO-RMNv8kq-VKkHEqMgS_aeldcOI,3020
|
|
26
|
+
statemachine/state.py,sha256=DlFAuQb8HN6pKOyaRgLvYRHJZCFYo2LFLbikGzbE9Jw,8526
|
|
27
|
+
statemachine/statemachine.py,sha256=02qe_x2IAcLameakllXPviQ2pkAs9x9QA_ws-po-hWg,11372
|
|
28
|
+
statemachine/states.py,sha256=pPROZwIyE3_tlBGL3DJXnM3gr1pWsWICEMMo2c742RY,4889
|
|
29
|
+
statemachine/transition.py,sha256=4_dq2rR4x2at4d3B-3zuaP5mTa68P89DpFO2moPTTPM,4657
|
|
30
|
+
statemachine/transition_list.py,sha256=4xxMaD265GuSCXA8gzeEF06lh-eRO0O_X4YgsRZYWVA,6120
|
|
31
|
+
statemachine/utils.py,sha256=DpcrGqlbrnT-ogh-BogG0L07EG3KirHOsKORHlspDlI,1041
|
|
32
|
+
python_statemachine-2.4.0.dist-info/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
|
|
33
|
+
python_statemachine-2.4.0.dist-info/METADATA,sha256=qZJym13d9zQll_snjRZ53kl982Mcf0NTVxfq7P0OOT8,14036
|
|
34
|
+
python_statemachine-2.4.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
|
35
|
+
python_statemachine-2.4.0.dist-info/RECORD,,
|
statemachine/__init__.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
from .event import Event
|
|
1
2
|
from .state import State
|
|
2
3
|
from .statemachine import StateMachine
|
|
3
4
|
|
|
4
5
|
__author__ = """Fernando Macedo"""
|
|
5
6
|
__email__ = "fgmacedo@gmail.com"
|
|
6
|
-
__version__ = "2.
|
|
7
|
+
__version__ = "2.4.0"
|
|
7
8
|
|
|
8
|
-
__all__ = ["StateMachine", "State"]
|
|
9
|
+
__all__ = ["StateMachine", "State", "Event"]
|
statemachine/callbacks.py
CHANGED
|
@@ -5,19 +5,30 @@ from collections import deque
|
|
|
5
5
|
from enum import IntEnum
|
|
6
6
|
from enum import IntFlag
|
|
7
7
|
from enum import auto
|
|
8
|
+
from functools import partial
|
|
9
|
+
from functools import reduce
|
|
8
10
|
from inspect import isawaitable
|
|
9
11
|
from inspect import iscoroutinefunction
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
10
13
|
from typing import Callable
|
|
11
14
|
from typing import Dict
|
|
12
15
|
from typing import Generator
|
|
13
16
|
from typing import Iterable
|
|
14
17
|
from typing import List
|
|
18
|
+
from typing import Set
|
|
15
19
|
from typing import Type
|
|
16
20
|
|
|
17
21
|
from .exceptions import AttrNotFound
|
|
22
|
+
from .exceptions import InvalidDefinition
|
|
18
23
|
from .i18n import _
|
|
24
|
+
from .spec_parser import custom_and
|
|
25
|
+
from .spec_parser import operator_mapping
|
|
26
|
+
from .spec_parser import parse_boolean_expr
|
|
19
27
|
from .utils import ensure_iterable
|
|
20
28
|
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from statemachine.dispatcher import Listeners
|
|
31
|
+
|
|
21
32
|
|
|
22
33
|
class CallbackPriority(IntEnum):
|
|
23
34
|
GENERIC = 0
|
|
@@ -54,6 +65,17 @@ def allways_true(*args, **kwargs):
|
|
|
54
65
|
return True
|
|
55
66
|
|
|
56
67
|
|
|
68
|
+
def take_callback(name: str, resolver: "Listeners", not_found_handler: Callable) -> Callable:
|
|
69
|
+
callbacks = list(resolver.search_name(name))
|
|
70
|
+
if len(callbacks) == 0:
|
|
71
|
+
not_found_handler(name)
|
|
72
|
+
return allways_true
|
|
73
|
+
elif len(callbacks) == 1:
|
|
74
|
+
return callbacks[0]
|
|
75
|
+
else:
|
|
76
|
+
return reduce(custom_and, callbacks)
|
|
77
|
+
|
|
78
|
+
|
|
57
79
|
class CallbackSpec:
|
|
58
80
|
"""Specs about callbacks.
|
|
59
81
|
|
|
@@ -110,7 +132,16 @@ class CallbackSpec:
|
|
|
110
132
|
self.reference = SpecReference.CALLABLE
|
|
111
133
|
self.attr_name = attr_name
|
|
112
134
|
|
|
113
|
-
def
|
|
135
|
+
def _wrap(self, callback):
|
|
136
|
+
condition = self.cond if self.cond is not None else allways_true
|
|
137
|
+
return CallbackWrapper(
|
|
138
|
+
callback=callback,
|
|
139
|
+
condition=condition,
|
|
140
|
+
meta=self,
|
|
141
|
+
unique_key=callback.unique_key,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def build(self, resolver: "Listeners") -> Generator["CallbackWrapper", None, None]:
|
|
114
145
|
"""
|
|
115
146
|
Resolves the `func` into a usable callable.
|
|
116
147
|
|
|
@@ -118,14 +149,29 @@ class CallbackSpec:
|
|
|
118
149
|
resolver (callable): A method responsible to build and return a valid callable that
|
|
119
150
|
can receive arbitrary parameters like `*args, **kwargs`.
|
|
120
151
|
"""
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
152
|
+
if (
|
|
153
|
+
not self.is_convention
|
|
154
|
+
and self.group == CallbackGroup.COND
|
|
155
|
+
and self.reference == SpecReference.NAME
|
|
156
|
+
):
|
|
157
|
+
names_not_found: Set[str] = set()
|
|
158
|
+
take_callback_partial = partial(
|
|
159
|
+
take_callback, resolver=resolver, not_found_handler=names_not_found.add
|
|
128
160
|
)
|
|
161
|
+
try:
|
|
162
|
+
expression = parse_boolean_expr(self.func, take_callback_partial, operator_mapping)
|
|
163
|
+
except SyntaxError as err:
|
|
164
|
+
raise InvalidDefinition(
|
|
165
|
+
_("Failed to parse boolean expression '{}'").format(self.func)
|
|
166
|
+
) from err
|
|
167
|
+
if not expression or names_not_found:
|
|
168
|
+
self.names_not_found = names_not_found
|
|
169
|
+
return
|
|
170
|
+
yield self._wrap(expression)
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
for callback in resolver.search(self):
|
|
174
|
+
yield self._wrap(callback)
|
|
129
175
|
|
|
130
176
|
|
|
131
177
|
class SpecListGrouper:
|
|
@@ -292,7 +338,7 @@ class CallbacksExecutor:
|
|
|
292
338
|
def __str__(self):
|
|
293
339
|
return ", ".join(str(c) for c in self)
|
|
294
340
|
|
|
295
|
-
def _add(self, spec: CallbackSpec, resolver:
|
|
341
|
+
def _add(self, spec: CallbackSpec, resolver: "Listeners"):
|
|
296
342
|
for callback in spec.build(resolver):
|
|
297
343
|
if callback.unique_key in self.items_already_seen:
|
|
298
344
|
continue
|
|
@@ -300,7 +346,7 @@ class CallbacksExecutor:
|
|
|
300
346
|
self.items_already_seen.add(callback.unique_key)
|
|
301
347
|
insort(self.items, callback)
|
|
302
348
|
|
|
303
|
-
def add(self, items: Iterable[CallbackSpec], resolver:
|
|
349
|
+
def add(self, items: Iterable[CallbackSpec], resolver: "Listeners"):
|
|
304
350
|
"""Validate configurations"""
|
|
305
351
|
for item in items:
|
|
306
352
|
self._add(item, resolver)
|
|
@@ -356,6 +402,12 @@ class CallbacksRegistry:
|
|
|
356
402
|
callback for callback in self[meta.group.build_key(specs)] if callback.meta == meta
|
|
357
403
|
):
|
|
358
404
|
continue
|
|
405
|
+
if hasattr(meta, "names_not_found"):
|
|
406
|
+
raise AttrNotFound(
|
|
407
|
+
_("Did not found name '{}' from model or statemachine").format(
|
|
408
|
+
", ".join(meta.names_not_found)
|
|
409
|
+
),
|
|
410
|
+
)
|
|
359
411
|
raise AttrNotFound(
|
|
360
412
|
_("Did not found name '{}' from model or statemachine").format(meta.func)
|
|
361
413
|
)
|
statemachine/contrib/diagram.py
CHANGED
|
@@ -166,10 +166,6 @@ def quickchart_write_svg(sm: StateMachine, path: str):
|
|
|
166
166
|
>>> sm = OrderControl()
|
|
167
167
|
>>> print(sm._graph().to_string())
|
|
168
168
|
digraph list {
|
|
169
|
-
fontname=Arial;
|
|
170
|
-
fontsize=10;
|
|
171
|
-
label=OrderControl;
|
|
172
|
-
rankdir=LR;
|
|
173
169
|
...
|
|
174
170
|
|
|
175
171
|
To give you an example, we included this method that will serialize the dot, request the graph
|
statemachine/dispatcher.py
CHANGED
|
@@ -10,6 +10,7 @@ from typing import Tuple
|
|
|
10
10
|
|
|
11
11
|
from .callbacks import SPECS_ALL
|
|
12
12
|
from .callbacks import SpecReference
|
|
13
|
+
from .event import Event
|
|
13
14
|
from .signature import SignatureAdapter
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
@@ -75,7 +76,7 @@ class Listeners:
|
|
|
75
76
|
|
|
76
77
|
def search(self, spec: "CallbackSpec") -> Generator["Callable", None, None]:
|
|
77
78
|
if spec.reference is SpecReference.NAME:
|
|
78
|
-
yield from self.
|
|
79
|
+
yield from self.search_name(spec.func)
|
|
79
80
|
return
|
|
80
81
|
elif spec.reference is SpecReference.CALLABLE:
|
|
81
82
|
yield self._search_callable(spec)
|
|
@@ -111,7 +112,7 @@ class Listeners:
|
|
|
111
112
|
|
|
112
113
|
return callable_method(spec.attr_name, spec.func, None)
|
|
113
114
|
|
|
114
|
-
def
|
|
115
|
+
def search_name(self, name) -> Generator["Callable", None, None]:
|
|
115
116
|
for config in self.items:
|
|
116
117
|
if name not in config.all_attrs:
|
|
117
118
|
continue
|
|
@@ -121,7 +122,7 @@ class Listeners:
|
|
|
121
122
|
yield attr_method(name, config.obj, config.resolver_id)
|
|
122
123
|
continue
|
|
123
124
|
|
|
124
|
-
if
|
|
125
|
+
if isinstance(func, Event):
|
|
125
126
|
yield event_method(name, func, config.resolver_id)
|
|
126
127
|
continue
|
|
127
128
|
|
|
@@ -143,6 +144,7 @@ def attr_method(attribute, obj, resolver_id) -> Callable:
|
|
|
143
144
|
return getter(obj)
|
|
144
145
|
|
|
145
146
|
method.unique_key = f"{attribute}@{resolver_id}" # type: ignore[attr-defined]
|
|
147
|
+
method.__name__ = attribute
|
|
146
148
|
return method
|
|
147
149
|
|
|
148
150
|
|
statemachine/event.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from inspect import isawaitable
|
|
2
2
|
from typing import TYPE_CHECKING
|
|
3
|
+
from typing import List
|
|
4
|
+
from uuid import uuid4
|
|
3
5
|
|
|
4
6
|
from statemachine.utils import run_async_from_sync
|
|
5
7
|
|
|
@@ -7,47 +9,123 @@ from .event_data import TriggerData
|
|
|
7
9
|
|
|
8
10
|
if TYPE_CHECKING:
|
|
9
11
|
from .statemachine import StateMachine
|
|
12
|
+
from .transition_list import TransitionList
|
|
10
13
|
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
_event_data_kwargs = {
|
|
16
|
+
"event_data",
|
|
17
|
+
"machine",
|
|
18
|
+
"event",
|
|
19
|
+
"model",
|
|
20
|
+
"transition",
|
|
21
|
+
"state",
|
|
22
|
+
"source",
|
|
23
|
+
"target",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Event(str):
|
|
28
|
+
"""An event is triggers a signal that something has happened.
|
|
29
|
+
|
|
30
|
+
They are send to a state machine and allow the state machine to react.
|
|
31
|
+
|
|
32
|
+
An event starts a :ref:`Transition`, which can be thought of as a “cause” that initiates a
|
|
33
|
+
change in the state of the system.
|
|
34
|
+
|
|
35
|
+
See also :ref:`events`.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
id: str
|
|
39
|
+
"""The event identifier."""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
"""The event name."""
|
|
43
|
+
|
|
44
|
+
_sm: "StateMachine | None" = None
|
|
45
|
+
"""The state machine instance."""
|
|
46
|
+
|
|
47
|
+
_transitions: "TransitionList | None" = None
|
|
48
|
+
_has_real_id = False
|
|
49
|
+
|
|
50
|
+
def __new__(
|
|
51
|
+
cls,
|
|
52
|
+
transitions: "str | TransitionList | None" = None,
|
|
53
|
+
id: "str | None" = None,
|
|
54
|
+
name: "str | None" = None,
|
|
55
|
+
_sm: "StateMachine | None" = None,
|
|
56
|
+
):
|
|
57
|
+
if isinstance(transitions, str):
|
|
58
|
+
id = transitions
|
|
59
|
+
transitions = None
|
|
60
|
+
|
|
61
|
+
_has_real_id = id is not None
|
|
62
|
+
id = str(id) if _has_real_id else f"__event__{uuid4().hex}"
|
|
63
|
+
|
|
64
|
+
instance = super().__new__(cls, id)
|
|
65
|
+
instance.id = id
|
|
66
|
+
if name:
|
|
67
|
+
instance.name = name
|
|
68
|
+
elif _has_real_id:
|
|
69
|
+
instance.name = str(id).replace("_", " ").capitalize()
|
|
70
|
+
else:
|
|
71
|
+
instance.name = ""
|
|
72
|
+
if transitions:
|
|
73
|
+
instance._transitions = transitions
|
|
74
|
+
instance._has_real_id = _has_real_id
|
|
75
|
+
instance._sm = _sm
|
|
76
|
+
return instance
|
|
15
77
|
|
|
16
78
|
def __repr__(self):
|
|
17
|
-
return f"{type(self).__name__}({self.
|
|
79
|
+
return f"{type(self).__name__}({self.id!r})"
|
|
18
80
|
|
|
19
|
-
def
|
|
81
|
+
def is_same_event(self, *_args, event: "str | None" = None, **_kwargs) -> bool:
|
|
82
|
+
return self == event
|
|
83
|
+
|
|
84
|
+
def __get__(self, instance, owner):
|
|
85
|
+
"""By implementing this method `Event` can be used as a property descriptor
|
|
86
|
+
|
|
87
|
+
When attached to a SM class, if the user tries to get the `Event` instance,
|
|
88
|
+
we intercept here and return a `BoundEvent` instance, so the user can call
|
|
89
|
+
it as a method with the correct SM instance.
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
if instance is None:
|
|
93
|
+
return self
|
|
94
|
+
return BoundEvent(id=self.id, name=self.name, _sm=instance)
|
|
95
|
+
|
|
96
|
+
def __call__(self, *args, **kwargs):
|
|
97
|
+
"""Send this event to the current state machine.
|
|
98
|
+
|
|
99
|
+
Triggering an event on a state machine means invoking or sending a signal, initiating the
|
|
100
|
+
process that may result in executing a transition.
|
|
101
|
+
"""
|
|
102
|
+
# The `__call__` is declared here to help IDEs knowing that an `Event`
|
|
103
|
+
# can be called as a method. But it is not meant to be called without
|
|
104
|
+
# an SM instance. Such SM instance is provided by `__get__` method when
|
|
105
|
+
# used as a property descriptor.
|
|
106
|
+
|
|
107
|
+
machine = self._sm
|
|
108
|
+
kwargs = {k: v for k, v in kwargs.items() if k not in _event_data_kwargs}
|
|
20
109
|
trigger_data = TriggerData(
|
|
21
110
|
machine=machine,
|
|
22
|
-
event=self
|
|
111
|
+
event=self,
|
|
23
112
|
args=args,
|
|
24
113
|
kwargs=kwargs,
|
|
25
114
|
)
|
|
26
115
|
machine._put_nonblocking(trigger_data)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def trigger_event_factory(event_instance: Event):
|
|
31
|
-
"""Build a method that sends specific `event` to the machine"""
|
|
32
|
-
|
|
33
|
-
def trigger_event(self, *args, **kwargs):
|
|
34
|
-
result = event_instance.trigger(self, *args, **kwargs)
|
|
116
|
+
result = machine._processing_loop()
|
|
35
117
|
if not isawaitable(result):
|
|
36
118
|
return result
|
|
37
119
|
return run_async_from_sync(result)
|
|
38
120
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"""
|
|
47
|
-
Builds a condition method that evaluates to ``True`` when the expected event is received.
|
|
48
|
-
"""
|
|
121
|
+
def split( # type: ignore[override]
|
|
122
|
+
self, sep: "str | None" = None, maxsplit: int = -1
|
|
123
|
+
) -> List["Event"]:
|
|
124
|
+
result = super().split(sep, maxsplit)
|
|
125
|
+
if len(result) == 1:
|
|
126
|
+
return [self]
|
|
127
|
+
return [Event(event) for event in result]
|
|
49
128
|
|
|
50
|
-
def cond(*args, event: "str | None" = None, **kwargs) -> bool:
|
|
51
|
-
return event == expected_event
|
|
52
129
|
|
|
53
|
-
|
|
130
|
+
class BoundEvent(Event):
|
|
131
|
+
pass
|
statemachine/event_data.py
CHANGED
|
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
|
+
from .event import Event
|
|
7
8
|
from .state import State
|
|
8
9
|
from .statemachine import StateMachine
|
|
9
10
|
from .transition import Transition
|
|
@@ -13,7 +14,7 @@ if TYPE_CHECKING:
|
|
|
13
14
|
class TriggerData:
|
|
14
15
|
machine: "StateMachine"
|
|
15
16
|
|
|
16
|
-
event:
|
|
17
|
+
event: "Event"
|
|
17
18
|
"""The Event that was triggered."""
|
|
18
19
|
|
|
19
20
|
model: Any = field(init=False)
|
statemachine/events.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from statemachine.event import Event
|
|
2
|
+
|
|
1
3
|
from .utils import ensure_iterable
|
|
2
4
|
|
|
3
5
|
|
|
@@ -5,14 +7,14 @@ class Events:
|
|
|
5
7
|
"""A collection of event names."""
|
|
6
8
|
|
|
7
9
|
def __init__(self):
|
|
8
|
-
self.
|
|
10
|
+
self._items: list[Event] = []
|
|
9
11
|
|
|
10
12
|
def __repr__(self):
|
|
11
|
-
sep = " " if len(self.
|
|
12
|
-
return sep.join(item for item in self.
|
|
13
|
+
sep = " " if len(self._items) > 1 else ""
|
|
14
|
+
return sep.join(item for item in self._items)
|
|
13
15
|
|
|
14
16
|
def __iter__(self):
|
|
15
|
-
return iter(self.
|
|
17
|
+
return iter(self._items)
|
|
16
18
|
|
|
17
19
|
def add(self, events):
|
|
18
20
|
if events is None:
|
|
@@ -21,11 +23,18 @@ class Events:
|
|
|
21
23
|
unprepared = ensure_iterable(events)
|
|
22
24
|
for events in unprepared:
|
|
23
25
|
for event in events.split(" "):
|
|
24
|
-
if event in self.
|
|
26
|
+
if event in self._items:
|
|
25
27
|
continue
|
|
26
|
-
|
|
28
|
+
if isinstance(event, Event):
|
|
29
|
+
self._items.append(event)
|
|
30
|
+
else:
|
|
31
|
+
self._items.append(Event(id=event, name=event))
|
|
27
32
|
|
|
28
33
|
return self
|
|
29
34
|
|
|
30
35
|
def match(self, event: str):
|
|
31
36
|
return any(e == event for e in self)
|
|
37
|
+
|
|
38
|
+
def _replace(self, old, new):
|
|
39
|
+
self._items.remove(old)
|
|
40
|
+
self._items.append(new)
|
statemachine/exceptions.py
CHANGED
|
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
|
|
|
3
3
|
from .i18n import _
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
|
+
from .event import Event
|
|
6
7
|
from .state import State
|
|
7
8
|
|
|
8
9
|
|
|
@@ -31,8 +32,8 @@ class AttrNotFound(InvalidDefinition):
|
|
|
31
32
|
class TransitionNotAllowed(StateMachineError):
|
|
32
33
|
"Raised when there's no transition that can run from the current :ref:`state`."
|
|
33
34
|
|
|
34
|
-
def __init__(self, event:
|
|
35
|
+
def __init__(self, event: "Event", state: "State"):
|
|
35
36
|
self.event = event
|
|
36
37
|
self.state = state
|
|
37
|
-
msg = _("Can't {} when in {}.").format(self.event, self.state.name)
|
|
38
|
+
msg = _("Can't {} when in {}.").format(self.event.name, self.state.name)
|
|
38
39
|
super().__init__(msg)
|
statemachine/factory.py
CHANGED
|
@@ -8,7 +8,6 @@ from uuid import uuid4
|
|
|
8
8
|
|
|
9
9
|
from . import registry
|
|
10
10
|
from .event import Event
|
|
11
|
-
from .event import trigger_event_factory
|
|
12
11
|
from .exceptions import InvalidDefinition
|
|
13
12
|
from .graph import iterate_states_and_transitions
|
|
14
13
|
from .graph import visit_connected_states
|
|
@@ -38,11 +37,13 @@ class StateMachineMetaclass(type):
|
|
|
38
37
|
|
|
39
38
|
cls._abstract = True
|
|
40
39
|
cls._strict_states = strict_states
|
|
41
|
-
cls._events: Dict[
|
|
40
|
+
cls._events: Dict[Event, None] = {} # used Dict to preserve order and avoid duplicates
|
|
42
41
|
cls._protected_attrs: set = set()
|
|
42
|
+
cls._events_to_update: Dict[Event, Event | None] = {}
|
|
43
43
|
|
|
44
44
|
cls.add_inherited(bases)
|
|
45
45
|
cls.add_from_attributes(attrs)
|
|
46
|
+
cls._update_event_references()
|
|
46
47
|
|
|
47
48
|
try:
|
|
48
49
|
cls.initial_state: State = next(s for s in cls.states if s.initial)
|
|
@@ -174,17 +175,26 @@ class StateMachineMetaclass(type):
|
|
|
174
175
|
cls.add_state(state.id, state)
|
|
175
176
|
|
|
176
177
|
events = getattr(base, "_events", {})
|
|
177
|
-
for event in events
|
|
178
|
-
cls.add_event(event.name)
|
|
178
|
+
for event in events:
|
|
179
|
+
cls.add_event(event=Event(id=event.id, name=event.name))
|
|
179
180
|
|
|
180
|
-
def add_from_attributes(cls, attrs):
|
|
181
|
+
def add_from_attributes(cls, attrs): # noqa: C901
|
|
181
182
|
for key, value in sorted(attrs.items(), key=lambda pair: pair[0]):
|
|
182
183
|
if isinstance(value, States):
|
|
183
184
|
cls._add_states_from_dict(value)
|
|
184
185
|
if isinstance(value, State):
|
|
185
186
|
cls.add_state(key, value)
|
|
186
187
|
elif isinstance(value, (Transition, TransitionList)):
|
|
187
|
-
cls.add_event(key,
|
|
188
|
+
cls.add_event(event=Event(transitions=value, id=key, name=key))
|
|
189
|
+
elif isinstance(value, (Event,)):
|
|
190
|
+
cls.add_event(
|
|
191
|
+
event=Event(
|
|
192
|
+
transitions=value._transitions,
|
|
193
|
+
id=key,
|
|
194
|
+
name=value.name,
|
|
195
|
+
),
|
|
196
|
+
old_event=value,
|
|
197
|
+
)
|
|
188
198
|
elif getattr(value, "_specs_to_update", None):
|
|
189
199
|
cls._add_unbounded_callback(key, value)
|
|
190
200
|
|
|
@@ -196,7 +206,7 @@ class StateMachineMetaclass(type):
|
|
|
196
206
|
# if func is an event, the `attr_name` will be replaced by an event trigger,
|
|
197
207
|
# so we'll also give the ``func`` a new unique name to be used by the callback
|
|
198
208
|
# machinery.
|
|
199
|
-
cls.add_event(attr_name,
|
|
209
|
+
cls.add_event(event=Event(func._transitions, id=attr_name, name=attr_name))
|
|
200
210
|
attr_name = f"_{attr_name}_{uuid4().hex}"
|
|
201
211
|
setattr(cls, attr_name, func)
|
|
202
212
|
|
|
@@ -214,17 +224,42 @@ class StateMachineMetaclass(type):
|
|
|
214
224
|
for event in state.transitions.unique_events:
|
|
215
225
|
cls.add_event(event)
|
|
216
226
|
|
|
217
|
-
def add_event(
|
|
227
|
+
def add_event(
|
|
228
|
+
cls,
|
|
229
|
+
event: Event,
|
|
230
|
+
old_event: "Event | None" = None,
|
|
231
|
+
):
|
|
232
|
+
if not event._has_real_id:
|
|
233
|
+
if event not in cls._events_to_update:
|
|
234
|
+
cls._events_to_update[event] = None
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
transitions = event._transitions
|
|
218
238
|
if transitions is not None:
|
|
219
239
|
transitions.add_event(event)
|
|
220
240
|
|
|
221
241
|
if event not in cls._events:
|
|
222
|
-
|
|
223
|
-
cls.
|
|
224
|
-
|
|
242
|
+
cls._events[event] = None
|
|
243
|
+
setattr(cls, event.id, event)
|
|
244
|
+
|
|
245
|
+
if old_event is not None:
|
|
246
|
+
cls._events_to_update[old_event] = event
|
|
225
247
|
|
|
226
248
|
return cls._events[event]
|
|
227
249
|
|
|
250
|
+
def _update_event_references(cls):
|
|
251
|
+
for old_event, new_event in cls._events_to_update.items():
|
|
252
|
+
for state in cls.states:
|
|
253
|
+
for transition in state.transitions:
|
|
254
|
+
if transition._events.match(old_event):
|
|
255
|
+
if new_event is None:
|
|
256
|
+
raise InvalidDefinition(
|
|
257
|
+
_("An event in the '{}' has no id.").format(transition)
|
|
258
|
+
)
|
|
259
|
+
transition.events._replace(old_event, new_event)
|
|
260
|
+
|
|
261
|
+
cls._events_to_update = {}
|
|
262
|
+
|
|
228
263
|
@property
|
|
229
264
|
def events(self):
|
|
230
|
-
return list(self._events
|
|
265
|
+
return list(self._events)
|