python-statemachine 2.2.0__py3-none-any.whl → 2.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_statemachine-2.2.0.dist-info → python_statemachine-2.3.0.dist-info}/METADATA +88 -27
- python_statemachine-2.3.0.dist-info/RECORD +28 -0
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +14 -9
- statemachine/dispatcher.py +18 -15
- statemachine/event.py +21 -20
- statemachine/exceptions.py +9 -3
- statemachine/factory.py +1 -2
- statemachine/locale/en/LC_MESSAGES/statemachine.po +40 -26
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +60 -39
- statemachine/signature.py +13 -5
- statemachine/statemachine.py +78 -25
- statemachine/transition.py +4 -4
- statemachine/utils.py +15 -0
- python_statemachine-2.2.0.dist-info/RECORD +0 -28
- {python_statemachine-2.2.0.dist-info → python_statemachine-2.3.0.dist-info}/LICENSE +0 -0
- {python_statemachine-2.2.0.dist-info → python_statemachine-2.3.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.3.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,18 @@ 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.7
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
12
13
|
Classifier: Intended Audience :: Developers
|
|
13
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
15
|
Classifier: Natural Language :: English
|
|
15
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
19
|
Classifier: Programming Language :: Python :: 3.9
|
|
17
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
22
23
|
Classifier: Topic :: Software Development :: Libraries
|
|
23
24
|
Provides-Extra: diagrams
|
|
24
25
|
Description-Content-Type: text/markdown
|
|
@@ -26,8 +27,8 @@ Description-Content-Type: text/markdown
|
|
|
26
27
|
# Python StateMachine
|
|
27
28
|
|
|
28
29
|
[](https://pypi.python.org/pypi/python-statemachine)
|
|
30
|
+
[](https://pepy.tech/project/python-statemachine)
|
|
29
31
|
[](https://pypi.python.org/pypi/python-statemachine)
|
|
30
|
-
[](https://github.com/fgmacedo/python-statemachine/actions/workflows/python-package.yml?query=branch%3Adevelop)
|
|
31
32
|
[](https://codecov.io/gh/fgmacedo/python-statemachine)
|
|
32
33
|
[](https://python-statemachine.readthedocs.io/en/latest/?badge=latest)
|
|
33
34
|
[](https://github.com/fgmacedo/python-statemachine/compare/main...develop)
|
|
@@ -35,33 +36,43 @@ Description-Content-Type: text/markdown
|
|
|
35
36
|
|
|
36
37
|
Python [finite-state machines](https://en.wikipedia.org/wiki/Finite-state_machine) made easy.
|
|
37
38
|
|
|
39
|
+
<div align="center">
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
* Documentation: https://python-statemachine.readthedocs.io.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Welcome to python-statemachine, an intuitive and powerful state machine framework designed for a
|
|
44
|
-
great developer experience.
|
|
45
|
-
|
|
46
|
-
🚀 With StateMachine, you can easily create complex, dynamic systems with clean, readable code.
|
|
41
|
+

|
|
47
42
|
|
|
48
|
-
|
|
49
|
-
transitions in your system, so you can focus on building great products.
|
|
43
|
+
</div>
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
Welcome to python-statemachine, an intuitive and powerful state machine library designed for a
|
|
46
|
+
great developer experience. We provide an _pythonic_ and expressive API for implementing state
|
|
47
|
+
machines in sync or asynchonous Python codebases.
|
|
53
48
|
|
|
49
|
+
## Features
|
|
54
50
|
|
|
55
|
-
|
|
51
|
+
- ✨ **Basic components**: Easily define **States**, **Events**, and **Transitions** to model your logic.
|
|
52
|
+
- ⚙️ **Actions and handlers**: Attach actions and handlers to states, events, and transitions to control behavior dynamically.
|
|
53
|
+
- 🛡️ **Conditional transitions**: Implement **Guards** and **Validators** to conditionally control transitions, ensuring they only occur when specific conditions are met.
|
|
54
|
+
- 🚀 **Full async support**: Enjoy full asynchronous support. Await events, and dispatch callbacks asynchronously for seamless integration with async codebases.
|
|
55
|
+
- 🔄 **Full sync support**: Use the same state machine from synchronous codebases without any modifications.
|
|
56
|
+
- 🎨 **Declarative and simple API**: Utilize a clean, elegant, and readable API to define your state machine, making it easy to maintain and understand.
|
|
57
|
+
- 👀 **Observer pattern support**: Register external and generic objects to watch events and register callbacks.
|
|
58
|
+
- 🔍 **Decoupled design**: Separate concerns with a decoupled "state machine" and "model" design, promoting cleaner architecture and easier maintenance.
|
|
59
|
+
- ✅ **Correctness guarantees**: Ensured correctness with validations at class definition time:
|
|
60
|
+
- Ensures exactly one `initial` state.
|
|
61
|
+
- Disallows transitions from `final` states.
|
|
62
|
+
- Requires ongoing transitions for all non-final states.
|
|
63
|
+
- Guarantees all non-final states have at least one path to a final state if final states are declared.
|
|
64
|
+
- Validates the state machine graph representation has a single component.
|
|
65
|
+
- 📦 **Flexible event dispatching**: Dispatch events with any extra data, making it available to all callbacks, including actions and guards.
|
|
66
|
+
- 🔧 **Dependency injection**: Needed parameters are injected into callbacks.
|
|
67
|
+
- 📊 **Graphical representation**: Generate and output graphical representations of state machines. Create diagrams from the command line, at runtime, or even in Jupyter notebooks.
|
|
68
|
+
- 🌍 **Internationalization support**: Provides error messages in different languages, making the library accessible to a global audience.
|
|
69
|
+
- 🛡️ **Robust testing**: Ensured reliability with a codebase that is 100% covered by automated tests, including all docs examples. Releases follow semantic versioning for predictable releases.
|
|
70
|
+
- 🏛️ **Domain model integration**: Seamlessly integrate with domain models using Mixins.
|
|
71
|
+
- 🔧 **Django integration**: Automatically discover state machines in Django applications.
|
|
56
72
|
|
|
57
|
-
* 📈 python-statemachine is designed to help you build scalable,
|
|
58
|
-
maintainable systems that can handle any complexity.
|
|
59
|
-
* 💪 You can easily create and manage multiple state machines within a single application.
|
|
60
|
-
* 🚫 Prevents common mistakes and ensures that your system stays in a valid state at all times.
|
|
61
73
|
|
|
62
74
|
|
|
63
|
-
##
|
|
64
|
-
|
|
75
|
+
## Installing
|
|
65
76
|
|
|
66
77
|
To install Python State Machine, run this command in your terminal:
|
|
67
78
|
|
|
@@ -73,6 +84,8 @@ our docs for more details.
|
|
|
73
84
|
|
|
74
85
|
pip install python-statemachine[diagrams]
|
|
75
86
|
|
|
87
|
+
## First example
|
|
88
|
+
|
|
76
89
|
Define your state machine:
|
|
77
90
|
|
|
78
91
|
```py
|
|
@@ -90,7 +103,7 @@ Define your state machine:
|
|
|
90
103
|
... | red.to(green)
|
|
91
104
|
... )
|
|
92
105
|
...
|
|
93
|
-
... def before_cycle(self, event: str, source: State, target: State, message: str = ""):
|
|
106
|
+
... async def before_cycle(self, event: str, source: State, target: State, message: str = ""):
|
|
94
107
|
... message = ". " + message if message else ""
|
|
95
108
|
... return f"Running {event} from {source.id} to {target.id}{message}"
|
|
96
109
|
...
|
|
@@ -133,7 +146,27 @@ Then start sending events to your new state machine:
|
|
|
133
146
|
|
|
134
147
|
```
|
|
135
148
|
|
|
136
|
-
|
|
149
|
+
You can use the exactly same state machine from an async codebase:
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
```py
|
|
153
|
+
>>> async def run_sm():
|
|
154
|
+
... asm = TrafficLightMachine()
|
|
155
|
+
... results = []
|
|
156
|
+
... for _i in range(4):
|
|
157
|
+
... result = await asm.send("cycle")
|
|
158
|
+
... results.append(result)
|
|
159
|
+
... return results
|
|
160
|
+
|
|
161
|
+
>>> asyncio.run(run_sm())
|
|
162
|
+
Don't move.
|
|
163
|
+
Go ahead!
|
|
164
|
+
['Running cycle from green to yellow', 'Running cycle from yellow to red', ...
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
**That's it.** This is all an external object needs to know about your state machine: How to send events.
|
|
137
170
|
Ideally, all states, transitions, and actions should be kept internally and not checked externally to avoid unnecessary coupling.
|
|
138
171
|
|
|
139
172
|
But if your use case needs, you can inspect state machine properties, like the current state:
|
|
@@ -220,7 +253,7 @@ callback method.
|
|
|
220
253
|
Note how `before_cycle` was declared:
|
|
221
254
|
|
|
222
255
|
```py
|
|
223
|
-
def before_cycle(self, event: str, source: State, target: State, message: str = ""):
|
|
256
|
+
async def before_cycle(self, event: str, source: State, target: State, message: str = ""):
|
|
224
257
|
message = ". " + message if message else ""
|
|
225
258
|
return f"Running {event} from {source.id} to {target.id}{message}"
|
|
226
259
|
```
|
|
@@ -265,6 +298,34 @@ and in diagrams:
|
|
|
265
298
|
|
|
266
299
|
```
|
|
267
300
|
|
|
301
|
+
## Async support
|
|
302
|
+
|
|
303
|
+
We support native coroutine using `asyncio`, enabling seamless integration with asynchronous code.
|
|
304
|
+
There's no change on the public API of the library to work on async codebases.
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
```py
|
|
308
|
+
>>> class AsyncStateMachine(StateMachine):
|
|
309
|
+
... initial = State('Initial', initial=True)
|
|
310
|
+
... final = State('Final', final=True)
|
|
311
|
+
...
|
|
312
|
+
... advance = initial.to(final)
|
|
313
|
+
...
|
|
314
|
+
... async def on_advance(self):
|
|
315
|
+
... return 42
|
|
316
|
+
|
|
317
|
+
>>> async def run_sm():
|
|
318
|
+
... sm = AsyncStateMachine()
|
|
319
|
+
... result = await sm.advance()
|
|
320
|
+
... print(f"Result is {result}")
|
|
321
|
+
... print(sm.current_state)
|
|
322
|
+
|
|
323
|
+
>>> asyncio.run(run_sm())
|
|
324
|
+
Result is 42
|
|
325
|
+
Final
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
|
|
268
329
|
## A more useful example
|
|
269
330
|
|
|
270
331
|
A simple didactic state machine for controlling an `Order`:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
statemachine/__init__.py,sha256=Omi4-IXrTE0oLfO3oF75OqMYBI9TrbZeBzZIkt-F0rU,192
|
|
2
|
+
statemachine/callbacks.py,sha256=D1mb3Ky18S1jk0-ursuJljqDv63WZShnMvMK_S1Hi9k,8846
|
|
3
|
+
statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
statemachine/contrib/diagram.py,sha256=CTlNRtAHEzCCzRUlxkiyzJOCOp1CohihdD0_7pHrx4c,7004
|
|
5
|
+
statemachine/dispatcher.py,sha256=Zizr_Qaj2BfuHu5VbbnNh1giT_pUVLe4mekHj6Ca2dk,5075
|
|
6
|
+
statemachine/event.py,sha256=lfJUSvkDQTnk1COueuV--xQD4o6GeaI94Y8tTOmiS6s,2304
|
|
7
|
+
statemachine/event_data.py,sha256=nxOfypngK-78A77gj4pS-oxLx9zl1S9wnSt_rTiCyZA,2257
|
|
8
|
+
statemachine/events.py,sha256=rUbiTX-QGoo7zzmQMEeWeLrIVnp9zhicdRIm34CoAVI,731
|
|
9
|
+
statemachine/exceptions.py,sha256=RWq1vTjajxt8CzfYCD4DfI2nrgkfCPBL0DjRDOzKkQI,1086
|
|
10
|
+
statemachine/factory.py,sha256=pAfRUuO474FekxrUZkKr8NkQqQ59vOu4dRHSD8Dr9rI,7982
|
|
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=zJYaV2DxrHdPngDOt1bji8-lPOZMSDjWG83Ypru5DOI,2126
|
|
14
|
+
statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=SPrt-KaCBCKQq7PoOMtyyqU1bihrw_3MPL_NbqZzTVs,3214
|
|
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=aGoKxC36mTO60GGICVlyduhhB2nKVOXOOHepcBG2Uws,7809
|
|
19
|
+
statemachine/state.py,sha256=bZki0DyemZa-aVAfollQCmpXxesWONzUmE28eDfqz3o,8315
|
|
20
|
+
statemachine/statemachine.py,sha256=5r8vtbMFQtOq9W8UmaWBN_SIzOHkO-4JfPysHfdoB8w,13827
|
|
21
|
+
statemachine/states.py,sha256=gdGemJYF9k-cifs9Tk0Pe_-1u1Lanf3l08o0t8wAMgg,4203
|
|
22
|
+
statemachine/transition.py,sha256=fo6B-XlHDqU8TNrDXBSn4GStdGUrZbcfbEgtsdESmrk,5412
|
|
23
|
+
statemachine/transition_list.py,sha256=DatsmMWgK0YK30Nrj-josVvlTgeGapKutzYur9-puF8,5949
|
|
24
|
+
statemachine/utils.py,sha256=FVtvT1lBSP3mRrYM-wxsX2pV_NZwSDsoBW4syIpACeE,798
|
|
25
|
+
python_statemachine-2.3.0.dist-info/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
|
|
26
|
+
python_statemachine-2.3.0.dist-info/METADATA,sha256=ubb6vnMCYaYyz486RJW5r1dKKu_emWD7Rwc4x2S7lyY,14361
|
|
27
|
+
python_statemachine-2.3.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
|
28
|
+
python_statemachine-2.3.0.dist-info/RECORD,,
|
statemachine/__init__.py
CHANGED
statemachine/callbacks.py
CHANGED
|
@@ -20,7 +20,7 @@ class CallbackPriority(IntEnum):
|
|
|
20
20
|
AFTER = 40
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def allways_true(*args, **kwargs):
|
|
23
|
+
async def allways_true(*args, **kwargs):
|
|
24
24
|
return True
|
|
25
25
|
|
|
26
26
|
|
|
@@ -46,8 +46,8 @@ class CallbackWrapper:
|
|
|
46
46
|
def __lt__(self, other):
|
|
47
47
|
return self.meta.priority < other.meta.priority
|
|
48
48
|
|
|
49
|
-
def __call__(self, *args, **kwargs):
|
|
50
|
-
return self._callback(*args, **kwargs)
|
|
49
|
+
async def __call__(self, *args, **kwargs):
|
|
50
|
+
return await self._callback(*args, **kwargs)
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
class CallbackMeta:
|
|
@@ -137,8 +137,8 @@ class BoolCallbackMeta(CallbackMeta):
|
|
|
137
137
|
return name if self.expected_value else f"!{name}"
|
|
138
138
|
|
|
139
139
|
def _wrap_callable(self, func, expected_value):
|
|
140
|
-
def bool_wrapper(*args, **kwargs):
|
|
141
|
-
return bool(func(*args, **kwargs)) == expected_value
|
|
140
|
+
async def bool_wrapper(*args, **kwargs):
|
|
141
|
+
return bool(await func(*args, **kwargs)) == expected_value
|
|
142
142
|
|
|
143
143
|
return bool_wrapper
|
|
144
144
|
|
|
@@ -248,13 +248,18 @@ class CallbacksExecutor:
|
|
|
248
248
|
self._add(item, resolver)
|
|
249
249
|
return self
|
|
250
250
|
|
|
251
|
-
def call(self, *args, **kwargs):
|
|
251
|
+
async def call(self, *args, **kwargs):
|
|
252
252
|
return [
|
|
253
|
-
|
|
253
|
+
await callback(*args, **kwargs)
|
|
254
|
+
for callback in self
|
|
255
|
+
if await callback.condition(*args, **kwargs)
|
|
254
256
|
]
|
|
255
257
|
|
|
256
|
-
def all(self, *args, **kwargs):
|
|
257
|
-
|
|
258
|
+
async def all(self, *args, **kwargs):
|
|
259
|
+
for condition in self:
|
|
260
|
+
if not await condition(*args, **kwargs):
|
|
261
|
+
return False
|
|
262
|
+
return True
|
|
258
263
|
|
|
259
264
|
|
|
260
265
|
class CallbacksRegistry:
|
statemachine/dispatcher.py
CHANGED
|
@@ -2,6 +2,7 @@ from collections import namedtuple
|
|
|
2
2
|
from operator import attrgetter
|
|
3
3
|
from typing import Any
|
|
4
4
|
from typing import Generator
|
|
5
|
+
from typing import Tuple
|
|
5
6
|
|
|
6
7
|
from .signature import SignatureAdapter
|
|
7
8
|
|
|
@@ -15,7 +16,7 @@ class ObjectConfig(namedtuple("ObjectConfig", "obj skip_attrs resolver_id")):
|
|
|
15
16
|
"""
|
|
16
17
|
|
|
17
18
|
@classmethod
|
|
18
|
-
def from_obj(cls, obj, skip_attrs=None):
|
|
19
|
+
def from_obj(cls, obj, skip_attrs=None) -> "ObjectConfig":
|
|
19
20
|
if isinstance(obj, ObjectConfig):
|
|
20
21
|
return obj
|
|
21
22
|
else:
|
|
@@ -35,11 +36,11 @@ class WrapSearchResult:
|
|
|
35
36
|
def wrap(self): # pragma: no cover
|
|
36
37
|
pass
|
|
37
38
|
|
|
38
|
-
def __call__(self, *args: Any, **kwds: Any) -> Any:
|
|
39
|
+
async def __call__(self, *args: Any, **kwds: Any) -> Any:
|
|
39
40
|
if self._cache is None:
|
|
40
41
|
self._cache = self.wrap()
|
|
41
42
|
assert self._cache
|
|
42
|
-
return self._cache(*args, **kwds)
|
|
43
|
+
return await self._cache(*args, **kwds)
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
class CallableSearchResult(WrapSearchResult):
|
|
@@ -62,7 +63,7 @@ class AttributeCallableSearchResult(WrapSearchResult):
|
|
|
62
63
|
# we'll build a method that get's the fresh value for each call
|
|
63
64
|
getter = attrgetter(self.attribute)
|
|
64
65
|
|
|
65
|
-
def wrapper(*args, **kwargs):
|
|
66
|
+
async def wrapper(*args, **kwargs):
|
|
66
67
|
return getter(self.obj)
|
|
67
68
|
|
|
68
69
|
return wrapper
|
|
@@ -76,15 +77,15 @@ class EventSearchResult(WrapSearchResult):
|
|
|
76
77
|
def wrap(self):
|
|
77
78
|
"Events already have the 'machine' parameter defined."
|
|
78
79
|
|
|
79
|
-
def wrapper(*args, **kwargs):
|
|
80
|
+
async def wrapper(*args, **kwargs):
|
|
80
81
|
kwargs.pop("machine", None)
|
|
81
|
-
return self.func(*args, **kwargs)
|
|
82
|
+
return await self.func(*args, **kwargs)
|
|
82
83
|
|
|
83
84
|
return wrapper
|
|
84
85
|
|
|
85
86
|
|
|
86
87
|
def _search_callable_attr_is_property(
|
|
87
|
-
attr, configs:
|
|
88
|
+
attr, configs: Tuple[ObjectConfig, ...]
|
|
88
89
|
) -> "WrapSearchResult | None":
|
|
89
90
|
# if the attr is a property, we'll try to find the object that has the
|
|
90
91
|
# property on the configs
|
|
@@ -96,7 +97,7 @@ def _search_callable_attr_is_property(
|
|
|
96
97
|
return None
|
|
97
98
|
|
|
98
99
|
|
|
99
|
-
def _search_callable_attr_is_callable(attr, configs:
|
|
100
|
+
def _search_callable_attr_is_callable(attr, configs: Tuple[ObjectConfig, ...]) -> WrapSearchResult:
|
|
100
101
|
# if the attr is an unbounded method, we'll try to find the bounded method
|
|
101
102
|
# on the configs
|
|
102
103
|
if not hasattr(attr, "__self__"):
|
|
@@ -109,7 +110,7 @@ def _search_callable_attr_is_callable(attr, configs: tuple[ObjectConfig]) -> Wra
|
|
|
109
110
|
|
|
110
111
|
|
|
111
112
|
def _search_callable_in_configs(
|
|
112
|
-
attr, configs:
|
|
113
|
+
attr, configs: Tuple[ObjectConfig, ...]
|
|
113
114
|
) -> Generator[WrapSearchResult, None, None]:
|
|
114
115
|
for obj, skip_attrs, resolver_id in configs:
|
|
115
116
|
if attr in skip_attrs:
|
|
@@ -128,7 +129,9 @@ def _search_callable_in_configs(
|
|
|
128
129
|
yield CallableSearchResult(attr, func, resolver_id)
|
|
129
130
|
|
|
130
131
|
|
|
131
|
-
def search_callable(
|
|
132
|
+
def search_callable(
|
|
133
|
+
attr, configs: Tuple[ObjectConfig, ...]
|
|
134
|
+
) -> Generator[WrapSearchResult, None, None]: # noqa: C901
|
|
132
135
|
if isinstance(attr, property):
|
|
133
136
|
result = _search_callable_attr_is_property(attr, configs)
|
|
134
137
|
if result is not None:
|
|
@@ -142,15 +145,15 @@ def search_callable(attr, configs: tuple) -> Generator[WrapSearchResult, None, N
|
|
|
142
145
|
yield from _search_callable_in_configs(attr, configs)
|
|
143
146
|
|
|
144
147
|
|
|
145
|
-
def resolver_factory(objects:
|
|
148
|
+
def resolver_factory(objects: Tuple[ObjectConfig, ...]):
|
|
146
149
|
"""Factory that returns a configured resolver."""
|
|
147
150
|
|
|
148
|
-
def
|
|
151
|
+
def resolver(attr) -> Generator[WrapSearchResult, None, None]:
|
|
149
152
|
yield from search_callable(attr, objects)
|
|
150
153
|
|
|
151
|
-
return
|
|
154
|
+
return resolver
|
|
152
155
|
|
|
153
156
|
|
|
154
|
-
def resolver_factory_from_objects(*objects:
|
|
155
|
-
configs = tuple(ObjectConfig.from_obj(o) for o in objects)
|
|
157
|
+
def resolver_factory_from_objects(*objects: Tuple[Any, ...]):
|
|
158
|
+
configs: Tuple[ObjectConfig, ...] = tuple(ObjectConfig.from_obj(o) for o in objects)
|
|
156
159
|
return resolver_factory(configs)
|
statemachine/event.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
from functools import partial
|
|
1
2
|
from typing import TYPE_CHECKING
|
|
2
3
|
|
|
4
|
+
from statemachine.utils import run_async_from_sync
|
|
5
|
+
|
|
3
6
|
from .event_data import EventData
|
|
4
7
|
from .event_data import TriggerData
|
|
5
8
|
from .exceptions import TransitionNotAllowed
|
|
@@ -15,28 +18,28 @@ class Event:
|
|
|
15
18
|
def __repr__(self):
|
|
16
19
|
return f"{type(self).__name__}({self.name!r})"
|
|
17
20
|
|
|
18
|
-
def trigger(self, machine: "StateMachine", *args, **kwargs):
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
)
|
|
27
|
-
return self._trigger(trigger_data)
|
|
21
|
+
async def trigger(self, machine: "StateMachine", *args, **kwargs):
|
|
22
|
+
trigger_data = TriggerData(
|
|
23
|
+
machine=machine,
|
|
24
|
+
event=self.name,
|
|
25
|
+
args=args,
|
|
26
|
+
kwargs=kwargs,
|
|
27
|
+
)
|
|
28
|
+
trigger_wrapper = partial(self._trigger, trigger_data=trigger_data)
|
|
28
29
|
|
|
29
|
-
return machine._process(trigger_wrapper)
|
|
30
|
+
return await machine._process(trigger_wrapper)
|
|
30
31
|
|
|
31
|
-
def _trigger(self, trigger_data: TriggerData):
|
|
32
|
+
async def _trigger(self, trigger_data: TriggerData):
|
|
32
33
|
event_data = None
|
|
34
|
+
await trigger_data.machine._ensure_is_initialized()
|
|
35
|
+
|
|
33
36
|
state = trigger_data.machine.current_state
|
|
34
37
|
for transition in state.transitions:
|
|
35
38
|
if not transition.match(trigger_data.event):
|
|
36
39
|
continue
|
|
37
40
|
|
|
38
41
|
event_data = EventData(trigger_data=trigger_data, transition=transition)
|
|
39
|
-
if transition.execute(event_data):
|
|
42
|
+
if await transition.execute(event_data):
|
|
40
43
|
event_data.executed = True
|
|
41
44
|
break
|
|
42
45
|
else:
|
|
@@ -46,17 +49,15 @@ class Event:
|
|
|
46
49
|
return event_data.result if event_data else None
|
|
47
50
|
|
|
48
51
|
|
|
49
|
-
def trigger_event_factory(
|
|
52
|
+
def trigger_event_factory(event_instance: Event):
|
|
50
53
|
"""Build a method that sends specific `event` to the machine"""
|
|
51
|
-
event_instance = Event(event)
|
|
52
54
|
|
|
53
55
|
def trigger_event(self, *args, **kwargs):
|
|
54
|
-
return event_instance.trigger(self, *args, **kwargs)
|
|
55
|
-
|
|
56
|
-
trigger_event.name = event
|
|
57
|
-
trigger_event.identifier = event
|
|
58
|
-
trigger_event._is_sm_event = True
|
|
56
|
+
return run_async_from_sync(event_instance.trigger(self, *args, **kwargs))
|
|
59
57
|
|
|
58
|
+
trigger_event.name = event_instance.name # type: ignore[attr-defined]
|
|
59
|
+
trigger_event.identifier = event_instance.name # type: ignore[attr-defined]
|
|
60
|
+
trigger_event._is_sm_event = True # type: ignore[attr-defined]
|
|
60
61
|
return trigger_event
|
|
61
62
|
|
|
62
63
|
|
statemachine/exceptions.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
1
3
|
from .i18n import _
|
|
2
4
|
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .state import State
|
|
7
|
+
|
|
3
8
|
|
|
4
9
|
class StateMachineError(Exception):
|
|
5
10
|
"Base exception for this project, all exceptions that can be raised inherit from this class."
|
|
@@ -12,9 +17,10 @@ class InvalidDefinition(StateMachineError):
|
|
|
12
17
|
class InvalidStateValue(InvalidDefinition):
|
|
13
18
|
"The current model state value is not mapped to a state definition."
|
|
14
19
|
|
|
15
|
-
def __init__(self, value):
|
|
20
|
+
def __init__(self, value, msg=None):
|
|
16
21
|
self.value = value
|
|
17
|
-
msg
|
|
22
|
+
if msg is None:
|
|
23
|
+
msg = _("{!r} is not a valid state value.").format(value)
|
|
18
24
|
super().__init__(msg)
|
|
19
25
|
|
|
20
26
|
|
|
@@ -25,7 +31,7 @@ class AttrNotFound(InvalidDefinition):
|
|
|
25
31
|
class TransitionNotAllowed(StateMachineError):
|
|
26
32
|
"Raised when there's no transition that can run from the current :ref:`state`."
|
|
27
33
|
|
|
28
|
-
def __init__(self, event, state):
|
|
34
|
+
def __init__(self, event: str, state: "State"):
|
|
29
35
|
self.event = event
|
|
30
36
|
self.state = state
|
|
31
37
|
msg = _("Can't {} when in {}.").format(self.event, self.state.name)
|
statemachine/factory.py
CHANGED
|
@@ -222,8 +222,7 @@ class StateMachineMetaclass(type):
|
|
|
222
222
|
if event not in cls._events:
|
|
223
223
|
event_instance = Event(event)
|
|
224
224
|
cls._events[event] = event_instance
|
|
225
|
-
|
|
226
|
-
setattr(cls, event, event_trigger)
|
|
225
|
+
setattr(cls, event, trigger_event_factory(event_instance))
|
|
227
226
|
|
|
228
227
|
return cls._events[event]
|
|
229
228
|
|
|
@@ -1,66 +1,80 @@
|
|
|
1
1
|
# This file is distributed under the same license as the PROJECT project.
|
|
2
|
-
# Fernando Macedo <fgmacedo@gmail.com>,
|
|
2
|
+
# Fernando Macedo <fgmacedo@gmail.com>, 2024.
|
|
3
3
|
#
|
|
4
4
|
msgid ""
|
|
5
5
|
msgstr ""
|
|
6
|
-
"Project-Id-Version: 2.
|
|
6
|
+
"Project-Id-Version: 2.3.0\n"
|
|
7
7
|
"Report-Msgid-Bugs-To: fgmacedo@gmail.com\n"
|
|
8
8
|
"POT-Creation-Date: 2023-03-04 16:10-0300\n"
|
|
9
|
-
"PO-Revision-Date:
|
|
9
|
+
"PO-Revision-Date: 2024-06-07 17:41-0300\n"
|
|
10
10
|
"Last-Translator: Fernando Macedo <fgmacedo@gmail.com>\n"
|
|
11
11
|
"MIME-Version: 1.0\n"
|
|
12
12
|
"Content-Type: text/plain; charset=utf-8\n"
|
|
13
13
|
"Content-Transfer-Encoding: 8bit\n"
|
|
14
14
|
"Generated-By: Babel 2.12.1\n"
|
|
15
15
|
|
|
16
|
-
#: statemachine/callbacks.py:
|
|
17
|
-
msgid "Callback {!r} not property configured."
|
|
18
|
-
msgstr ""
|
|
19
|
-
|
|
20
|
-
#: statemachine/dispatcher.py:35
|
|
16
|
+
#: statemachine/callbacks.py:289
|
|
21
17
|
msgid "Did not found name '{}' from model or statemachine"
|
|
22
18
|
msgstr ""
|
|
23
19
|
|
|
24
|
-
#: statemachine/exceptions.py:
|
|
20
|
+
#: statemachine/exceptions.py:23
|
|
25
21
|
msgid "{!r} is not a valid state value."
|
|
26
22
|
msgstr ""
|
|
27
23
|
|
|
28
|
-
#: statemachine/exceptions.py:
|
|
24
|
+
#: statemachine/exceptions.py:37
|
|
29
25
|
msgid "Can't {} when in {}."
|
|
30
26
|
msgstr ""
|
|
31
27
|
|
|
32
|
-
#: statemachine/factory.py:
|
|
33
|
-
msgid ""
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
#: statemachine/factory.py:73
|
|
29
|
+
msgid "There are no states."
|
|
30
|
+
msgstr ""
|
|
31
|
+
|
|
32
|
+
#: statemachine/factory.py:76
|
|
33
|
+
msgid "There are no events."
|
|
36
34
|
msgstr ""
|
|
37
35
|
|
|
38
|
-
#: statemachine/factory.py:
|
|
36
|
+
#: statemachine/factory.py:88
|
|
39
37
|
msgid ""
|
|
40
|
-
"There
|
|
41
|
-
"
|
|
38
|
+
"There should be one and only one initial state. You currently have these:"
|
|
39
|
+
" {!r}"
|
|
42
40
|
msgstr ""
|
|
43
41
|
|
|
44
|
-
#: statemachine/factory.py:
|
|
45
|
-
msgid "
|
|
42
|
+
#: statemachine/factory.py:101
|
|
43
|
+
msgid "Cannot declare transitions from final state. Invalid state(s): {}"
|
|
46
44
|
msgstr ""
|
|
47
45
|
|
|
48
|
-
#: statemachine/factory.py:
|
|
49
|
-
msgid "
|
|
46
|
+
#: statemachine/factory.py:109
|
|
47
|
+
msgid ""
|
|
48
|
+
"All non-final states should have at least one outgoing transition. These "
|
|
49
|
+
"states have no outgoing transition: {!r}"
|
|
50
50
|
msgstr ""
|
|
51
51
|
|
|
52
|
-
#: statemachine/factory.py:
|
|
53
|
-
msgid "
|
|
52
|
+
#: statemachine/factory.py:123
|
|
53
|
+
msgid ""
|
|
54
|
+
"All non-final states should have at least one path to a final state. "
|
|
55
|
+
"These states have no path to a final state: {!r}"
|
|
54
56
|
msgstr ""
|
|
55
57
|
|
|
56
|
-
#: statemachine/
|
|
58
|
+
#: statemachine/factory.py:147
|
|
59
|
+
msgid ""
|
|
60
|
+
"There are unreachable states. The statemachine graph should have a single"
|
|
61
|
+
" component. Disconnected states: {}"
|
|
62
|
+
msgstr ""
|
|
63
|
+
|
|
64
|
+
#: statemachine/mixins.py:23
|
|
57
65
|
msgid "{!r} is not a valid state machine name."
|
|
58
66
|
msgstr ""
|
|
59
67
|
|
|
60
|
-
#: statemachine/state.py:
|
|
68
|
+
#: statemachine/state.py:152
|
|
61
69
|
msgid "State overriding is not allowed. Trying to add '{}' to {}"
|
|
62
70
|
msgstr ""
|
|
63
71
|
|
|
64
|
-
#: statemachine/statemachine.py:
|
|
72
|
+
#: statemachine/statemachine.py:86
|
|
65
73
|
msgid "There are no states or transitions."
|
|
66
74
|
msgstr ""
|
|
75
|
+
|
|
76
|
+
#: statemachine/statemachine.py:249
|
|
77
|
+
msgid ""
|
|
78
|
+
"There's no current state set. In async code, did you activate the initial"
|
|
79
|
+
" state? (e.g., `await sm.activate_initial_state()`)"
|
|
80
|
+
msgstr ""
|
|
@@ -1,70 +1,91 @@
|
|
|
1
|
-
# This file is distributed under the same license as the
|
|
2
|
-
# Fernando Macedo <fgmacedo@gmail.com>,
|
|
1
|
+
# This file is distributed under the same license as the project.
|
|
2
|
+
# Fernando Macedo <fgmacedo@gmail.com>, 2024.
|
|
3
3
|
#
|
|
4
4
|
msgid ""
|
|
5
5
|
msgstr ""
|
|
6
|
-
"Project-Id-Version:
|
|
6
|
+
"Project-Id-Version: 2.3.0\n"
|
|
7
7
|
"Report-Msgid-Bugs-To: fgmacedo@gmail.com\n"
|
|
8
8
|
"POT-Creation-Date: 2023-03-04 16:10-0300\n"
|
|
9
|
-
"PO-Revision-Date:
|
|
9
|
+
"PO-Revision-Date: 2024-06-07 17:41-0300\n"
|
|
10
10
|
"Last-Translator: Fernando Macedo <fgmacedo@gmail.com>\n"
|
|
11
|
+
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
11
12
|
"MIME-Version: 1.0\n"
|
|
12
13
|
"Content-Type: text/plain; charset=utf-8\n"
|
|
13
14
|
"Content-Transfer-Encoding: 8bit\n"
|
|
14
|
-
"Generated-By: Babel 2.
|
|
15
|
+
"Generated-By: Babel 2.14.0\n"
|
|
15
16
|
|
|
16
|
-
#: statemachine/callbacks.py:
|
|
17
|
-
msgid "Callback {!r} not property configured."
|
|
18
|
-
msgstr "Callback {!r} não está configurado corretamente."
|
|
19
|
-
|
|
20
|
-
#: statemachine/dispatcher.py:35
|
|
17
|
+
#: statemachine/callbacks.py:289
|
|
21
18
|
msgid "Did not found name '{}' from model or statemachine"
|
|
22
|
-
msgstr "Não
|
|
19
|
+
msgstr "Não encontrou o nome '{}' no modelo ou na máquina de estados"
|
|
23
20
|
|
|
24
|
-
#: statemachine/exceptions.py:
|
|
21
|
+
#: statemachine/exceptions.py:23
|
|
25
22
|
msgid "{!r} is not a valid state value."
|
|
26
23
|
msgstr "{!r} não é um valor de estado válido."
|
|
27
24
|
|
|
28
|
-
#: statemachine/exceptions.py:
|
|
25
|
+
#: statemachine/exceptions.py:37
|
|
29
26
|
msgid "Can't {} when in {}."
|
|
30
|
-
msgstr "Não é possível {} quando em {}."
|
|
31
|
-
|
|
32
|
-
#: statemachine/factory.py:35
|
|
33
|
-
msgid ""
|
|
34
|
-
"There should be one and only one initial state. Your currently have "
|
|
35
|
-
"these: {!r}"
|
|
36
|
-
msgstr ""
|
|
37
|
-
"Deve haver um e apenas um estado inicial. Atualmente, você tem "
|
|
38
|
-
"os seguintes: {!r}"
|
|
27
|
+
msgstr "Não é possível {} quando está em {}."
|
|
39
28
|
|
|
40
|
-
#: statemachine/factory.py:
|
|
41
|
-
msgid ""
|
|
42
|
-
"There are unreachable states. The statemachine graph should have a single"
|
|
43
|
-
" component. Disconnected states: {}"
|
|
44
|
-
msgstr ""
|
|
45
|
-
"Há estados inalcançáveis. O grafo da máquina de estados deve ter apenas um"
|
|
46
|
-
" componente. Estados desconectados: {}"
|
|
47
|
-
|
|
48
|
-
#: statemachine/factory.py:69
|
|
29
|
+
#: statemachine/factory.py:73
|
|
49
30
|
msgid "There are no states."
|
|
50
31
|
msgstr "Não há estados."
|
|
51
32
|
|
|
52
|
-
#: statemachine/factory.py:
|
|
33
|
+
#: statemachine/factory.py:76
|
|
53
34
|
msgid "There are no events."
|
|
54
35
|
msgstr "Não há eventos."
|
|
55
36
|
|
|
56
|
-
#: statemachine/factory.py:
|
|
37
|
+
#: statemachine/factory.py:88
|
|
38
|
+
msgid ""
|
|
39
|
+
"There should be one and only one initial state. You currently have these:"
|
|
40
|
+
" {!r}"
|
|
41
|
+
msgstr "Deve haver um e apenas um estado inicial. Você atualmente tem estes: {!r}"
|
|
42
|
+
|
|
43
|
+
#: statemachine/factory.py:101
|
|
57
44
|
msgid "Cannot declare transitions from final state. Invalid state(s): {}"
|
|
58
|
-
msgstr "
|
|
45
|
+
msgstr ""
|
|
46
|
+
"Não é possível declarar transições a partir do estado final. Estado(s) "
|
|
47
|
+
"inválido(s): {}"
|
|
48
|
+
|
|
49
|
+
#: statemachine/factory.py:109
|
|
50
|
+
msgid ""
|
|
51
|
+
"All non-final states should have at least one outgoing transition. These "
|
|
52
|
+
"states have no outgoing transition: {!r}"
|
|
53
|
+
msgstr ""
|
|
54
|
+
"Todos os estados não finais devem ter pelo menos uma transição de saída. "
|
|
55
|
+
"Esses estados não têm transição de saída: {!r}"
|
|
59
56
|
|
|
60
|
-
#: statemachine/
|
|
57
|
+
#: statemachine/factory.py:123
|
|
58
|
+
msgid ""
|
|
59
|
+
"All non-final states should have at least one path to a final state. "
|
|
60
|
+
"These states have no path to a final state: {!r}"
|
|
61
|
+
msgstr ""
|
|
62
|
+
"Todos os estados não finais devem ter pelo menos um caminho para um "
|
|
63
|
+
"estado final. Esses estados não têm caminho para um estado final: {!r}"
|
|
64
|
+
|
|
65
|
+
#: statemachine/factory.py:147
|
|
66
|
+
msgid ""
|
|
67
|
+
"There are unreachable states. The statemachine graph should have a single"
|
|
68
|
+
" component. Disconnected states: {}"
|
|
69
|
+
msgstr ""
|
|
70
|
+
"Há estados inacessíveis. O gráfico da máquina de estados deve ter um "
|
|
71
|
+
"único componente. Estados desconectados: {}"
|
|
72
|
+
|
|
73
|
+
#: statemachine/mixins.py:23
|
|
61
74
|
msgid "{!r} is not a valid state machine name."
|
|
62
|
-
msgstr "{!r} não é um nome válido para
|
|
75
|
+
msgstr "{!r} não é um nome válido para uma máquina de estados."
|
|
63
76
|
|
|
64
|
-
#: statemachine/state.py:
|
|
77
|
+
#: statemachine/state.py:152
|
|
65
78
|
msgid "State overriding is not allowed. Trying to add '{}' to {}"
|
|
66
|
-
msgstr "
|
|
79
|
+
msgstr "Sobrescrever estados não é permitido. Tentando adicionar '{}' a {}"
|
|
67
80
|
|
|
68
|
-
#: statemachine/statemachine.py:
|
|
81
|
+
#: statemachine/statemachine.py:86
|
|
69
82
|
msgid "There are no states or transitions."
|
|
70
83
|
msgstr "Não há estados ou transições."
|
|
84
|
+
|
|
85
|
+
#: statemachine/statemachine.py:249
|
|
86
|
+
msgid ""
|
|
87
|
+
"There's no current state set. In async code, did you activate the initial"
|
|
88
|
+
" state? (e.g., `await sm.activate_initial_state()`)"
|
|
89
|
+
msgstr ""
|
|
90
|
+
"Não há estado atual definido. No código assíncrono, você ativou o estado"
|
|
91
|
+
" inicial? (por exemplo, `await sm.activate_initial_state()`)"
|
statemachine/signature.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import itertools
|
|
2
1
|
from functools import partial
|
|
3
2
|
from inspect import BoundArguments
|
|
4
3
|
from inspect import Parameter
|
|
5
4
|
from inspect import Signature
|
|
5
|
+
from inspect import iscoroutinefunction
|
|
6
|
+
from itertools import chain
|
|
6
7
|
from types import MethodType
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
@@ -51,9 +52,16 @@ class SignatureAdapter(Signature):
|
|
|
51
52
|
|
|
52
53
|
metadata_to_copy = method.func if isinstance(method, partial) else method
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
if iscoroutinefunction(method):
|
|
56
|
+
|
|
57
|
+
async def method_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
58
|
+
ba = sig_bind_expected(*args, **kwargs)
|
|
59
|
+
return await method(*ba.args, **ba.kwargs)
|
|
60
|
+
else:
|
|
61
|
+
|
|
62
|
+
async def method_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
63
|
+
ba = sig_bind_expected(*args, **kwargs)
|
|
64
|
+
return method(*ba.args, **ba.kwargs)
|
|
57
65
|
|
|
58
66
|
method_wrapper.__name__ = metadata_to_copy.__name__
|
|
59
67
|
|
|
@@ -160,7 +168,7 @@ class SignatureAdapter(Signature):
|
|
|
160
168
|
|
|
161
169
|
# Now, we iterate through the remaining parameters to process
|
|
162
170
|
# keyword arguments
|
|
163
|
-
for param in
|
|
171
|
+
for param in chain(parameters_ex, parameters):
|
|
164
172
|
if param.kind == Parameter.VAR_KEYWORD:
|
|
165
173
|
# Memorize that we have a '**kwargs'-like parameter
|
|
166
174
|
kwargs_param = param
|
statemachine/statemachine.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import Any
|
|
|
6
6
|
from typing import Dict
|
|
7
7
|
|
|
8
8
|
from statemachine.graph import iterate_states_and_transitions
|
|
9
|
+
from statemachine.utils import run_async_from_sync
|
|
9
10
|
|
|
10
11
|
from .callbacks import CallbackMetaList
|
|
11
12
|
from .callbacks import CallbacksExecutor
|
|
@@ -73,18 +74,23 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
73
74
|
self.state_field = state_field
|
|
74
75
|
self.start_value = start_value
|
|
75
76
|
self.allow_event_without_transition = allow_event_without_transition
|
|
77
|
+
self.__initialized = False
|
|
76
78
|
self.__rtc = rtc
|
|
77
79
|
self.__processing: bool = False
|
|
78
80
|
self._external_queue: deque = deque()
|
|
79
81
|
self._callbacks_registry = CallbacksRegistry()
|
|
80
|
-
self._states_for_instance: Dict[
|
|
82
|
+
self._states_for_instance: Dict[State, State] = {}
|
|
81
83
|
self._observers: Dict[Any, Any] = {}
|
|
82
84
|
|
|
83
85
|
if self._abstract:
|
|
84
86
|
raise InvalidDefinition(_("There are no states or transitions."))
|
|
85
87
|
|
|
86
88
|
self._register_callbacks()
|
|
87
|
-
|
|
89
|
+
|
|
90
|
+
# Activate the initial state, this only works if the outer scope is sync code.
|
|
91
|
+
# for async code, the user should manually call `await sm.activate_initial_state()`
|
|
92
|
+
# after state machine creation.
|
|
93
|
+
run_async_from_sync(self.activate_initial_state())
|
|
88
94
|
|
|
89
95
|
def __init_subclass__(cls, strict_states: bool = False):
|
|
90
96
|
cls._strict_states = strict_states
|
|
@@ -122,7 +128,23 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
122
128
|
except KeyError as err:
|
|
123
129
|
raise InvalidStateValue(current_state_value) from err
|
|
124
130
|
|
|
125
|
-
def
|
|
131
|
+
async def activate_initial_state(self):
|
|
132
|
+
"""
|
|
133
|
+
Activate the initial state.
|
|
134
|
+
|
|
135
|
+
Called automatically on state machine creation from sync code, but in
|
|
136
|
+
async code, the user must call this method explicitly.
|
|
137
|
+
|
|
138
|
+
Given how async works on python, there's no built-in way to activate the initial state that
|
|
139
|
+
may depend on async code from the StateMachine.__init__ method.
|
|
140
|
+
|
|
141
|
+
We do a `_ensure_is_initialized()` check before each event, but to check the current state
|
|
142
|
+
just before the state machine is created, the user must await the activation of the initial
|
|
143
|
+
state explicitly.
|
|
144
|
+
"""
|
|
145
|
+
if self.__initialized:
|
|
146
|
+
return
|
|
147
|
+
self.__initialized = True
|
|
126
148
|
if self.current_state_value is None:
|
|
127
149
|
# send an one-time event `__initial__` to enter the current state.
|
|
128
150
|
# current_state = self.current_state
|
|
@@ -138,7 +160,10 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
138
160
|
),
|
|
139
161
|
transition=initial_transition,
|
|
140
162
|
)
|
|
141
|
-
self._activate(event_data)
|
|
163
|
+
await self._activate(event_data)
|
|
164
|
+
|
|
165
|
+
async def _ensure_is_initialized(self):
|
|
166
|
+
await self.activate_initial_state()
|
|
142
167
|
|
|
143
168
|
def _register_callbacks(self):
|
|
144
169
|
self._add_observer(
|
|
@@ -195,8 +220,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
195
220
|
This is a low level API, that can be used to assign any valid state value
|
|
196
221
|
completely bypassing all the hooks and validations.
|
|
197
222
|
"""
|
|
198
|
-
|
|
199
|
-
return value
|
|
223
|
+
return getattr(self.model, self.state_field, None)
|
|
200
224
|
|
|
201
225
|
@current_state_value.setter
|
|
202
226
|
def current_state_value(self, value):
|
|
@@ -212,11 +236,23 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
212
236
|
completely bypassing all the hooks and validations.
|
|
213
237
|
"""
|
|
214
238
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
239
|
+
try:
|
|
240
|
+
state: State = self.states_map[self.current_state_value].for_instance(
|
|
241
|
+
machine=self,
|
|
242
|
+
cache=self._states_for_instance,
|
|
243
|
+
)
|
|
244
|
+
return state
|
|
245
|
+
except KeyError as err:
|
|
246
|
+
if self.current_state_value is None:
|
|
247
|
+
raise InvalidStateValue(
|
|
248
|
+
self.current_state_value,
|
|
249
|
+
_(
|
|
250
|
+
"There's no current state set. In async code, "
|
|
251
|
+
"did you activate the initial state? "
|
|
252
|
+
"(e.g., `await sm.activate_initial_state()`)"
|
|
253
|
+
),
|
|
254
|
+
) from err
|
|
255
|
+
raise InvalidStateValue(self.current_state_value) from err
|
|
220
256
|
|
|
221
257
|
@current_state.setter
|
|
222
258
|
def current_state(self, value):
|
|
@@ -231,7 +267,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
231
267
|
"""List of the current allowed events."""
|
|
232
268
|
return [getattr(self, event) for event in self.current_state.transitions.unique_events]
|
|
233
269
|
|
|
234
|
-
def _process(self, trigger):
|
|
270
|
+
async def _process(self, trigger):
|
|
235
271
|
"""Process event triggers.
|
|
236
272
|
|
|
237
273
|
The simplest implementation is the non-RTC (synchronous),
|
|
@@ -252,7 +288,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
252
288
|
"""
|
|
253
289
|
if not self.__rtc:
|
|
254
290
|
# The machine is in "synchronous" mode
|
|
255
|
-
return trigger()
|
|
291
|
+
return await trigger()
|
|
256
292
|
|
|
257
293
|
# The machine is in "queued" mode
|
|
258
294
|
# Add the trigger to queue and start processing in a loop.
|
|
@@ -263,9 +299,9 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
263
299
|
if self.__processing:
|
|
264
300
|
return
|
|
265
301
|
|
|
266
|
-
return self._processing_loop()
|
|
302
|
+
return await self._processing_loop()
|
|
267
303
|
|
|
268
|
-
def _processing_loop(self):
|
|
304
|
+
async def _processing_loop(self):
|
|
269
305
|
"""Execute the triggers in the queue in order until the queue is empty"""
|
|
270
306
|
self.__processing = True
|
|
271
307
|
|
|
@@ -278,7 +314,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
278
314
|
while self._external_queue:
|
|
279
315
|
trigger = self._external_queue.popleft()
|
|
280
316
|
try:
|
|
281
|
-
result = trigger()
|
|
317
|
+
result = await trigger()
|
|
282
318
|
if first_result is sentinel:
|
|
283
319
|
first_result = result
|
|
284
320
|
except Exception:
|
|
@@ -290,18 +326,18 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
290
326
|
self.__processing = False
|
|
291
327
|
return first_result if first_result is not sentinel else None
|
|
292
328
|
|
|
293
|
-
def _activate(self, event_data: EventData):
|
|
329
|
+
async def _activate(self, event_data: EventData):
|
|
294
330
|
transition = event_data.transition
|
|
295
331
|
source = event_data.state
|
|
296
332
|
target = transition.target
|
|
297
333
|
|
|
298
|
-
result = self._callbacks(transition.before).call(
|
|
334
|
+
result = await self._callbacks(transition.before).call(
|
|
299
335
|
*event_data.args, **event_data.extended_kwargs
|
|
300
336
|
)
|
|
301
337
|
if source is not None and not transition.internal:
|
|
302
|
-
self._callbacks(source.exit).call(*event_data.args, **event_data.extended_kwargs)
|
|
338
|
+
await self._callbacks(source.exit).call(*event_data.args, **event_data.extended_kwargs)
|
|
303
339
|
|
|
304
|
-
result += self._callbacks(transition.on).call(
|
|
340
|
+
result += await self._callbacks(transition.on).call(
|
|
305
341
|
*event_data.args, **event_data.extended_kwargs
|
|
306
342
|
)
|
|
307
343
|
|
|
@@ -309,8 +345,12 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
309
345
|
event_data.state = target
|
|
310
346
|
|
|
311
347
|
if not transition.internal:
|
|
312
|
-
self._callbacks(target.enter).call(
|
|
313
|
-
|
|
348
|
+
await self._callbacks(target.enter).call(
|
|
349
|
+
*event_data.args, **event_data.extended_kwargs
|
|
350
|
+
)
|
|
351
|
+
await self._callbacks(transition.after).call(
|
|
352
|
+
*event_data.args, **event_data.extended_kwargs
|
|
353
|
+
)
|
|
314
354
|
|
|
315
355
|
if len(result) == 0:
|
|
316
356
|
result = None
|
|
@@ -319,7 +359,20 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
319
359
|
|
|
320
360
|
return result
|
|
321
361
|
|
|
322
|
-
def send(self, event, *args, **kwargs):
|
|
362
|
+
def send(self, event: str, *args, **kwargs):
|
|
363
|
+
"""Send an :ref:`Event` to the state machine.
|
|
364
|
+
|
|
365
|
+
This is a thin wrapper around :meth:`async_send` to allow synchronous
|
|
366
|
+
code to send events.
|
|
367
|
+
|
|
368
|
+
.. seealso::
|
|
369
|
+
|
|
370
|
+
See: :ref:`triggering events`.
|
|
371
|
+
|
|
372
|
+
"""
|
|
373
|
+
return run_async_from_sync(self.async_send(event, *args, **kwargs))
|
|
374
|
+
|
|
375
|
+
async def async_send(self, event: str, *args, **kwargs):
|
|
323
376
|
"""Send an :ref:`Event` to the state machine.
|
|
324
377
|
|
|
325
378
|
.. seealso::
|
|
@@ -327,8 +380,8 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
327
380
|
See: :ref:`triggering events`.
|
|
328
381
|
|
|
329
382
|
"""
|
|
330
|
-
|
|
331
|
-
return
|
|
383
|
+
event_instance: Event = Event(event)
|
|
384
|
+
return await event_instance.trigger(self, *args, **kwargs)
|
|
332
385
|
|
|
333
386
|
def _callbacks(self, meta_list: CallbackMetaList) -> CallbacksExecutor:
|
|
334
387
|
return self._callbacks_registry[meta_list]
|
statemachine/transition.py
CHANGED
|
@@ -139,13 +139,13 @@ class Transition:
|
|
|
139
139
|
def add_event(self, value):
|
|
140
140
|
self._events.add(value)
|
|
141
141
|
|
|
142
|
-
def execute(self, event_data: "EventData"):
|
|
142
|
+
async def execute(self, event_data: "EventData"):
|
|
143
143
|
machine = event_data.machine
|
|
144
144
|
args, kwargs = event_data.args, event_data.extended_kwargs
|
|
145
|
-
machine._callbacks_registry[self.validators].call(*args, **kwargs)
|
|
146
|
-
if not machine._callbacks_registry[self.cond].all(*args, **kwargs):
|
|
145
|
+
await machine._callbacks_registry[self.validators].call(*args, **kwargs)
|
|
146
|
+
if not await machine._callbacks_registry[self.cond].all(*args, **kwargs):
|
|
147
147
|
return False
|
|
148
148
|
|
|
149
|
-
result = machine._activate(event_data)
|
|
149
|
+
result = await machine._activate(event_data)
|
|
150
150
|
event_data.result = result
|
|
151
151
|
return True
|
statemachine/utils.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
def qualname(cls):
|
|
2
5
|
"""
|
|
3
6
|
Returns a fully qualified name of the class, to avoid name collisions.
|
|
@@ -16,3 +19,15 @@ def ensure_iterable(obj):
|
|
|
16
19
|
return iter(obj)
|
|
17
20
|
except TypeError:
|
|
18
21
|
return [obj]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_async_from_sync(coroutine):
|
|
25
|
+
"""
|
|
26
|
+
Run an async coroutine from a synchronous context.
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
loop = asyncio.get_running_loop()
|
|
30
|
+
return asyncio.ensure_future(coroutine)
|
|
31
|
+
except RuntimeError:
|
|
32
|
+
loop = asyncio.get_event_loop()
|
|
33
|
+
return loop.run_until_complete(coroutine)
|
|
@@ -1,28 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|