python-statemachine 2.1.2__py3-none-any.whl → 2.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_statemachine-2.1.2.dist-info → python_statemachine-2.3.0.dist-info}/METADATA +86 -25
- python_statemachine-2.3.0.dist-info/RECORD +28 -0
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +106 -76
- statemachine/contrib/diagram.py +20 -6
- statemachine/dispatcher.py +72 -37
- statemachine/event.py +21 -20
- statemachine/exceptions.py +9 -3
- statemachine/factory.py +72 -9
- statemachine/graph.py +6 -0
- statemachine/locale/en/LC_MESSAGES/statemachine.po +40 -26
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +60 -39
- statemachine/mixins.py +1 -3
- statemachine/signature.py +15 -8
- statemachine/state.py +22 -25
- statemachine/statemachine.py +125 -97
- statemachine/states.py +6 -4
- statemachine/transition.py +35 -26
- statemachine/utils.py +19 -0
- python_statemachine-2.1.2.dist-info/RECORD +0 -30
- statemachine/locale/en/LC_MESSAGES/statemachine.mo +0 -0
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.mo +0 -0
- {python_statemachine-2.1.2.dist-info → python_statemachine-2.3.0.dist-info}/LICENSE +0 -0
- {python_statemachine-2.1.2.dist-info → python_statemachine-2.3.0.dist-info}/WHEEL +0 -0
|
@@ -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,7 +8,8 @@ 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.7
|
|
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
|
|
@@ -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
|
+

|
|
41
42
|
|
|
43
|
+
</div>
|
|
42
44
|
|
|
43
|
-
Welcome to python-statemachine, an intuitive and powerful state machine
|
|
44
|
-
great developer experience.
|
|
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.
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
## Features
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
50
72
|
|
|
51
|
-
🔒 python-statemachine also provides robust error handling and ensures that your system stays
|
|
52
|
-
in a valid state at all times.
|
|
53
73
|
|
|
54
74
|
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
## Getting started
|
|
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
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
from bisect import insort
|
|
1
2
|
from collections import defaultdict
|
|
2
3
|
from collections import deque
|
|
4
|
+
from enum import IntEnum
|
|
3
5
|
from typing import Callable
|
|
4
6
|
from typing import Dict
|
|
7
|
+
from typing import Generator
|
|
5
8
|
from typing import List
|
|
6
9
|
|
|
7
10
|
from .exceptions import AttrNotFound
|
|
@@ -9,27 +12,42 @@ from .i18n import _
|
|
|
9
12
|
from .utils import ensure_iterable
|
|
10
13
|
|
|
11
14
|
|
|
15
|
+
class CallbackPriority(IntEnum):
|
|
16
|
+
GENERIC = 0
|
|
17
|
+
INLINE = 10
|
|
18
|
+
DECORATOR = 20
|
|
19
|
+
NAMING = 30
|
|
20
|
+
AFTER = 40
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def allways_true(*args, **kwargs):
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
|
|
12
27
|
class CallbackWrapper:
|
|
13
28
|
def __init__(
|
|
14
29
|
self,
|
|
15
30
|
callback: Callable,
|
|
16
31
|
condition: Callable,
|
|
32
|
+
meta: "CallbackMeta",
|
|
17
33
|
unique_key: str,
|
|
18
|
-
expected_value: "bool | None" = None,
|
|
19
34
|
) -> None:
|
|
20
35
|
self._callback = callback
|
|
21
36
|
self.condition = condition
|
|
37
|
+
self.meta = meta
|
|
22
38
|
self.unique_key = unique_key
|
|
23
|
-
self.expected_value = expected_value
|
|
24
39
|
|
|
25
40
|
def __repr__(self):
|
|
26
41
|
return f"{type(self).__name__}({self.unique_key})"
|
|
27
42
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return
|
|
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
|
+
async def __call__(self, *args, **kwargs):
|
|
50
|
+
return await self._callback(*args, **kwargs)
|
|
33
51
|
|
|
34
52
|
|
|
35
53
|
class CallbackMeta:
|
|
@@ -42,14 +60,22 @@ class CallbackMeta:
|
|
|
42
60
|
call is performed, to allow the proper callback resolution.
|
|
43
61
|
"""
|
|
44
62
|
|
|
45
|
-
def __init__(
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
func,
|
|
66
|
+
suppress_errors=False,
|
|
67
|
+
cond=None,
|
|
68
|
+
priority: CallbackPriority = CallbackPriority.NAMING,
|
|
69
|
+
expected_value=None,
|
|
70
|
+
):
|
|
46
71
|
self.func = func
|
|
47
72
|
self.suppress_errors = suppress_errors
|
|
48
|
-
self.cond =
|
|
73
|
+
self.cond = cond
|
|
49
74
|
self.expected_value = expected_value
|
|
75
|
+
self.priority = priority
|
|
50
76
|
|
|
51
77
|
def __repr__(self):
|
|
52
|
-
return f"{type(self).__name__}({self.func!r})"
|
|
78
|
+
return f"{type(self).__name__}({self.func!r}, suppress_errors={self.suppress_errors!r})"
|
|
53
79
|
|
|
54
80
|
def __str__(self):
|
|
55
81
|
return getattr(self.func, "__name__", self.func)
|
|
@@ -63,7 +89,10 @@ class CallbackMeta:
|
|
|
63
89
|
def _update_func(self, func):
|
|
64
90
|
self.func = func
|
|
65
91
|
|
|
66
|
-
def
|
|
92
|
+
def _wrap_callable(self, func, _expected_value):
|
|
93
|
+
return func
|
|
94
|
+
|
|
95
|
+
def build(self, resolver) -> Generator["CallbackWrapper", None, None]:
|
|
67
96
|
"""
|
|
68
97
|
Resolves the `func` into a usable callable.
|
|
69
98
|
|
|
@@ -71,25 +100,14 @@ class CallbackMeta:
|
|
|
71
100
|
resolver (callable): A method responsible to build and return a valid callable that
|
|
72
101
|
can receive arbitrary parameters like `*args, **kwargs`.
|
|
73
102
|
"""
|
|
74
|
-
callback
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
callback=callback,
|
|
81
|
-
condition=conditions.all,
|
|
103
|
+
for callback in resolver(self.func):
|
|
104
|
+
condition = next(resolver(self.cond)) if self.cond is not None else allways_true
|
|
105
|
+
yield CallbackWrapper(
|
|
106
|
+
callback=self._wrap_callable(callback, self.expected_value),
|
|
107
|
+
condition=condition,
|
|
108
|
+
meta=self,
|
|
82
109
|
unique_key=callback.unique_key,
|
|
83
|
-
expected_value=self.expected_value,
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
if not self.suppress_errors:
|
|
87
|
-
raise AttrNotFound(
|
|
88
|
-
_("Did not found name '{}' from model or statemachine").format(
|
|
89
|
-
self.func
|
|
90
|
-
)
|
|
91
110
|
)
|
|
92
|
-
return None
|
|
93
111
|
|
|
94
112
|
|
|
95
113
|
class BoolCallbackMeta(CallbackMeta):
|
|
@@ -102,18 +120,32 @@ class BoolCallbackMeta(CallbackMeta):
|
|
|
102
120
|
call is performed, to allow the proper callback resolution.
|
|
103
121
|
"""
|
|
104
122
|
|
|
105
|
-
def __init__(
|
|
106
|
-
self
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
func,
|
|
126
|
+
suppress_errors=False,
|
|
127
|
+
cond=None,
|
|
128
|
+
priority: CallbackPriority = CallbackPriority.NAMING,
|
|
129
|
+
expected_value=True,
|
|
130
|
+
):
|
|
131
|
+
super().__init__(
|
|
132
|
+
func, suppress_errors, cond, priority=priority, expected_value=expected_value
|
|
133
|
+
)
|
|
110
134
|
|
|
111
135
|
def __str__(self):
|
|
112
136
|
name = super().__str__()
|
|
113
137
|
return name if self.expected_value else f"!{name}"
|
|
114
138
|
|
|
139
|
+
def _wrap_callable(self, func, expected_value):
|
|
140
|
+
async def bool_wrapper(*args, **kwargs):
|
|
141
|
+
return bool(await func(*args, **kwargs)) == expected_value
|
|
142
|
+
|
|
143
|
+
return bool_wrapper
|
|
144
|
+
|
|
115
145
|
|
|
116
146
|
class CallbackMetaList:
|
|
147
|
+
"""List of `CallbackMeta` instances"""
|
|
148
|
+
|
|
117
149
|
def __init__(self, factory=CallbackMeta):
|
|
118
150
|
self.items: List[CallbackMeta] = []
|
|
119
151
|
self.factory = factory
|
|
@@ -157,6 +189,7 @@ class CallbackMetaList:
|
|
|
157
189
|
return func
|
|
158
190
|
|
|
159
191
|
def __call__(self, callback):
|
|
192
|
+
"""Allows usage of the callback list as a decorator."""
|
|
160
193
|
return self._add_unbounded_callback(callback)
|
|
161
194
|
|
|
162
195
|
def __iter__(self):
|
|
@@ -165,18 +198,13 @@ class CallbackMetaList:
|
|
|
165
198
|
def clear(self):
|
|
166
199
|
self.items = []
|
|
167
200
|
|
|
168
|
-
def _add(self, func,
|
|
201
|
+
def _add(self, func, **kwargs):
|
|
169
202
|
meta = self.factory(func, **kwargs)
|
|
170
|
-
if registry is not None and not registry(self, meta, prepend=prepend):
|
|
171
|
-
return
|
|
172
203
|
|
|
173
204
|
if meta in self.items:
|
|
174
205
|
return
|
|
175
206
|
|
|
176
|
-
|
|
177
|
-
self.items.insert(0, meta)
|
|
178
|
-
else:
|
|
179
|
-
self.items.append(meta)
|
|
207
|
+
self.items.append(meta)
|
|
180
208
|
return meta
|
|
181
209
|
|
|
182
210
|
def add(self, callbacks, **kwargs):
|
|
@@ -191,6 +219,8 @@ class CallbackMetaList:
|
|
|
191
219
|
|
|
192
220
|
|
|
193
221
|
class CallbacksExecutor:
|
|
222
|
+
"""A list of callbacks that can be executed in order."""
|
|
223
|
+
|
|
194
224
|
def __init__(self):
|
|
195
225
|
self.items: List[CallbackWrapper] = deque()
|
|
196
226
|
self.items_already_seen = set()
|
|
@@ -201,60 +231,60 @@ class CallbacksExecutor:
|
|
|
201
231
|
def __repr__(self):
|
|
202
232
|
return f"{type(self).__name__}({self.items!r})"
|
|
203
233
|
|
|
204
|
-
def
|
|
205
|
-
|
|
206
|
-
) -> "CallbackWrapper | None":
|
|
207
|
-
callback = callback_info.build(resolver)
|
|
208
|
-
if callback is None:
|
|
209
|
-
return None
|
|
234
|
+
def __str__(self):
|
|
235
|
+
return ", ".join(str(c) for c in self)
|
|
210
236
|
|
|
211
|
-
|
|
212
|
-
|
|
237
|
+
def _add(self, callback_meta: CallbackMeta, resolver: Callable):
|
|
238
|
+
for callback in callback_meta.build(resolver):
|
|
239
|
+
if callback.unique_key in self.items_already_seen:
|
|
240
|
+
continue
|
|
213
241
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
self.items.insert(0, callback)
|
|
217
|
-
else:
|
|
218
|
-
self.items.append(callback)
|
|
219
|
-
return callback
|
|
242
|
+
self.items_already_seen.add(callback.unique_key)
|
|
243
|
+
insort(self.items, callback)
|
|
220
244
|
|
|
221
245
|
def add(self, items: CallbackMetaList, resolver: Callable):
|
|
222
246
|
"""Validate configurations"""
|
|
223
247
|
for item in items:
|
|
224
|
-
self.
|
|
248
|
+
self._add(item, resolver)
|
|
225
249
|
return self
|
|
226
250
|
|
|
227
|
-
def call(self, *args, **kwargs):
|
|
251
|
+
async def call(self, *args, **kwargs):
|
|
228
252
|
return [
|
|
229
|
-
callback(*args, **kwargs)
|
|
253
|
+
await callback(*args, **kwargs)
|
|
230
254
|
for callback in self
|
|
231
|
-
if callback.condition(*args, **kwargs)
|
|
255
|
+
if await callback.condition(*args, **kwargs)
|
|
232
256
|
]
|
|
233
257
|
|
|
234
|
-
def all(self, *args, **kwargs):
|
|
235
|
-
|
|
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
|
|
236
263
|
|
|
237
264
|
|
|
238
265
|
class CallbacksRegistry:
|
|
239
266
|
def __init__(self) -> None:
|
|
240
|
-
self._registry: Dict[CallbackMetaList, CallbacksExecutor] = defaultdict(
|
|
241
|
-
CallbacksExecutor
|
|
242
|
-
)
|
|
267
|
+
self._registry: Dict[CallbackMetaList, CallbacksExecutor] = defaultdict(CallbacksExecutor)
|
|
243
268
|
|
|
244
|
-
def register(self,
|
|
245
|
-
executor_list = self[
|
|
246
|
-
executor_list.add(
|
|
269
|
+
def register(self, meta_list: CallbackMetaList, resolver):
|
|
270
|
+
executor_list = self[meta_list]
|
|
271
|
+
executor_list.add(meta_list, resolver)
|
|
247
272
|
return executor_list
|
|
248
273
|
|
|
249
|
-
def
|
|
250
|
-
|
|
274
|
+
def clear(self):
|
|
275
|
+
self._registry.clear()
|
|
276
|
+
|
|
277
|
+
def __getitem__(self, meta_list: CallbackMetaList) -> CallbacksExecutor:
|
|
278
|
+
return self._registry[meta_list]
|
|
251
279
|
|
|
252
|
-
def
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
meta:
|
|
256
|
-
|
|
257
|
-
):
|
|
258
|
-
return self[meta_list].add_one(meta, resolver, prepend=prepend)
|
|
280
|
+
def check(self, meta_list: CallbackMetaList):
|
|
281
|
+
executor = self[meta_list]
|
|
282
|
+
for meta in meta_list:
|
|
283
|
+
if meta.suppress_errors:
|
|
284
|
+
continue
|
|
259
285
|
|
|
260
|
-
|
|
286
|
+
if any(callback for callback in executor if callback.meta == meta):
|
|
287
|
+
continue
|
|
288
|
+
raise AttrNotFound(
|
|
289
|
+
_("Did not found name '{}' from model or statemachine").format(meta.func)
|
|
290
|
+
)
|
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}"
|