python-statemachine 2.1.0__py3-none-any.whl → 2.3.5__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.0.dist-info → python_statemachine-2.3.5.dist-info}/METADATA +67 -29
- python_statemachine-2.3.5.dist-info/RECORD +32 -0
- {python_statemachine-2.1.0.dist-info → python_statemachine-2.3.5.dist-info}/WHEEL +1 -1
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +280 -86
- statemachine/contrib/diagram.py +29 -12
- statemachine/dispatcher.py +136 -78
- statemachine/engines/__init__.py +0 -0
- statemachine/engines/async_.py +136 -0
- statemachine/engines/sync.py +139 -0
- statemachine/event.py +21 -39
- statemachine/event_data.py +2 -4
- statemachine/events.py +2 -2
- statemachine/exceptions.py +9 -3
- statemachine/factory.py +115 -47
- 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 +11 -7
- statemachine/py.typed +0 -0
- statemachine/registry.py +5 -8
- statemachine/signature.py +68 -15
- statemachine/state.py +89 -38
- statemachine/statemachine.py +179 -178
- statemachine/states.py +32 -12
- statemachine/transition.py +55 -55
- statemachine/transition_list.py +6 -8
- statemachine/utils.py +26 -5
- python_statemachine-2.1.0.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.0.dist-info → python_statemachine-2.3.5.dist-info}/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-statemachine
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.5
|
|
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
|
|
@@ -18,11 +19,8 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.9
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
25
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
24
|
Classifier: Topic :: Software Development :: Libraries
|
|
27
25
|
Provides-Extra: diagrams
|
|
28
26
|
Description-Content-Type: text/markdown
|
|
@@ -30,8 +28,8 @@ Description-Content-Type: text/markdown
|
|
|
30
28
|
# Python StateMachine
|
|
31
29
|
|
|
32
30
|
[](https://pypi.python.org/pypi/python-statemachine)
|
|
31
|
+
[](https://pepy.tech/project/python-statemachine)
|
|
33
32
|
[](https://pypi.python.org/pypi/python-statemachine)
|
|
34
|
-
[](https://github.com/fgmacedo/python-statemachine/actions/workflows/python-package.yml?query=branch%3Adevelop)
|
|
35
33
|
[](https://codecov.io/gh/fgmacedo/python-statemachine)
|
|
36
34
|
[](https://python-statemachine.readthedocs.io/en/latest/?badge=latest)
|
|
37
35
|
[](https://github.com/fgmacedo/python-statemachine/compare/main...develop)
|
|
@@ -39,33 +37,43 @@ Description-Content-Type: text/markdown
|
|
|
39
37
|
|
|
40
38
|
Python [finite-state machines](https://en.wikipedia.org/wiki/Finite-state_machine) made easy.
|
|
41
39
|
|
|
40
|
+
<div align="center">
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
* Documentation: https://python-statemachine.readthedocs.io.
|
|
45
|
-
|
|
42
|
+

|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
great developer experience.
|
|
44
|
+
</div>
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
Welcome to python-statemachine, an intuitive and powerful state machine library designed for a
|
|
47
|
+
great developer experience. We provide a _pythonic_ and expressive API for implementing state
|
|
48
|
+
machines in sync or asynchonous Python codebases.
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
transitions in your system, so you can focus on building great products.
|
|
50
|
+
## Features
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
- ✨ **Basic components**: Easily define **States**, **Events**, and **Transitions** to model your logic.
|
|
53
|
+
- ⚙️ **Actions and handlers**: Attach actions and handlers to states, events, and transitions to control behavior dynamically.
|
|
54
|
+
- 🛡️ **Conditional transitions**: Implement **Guards** and **Validators** to conditionally control transitions, ensuring they only occur when specific conditions are met.
|
|
55
|
+
- 🚀 **Full async support**: Enjoy full asynchronous support. Await events, and dispatch callbacks asynchronously for seamless integration with async codebases.
|
|
56
|
+
- 🔄 **Full sync support**: Use the same state machine from synchronous codebases without any modifications.
|
|
57
|
+
- 🎨 **Declarative and simple API**: Utilize a clean, elegant, and readable API to define your state machine, making it easy to maintain and understand.
|
|
58
|
+
- 👀 **Observer pattern support**: Register external and generic objects to watch events and register callbacks.
|
|
59
|
+
- 🔍 **Decoupled design**: Separate concerns with a decoupled "state machine" and "model" design, promoting cleaner architecture and easier maintenance.
|
|
60
|
+
- ✅ **Correctness guarantees**: Ensured correctness with validations at class definition time:
|
|
61
|
+
- Ensures exactly one `initial` state.
|
|
62
|
+
- Disallows transitions from `final` states.
|
|
63
|
+
- Requires ongoing transitions for all non-final states.
|
|
64
|
+
- Guarantees all non-final states have at least one path to a final state if final states are declared.
|
|
65
|
+
- Validates the state machine graph representation has a single component.
|
|
66
|
+
- 📦 **Flexible event dispatching**: Dispatch events with any extra data, making it available to all callbacks, including actions and guards.
|
|
67
|
+
- 🔧 **Dependency injection**: Needed parameters are injected into callbacks.
|
|
68
|
+
- 📊 **Graphical representation**: Generate and output graphical representations of state machines. Create diagrams from the command line, at runtime, or even in Jupyter notebooks.
|
|
69
|
+
- 🌍 **Internationalization support**: Provides error messages in different languages, making the library accessible to a global audience.
|
|
70
|
+
- 🛡️ **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.
|
|
71
|
+
- 🏛️ **Domain model integration**: Seamlessly integrate with domain models using Mixins.
|
|
72
|
+
- 🔧 **Django integration**: Automatically discover state machines in Django applications.
|
|
57
73
|
|
|
58
74
|
|
|
59
|
-
A few reasons why you may consider using it:
|
|
60
|
-
|
|
61
|
-
* 📈 python-statemachine is designed to help you build scalable,
|
|
62
|
-
maintainable systems that can handle any complexity.
|
|
63
|
-
* 💪 You can easily create and manage multiple state machines within a single application.
|
|
64
|
-
* 🚫 Prevents common mistakes and ensures that your system stays in a valid state at all times.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
## Getting started
|
|
68
75
|
|
|
76
|
+
## Installing
|
|
69
77
|
|
|
70
78
|
To install Python State Machine, run this command in your terminal:
|
|
71
79
|
|
|
@@ -77,6 +85,8 @@ our docs for more details.
|
|
|
77
85
|
|
|
78
86
|
pip install python-statemachine[diagrams]
|
|
79
87
|
|
|
88
|
+
## First example
|
|
89
|
+
|
|
80
90
|
Define your state machine:
|
|
81
91
|
|
|
82
92
|
```py
|
|
@@ -137,7 +147,7 @@ Then start sending events to your new state machine:
|
|
|
137
147
|
|
|
138
148
|
```
|
|
139
149
|
|
|
140
|
-
That's it
|
|
150
|
+
**That's it.** This is all an external object needs to know about your state machine: How to send events.
|
|
141
151
|
Ideally, all states, transitions, and actions should be kept internally and not checked externally to avoid unnecessary coupling.
|
|
142
152
|
|
|
143
153
|
But if your use case needs, you can inspect state machine properties, like the current state:
|
|
@@ -269,6 +279,34 @@ and in diagrams:
|
|
|
269
279
|
|
|
270
280
|
```
|
|
271
281
|
|
|
282
|
+
## Async support
|
|
283
|
+
|
|
284
|
+
We support native coroutine using `asyncio`, enabling seamless integration with asynchronous code.
|
|
285
|
+
There's no change on the public API of the library to work on async codebases.
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
```py
|
|
289
|
+
>>> class AsyncStateMachine(StateMachine):
|
|
290
|
+
... initial = State('Initial', initial=True)
|
|
291
|
+
... final = State('Final', final=True)
|
|
292
|
+
...
|
|
293
|
+
... advance = initial.to(final)
|
|
294
|
+
...
|
|
295
|
+
... async def on_advance(self):
|
|
296
|
+
... return 42
|
|
297
|
+
|
|
298
|
+
>>> async def run_sm():
|
|
299
|
+
... sm = AsyncStateMachine()
|
|
300
|
+
... result = await sm.advance()
|
|
301
|
+
... print(f"Result is {result}")
|
|
302
|
+
... print(sm.current_state)
|
|
303
|
+
|
|
304
|
+
>>> asyncio.run(run_sm())
|
|
305
|
+
Result is 42
|
|
306
|
+
Final
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
|
|
272
310
|
## A more useful example
|
|
273
311
|
|
|
274
312
|
A simple didactic state machine for controlling an `Order`:
|
|
@@ -375,7 +413,7 @@ https://python-statemachine.readthedocs.io.
|
|
|
375
413
|
- If you found this project helpful, please consider giving it a star on GitHub.
|
|
376
414
|
|
|
377
415
|
- **Contribute code**: If you would like to contribute code to this project, please submit a pull
|
|
378
|
-
request. For more information on how to contribute, please see our [contributing.md]contributing.md) file.
|
|
416
|
+
request. For more information on how to contribute, please see our [contributing.md](contributing.md) file.
|
|
379
417
|
|
|
380
418
|
- **Report bugs**: If you find any bugs in this project, please report them by opening an issue
|
|
381
419
|
on our GitHub issue tracker.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
statemachine/__init__.py,sha256=ZSwV-f_lq_MIAG00F_PCnS-514sn_jf-6uykaeGD5Ug,192
|
|
2
|
+
statemachine/callbacks.py,sha256=5_WaILrjUvZOYX_LQe4Gd8nsCvkErjw9DaHYG_ggvxs,11101
|
|
3
|
+
statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
statemachine/contrib/diagram.py,sha256=eK3unncEXNq3e_yfBTZmvgdWWWDZq69r7gsHvCHr5Ys,7208
|
|
5
|
+
statemachine/dispatcher.py,sha256=2DtjCj_kiDF2uk_EEvXxs5NTABOvCr4paxVz77s6mXA,5365
|
|
6
|
+
statemachine/engines/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
statemachine/engines/async_.py,sha256=Z-AS4zQ2xDj2uexJ7fepQTSaw5PjqesemWGXX9g0rpU,5546
|
|
8
|
+
statemachine/engines/sync.py,sha256=bYCyzoYo03LLfxzUl2XyyNlmMmxDLn_w0AMa5IHCF3U,5497
|
|
9
|
+
statemachine/event.py,sha256=LfvO-imBVyVX6krBK0Pz7q-iTRudo_5OjTFaYJpc6IQ,1556
|
|
10
|
+
statemachine/event_data.py,sha256=x9m9Q2q-78kVFnJVJmOAwq6lo9zgSVxko3q4Omwjqxk,2228
|
|
11
|
+
statemachine/events.py,sha256=QgXcxu25WkUaxJvonh0K7hOlYYSQlV0jbhTCAi_gpwc,736
|
|
12
|
+
statemachine/exceptions.py,sha256=RWq1vTjajxt8CzfYCD4DfI2nrgkfCPBL0DjRDOzKkQI,1086
|
|
13
|
+
statemachine/factory.py,sha256=8rRvDXWGjEX6v6mP7YiXOJPTCcm-GhDmuTvyotw1zoc,7948
|
|
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=zJYaV2DxrHdPngDOt1bji8-lPOZMSDjWG83Ypru5DOI,2126
|
|
17
|
+
statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=SPrt-KaCBCKQq7PoOMtyyqU1bihrw_3MPL_NbqZzTVs,3214
|
|
18
|
+
statemachine/mixins.py,sha256=Y1fa52Cj20JaGkyNk3P7Gpqkt4cGTjJ0YyV_VQyCl0M,1231
|
|
19
|
+
statemachine/model.py,sha256=OylI3FjMiHpYyDl9mtK1zEJMeSvemaN4giQDonpc8kI,211
|
|
20
|
+
statemachine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
statemachine/registry.py,sha256=HmV9sUGkYVrNUZxJYoZo-trSUis7dIun_WcGktblgc8,922
|
|
22
|
+
statemachine/signature.py,sha256=HwuyWX3p_GZwKKrGbt9wk4-bHpymqs_pgqW7OlDgGRc,7877
|
|
23
|
+
statemachine/state.py,sha256=IKdkMBkXCgcvswMKQZ-hn0-739Hhf6z6q8mSEoevV7o,8262
|
|
24
|
+
statemachine/statemachine.py,sha256=WgI4LbnOyOBl8OZqBXFRYETtt8rQi0XEhQN-D_2oMFM,11536
|
|
25
|
+
statemachine/states.py,sha256=vpRNjiGmTDHvE0fn0gwdq8-WcG32n4ZiPTU-kgZ73rc,4870
|
|
26
|
+
statemachine/transition.py,sha256=R7gfSZjqR59OIdpKePgdr66zTp5azow-tDqqYJsECR4,4711
|
|
27
|
+
statemachine/transition_list.py,sha256=_tdFD1O7hw8xVAU6nRYmGrj6TCAM0SD-Sa0_DYKa88Y,6034
|
|
28
|
+
statemachine/utils.py,sha256=DpcrGqlbrnT-ogh-BogG0L07EG3KirHOsKORHlspDlI,1041
|
|
29
|
+
python_statemachine-2.3.5.dist-info/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
|
|
30
|
+
python_statemachine-2.3.5.dist-info/METADATA,sha256=CWbBQMeVziWU_TJdMiH67aDjn-Tw7OeOXVSFRzXO83Y,13983
|
|
31
|
+
python_statemachine-2.3.5.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
|
32
|
+
python_statemachine-2.3.5.dist-info/RECORD,,
|
statemachine/__init__.py
CHANGED
statemachine/callbacks.py
CHANGED
|
@@ -1,42 +1,116 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from bisect import insort
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from collections import deque
|
|
5
|
+
from enum import IntEnum
|
|
6
|
+
from enum import IntFlag
|
|
7
|
+
from enum import auto
|
|
8
|
+
from inspect import isawaitable
|
|
9
|
+
from inspect import iscoroutinefunction
|
|
10
|
+
from typing import Callable
|
|
11
|
+
from typing import Dict
|
|
12
|
+
from typing import Generator
|
|
13
|
+
from typing import Iterable
|
|
14
|
+
from typing import List
|
|
15
|
+
from typing import Type
|
|
16
|
+
|
|
1
17
|
from .exceptions import AttrNotFound
|
|
2
|
-
from .exceptions import InvalidDefinition
|
|
3
18
|
from .i18n import _
|
|
4
19
|
from .utils import ensure_iterable
|
|
5
20
|
|
|
6
21
|
|
|
7
|
-
class
|
|
8
|
-
|
|
22
|
+
class CallbackPriority(IntEnum):
|
|
23
|
+
GENERIC = 0
|
|
24
|
+
INLINE = 10
|
|
25
|
+
DECORATOR = 20
|
|
26
|
+
NAMING = 30
|
|
27
|
+
AFTER = 40
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SpecReference(IntFlag):
|
|
31
|
+
NAME = auto()
|
|
32
|
+
CALLABLE = auto()
|
|
33
|
+
PROPERTY = auto()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
SPECS_ALL = SpecReference.NAME | SpecReference.CALLABLE | SpecReference.PROPERTY
|
|
37
|
+
SPECS_SAFE = SpecReference.NAME
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CallbackGroup(IntEnum):
|
|
41
|
+
ENTER = auto()
|
|
42
|
+
EXIT = auto()
|
|
43
|
+
VALIDATOR = auto()
|
|
44
|
+
BEFORE = auto()
|
|
45
|
+
ON = auto()
|
|
46
|
+
AFTER = auto()
|
|
47
|
+
COND = auto()
|
|
48
|
+
|
|
49
|
+
def build_key(self, specs: "CallbackSpecList") -> str:
|
|
50
|
+
return f"{self.name}@{id(specs)}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def allways_true(*args, **kwargs):
|
|
54
|
+
return True
|
|
9
55
|
|
|
10
|
-
At first, `func` can be a string or a callable, and even if it's already
|
|
11
|
-
a callable, his signature can mismatch.
|
|
12
56
|
|
|
13
|
-
|
|
14
|
-
|
|
57
|
+
class CallbackSpec:
|
|
58
|
+
"""Specs about callbacks.
|
|
59
|
+
|
|
60
|
+
At first, `func` can be a name (string), a property or a callable.
|
|
61
|
+
|
|
62
|
+
Names, properties and unbounded callables should be resolved to a callable
|
|
63
|
+
before any real call is performed.
|
|
15
64
|
"""
|
|
16
65
|
|
|
17
|
-
def __init__(
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
func,
|
|
69
|
+
group: CallbackGroup,
|
|
70
|
+
is_convention=False,
|
|
71
|
+
cond=None,
|
|
72
|
+
priority: CallbackPriority = CallbackPriority.NAMING,
|
|
73
|
+
expected_value=None,
|
|
74
|
+
):
|
|
18
75
|
self.func = func
|
|
19
|
-
self.
|
|
20
|
-
self.
|
|
21
|
-
self.
|
|
22
|
-
self.
|
|
76
|
+
self.group = group
|
|
77
|
+
self.is_convention = is_convention
|
|
78
|
+
self.cond = cond
|
|
79
|
+
self.expected_value = expected_value
|
|
80
|
+
self.priority = priority
|
|
81
|
+
|
|
82
|
+
if isinstance(func, property):
|
|
83
|
+
self.reference = SpecReference.PROPERTY
|
|
84
|
+
self.attr_name: str = func and func.fget and func.fget.__name__ or ""
|
|
85
|
+
elif callable(func):
|
|
86
|
+
self.reference = SpecReference.CALLABLE
|
|
87
|
+
self.is_bounded = hasattr(func, "__self__")
|
|
88
|
+
self.attr_name = func.__name__
|
|
89
|
+
else:
|
|
90
|
+
self.reference = SpecReference.NAME
|
|
91
|
+
self.attr_name = func
|
|
23
92
|
|
|
24
93
|
def __repr__(self):
|
|
25
|
-
return f"{type(self).__name__}({self.func!r})"
|
|
94
|
+
return f"{type(self).__name__}({self.func!r}, is_convention={self.is_convention!r})"
|
|
26
95
|
|
|
27
96
|
def __str__(self):
|
|
28
|
-
|
|
97
|
+
name = getattr(self.func, "__name__", self.func)
|
|
98
|
+
if self.expected_value is False:
|
|
99
|
+
name = f"!{name}"
|
|
100
|
+
return name
|
|
29
101
|
|
|
30
102
|
def __eq__(self, other):
|
|
31
|
-
return self.func == other.func and self.
|
|
103
|
+
return self.func == other.func and self.group == other.group
|
|
32
104
|
|
|
33
105
|
def __hash__(self):
|
|
34
106
|
return id(self)
|
|
35
107
|
|
|
36
|
-
def _update_func(self, func):
|
|
108
|
+
def _update_func(self, func: Callable, attr_name: str):
|
|
37
109
|
self.func = func
|
|
110
|
+
self.reference = SpecReference.CALLABLE
|
|
111
|
+
self.attr_name = attr_name
|
|
38
112
|
|
|
39
|
-
def
|
|
113
|
+
def build(self, resolver) -> Generator["CallbackWrapper", None, None]:
|
|
40
114
|
"""
|
|
41
115
|
Resolves the `func` into a usable callable.
|
|
42
116
|
|
|
@@ -44,56 +118,57 @@ class CallbackWrapper:
|
|
|
44
118
|
resolver (callable): A method responsible to build and return a valid callable that
|
|
45
119
|
can receive arbitrary parameters like `*args, **kwargs`.
|
|
46
120
|
"""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
raise
|
|
55
|
-
return False
|
|
56
|
-
|
|
57
|
-
def __call__(self, *args, **kwargs):
|
|
58
|
-
if self._callback is None:
|
|
59
|
-
raise InvalidDefinition(
|
|
60
|
-
_("Callback {!r} not property configured.").format(self)
|
|
121
|
+
for callback in resolver.search(self):
|
|
122
|
+
condition = self.cond if self.cond is not None else allways_true
|
|
123
|
+
yield CallbackWrapper(
|
|
124
|
+
callback=callback,
|
|
125
|
+
condition=condition,
|
|
126
|
+
meta=self,
|
|
127
|
+
unique_key=callback.unique_key,
|
|
61
128
|
)
|
|
62
|
-
return self._callback(*args, **kwargs)
|
|
63
129
|
|
|
64
130
|
|
|
65
|
-
class
|
|
66
|
-
def __init__(
|
|
67
|
-
|
|
68
|
-
|
|
131
|
+
class SpecListGrouper:
|
|
132
|
+
def __init__(
|
|
133
|
+
self, list: "CallbackSpecList", group: CallbackGroup, factory=CallbackSpec
|
|
134
|
+
) -> None:
|
|
135
|
+
self.list = list
|
|
136
|
+
self.group = group
|
|
137
|
+
self.factory = factory
|
|
138
|
+
self.key = group.build_key(list)
|
|
69
139
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
return
|
|
140
|
+
def add(self, callbacks, **kwargs):
|
|
141
|
+
self.list.add(callbacks, group=self.group, factory=self.factory, **kwargs)
|
|
142
|
+
return self
|
|
143
|
+
|
|
144
|
+
def __call__(self, callback):
|
|
145
|
+
return self.list._add_unbounded_callback(callback, group=self.group, factory=self.factory)
|
|
73
146
|
|
|
74
|
-
def
|
|
75
|
-
|
|
147
|
+
def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
|
|
148
|
+
self.list._add_unbounded_callback(
|
|
149
|
+
func,
|
|
150
|
+
is_event=is_event,
|
|
151
|
+
transitions=transitions,
|
|
152
|
+
group=self.group,
|
|
153
|
+
factory=self.factory,
|
|
154
|
+
**kwargs,
|
|
155
|
+
)
|
|
76
156
|
|
|
157
|
+
def __iter__(self):
|
|
158
|
+
return (item for item in self.list if item.group == self.group)
|
|
77
159
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
160
|
+
|
|
161
|
+
class CallbackSpecList:
|
|
162
|
+
"""List of {ref}`CallbackSpec` instances"""
|
|
163
|
+
|
|
164
|
+
def __init__(self, factory=CallbackSpec):
|
|
165
|
+
self.items: List[CallbackSpec] = []
|
|
166
|
+
self.conventional_specs = set()
|
|
82
167
|
self.factory = factory
|
|
83
168
|
|
|
84
169
|
def __repr__(self):
|
|
85
170
|
return f"{type(self).__name__}({self.items!r}, factory={self.factory!r})"
|
|
86
171
|
|
|
87
|
-
def __str__(self):
|
|
88
|
-
return ", ".join(str(c) for c in self)
|
|
89
|
-
|
|
90
|
-
def setup(self, resolver):
|
|
91
|
-
"""Validate configurations"""
|
|
92
|
-
self._resolver = resolver
|
|
93
|
-
self.items = [
|
|
94
|
-
callback for callback in self.items if callback.setup(self._resolver)
|
|
95
|
-
]
|
|
96
|
-
|
|
97
172
|
def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
|
|
98
173
|
"""This list was a target for adding a func using decorator
|
|
99
174
|
`@<state|event>[.on|before|after|enter|exit]` syntax.
|
|
@@ -117,56 +192,175 @@ class Callbacks:
|
|
|
117
192
|
event.
|
|
118
193
|
|
|
119
194
|
"""
|
|
120
|
-
|
|
121
|
-
if not getattr(func, "
|
|
122
|
-
func.
|
|
123
|
-
|
|
124
|
-
|
|
195
|
+
spec = self._add(func, **kwargs)
|
|
196
|
+
if not getattr(func, "_specs_to_update", None):
|
|
197
|
+
func._specs_to_update = set()
|
|
198
|
+
if is_event:
|
|
199
|
+
func._specs_to_update.add(spec._update_func)
|
|
125
200
|
func._transitions = transitions
|
|
126
201
|
|
|
127
202
|
return func
|
|
128
203
|
|
|
129
|
-
def __call__(self, callback):
|
|
130
|
-
return self._add_unbounded_callback(callback)
|
|
131
|
-
|
|
132
204
|
def __iter__(self):
|
|
133
205
|
return iter(self.items)
|
|
134
206
|
|
|
135
207
|
def clear(self):
|
|
136
208
|
self.items = []
|
|
137
209
|
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if callback.cond.all(*args, **kwargs)
|
|
143
|
-
]
|
|
210
|
+
def grouper(
|
|
211
|
+
self, group: CallbackGroup, factory: Type[CallbackSpec] = CallbackSpec
|
|
212
|
+
) -> SpecListGrouper:
|
|
213
|
+
return SpecListGrouper(self, group, factory=factory)
|
|
144
214
|
|
|
145
|
-
def
|
|
146
|
-
|
|
215
|
+
def _add(self, func, group: CallbackGroup, factory=None, **kwargs):
|
|
216
|
+
if factory is None:
|
|
217
|
+
factory = self.factory
|
|
218
|
+
spec = factory(func, group, **kwargs)
|
|
147
219
|
|
|
148
|
-
|
|
149
|
-
resolver = resolver or self._resolver
|
|
150
|
-
|
|
151
|
-
callback = self.factory(func, **kwargs)
|
|
152
|
-
if resolver is not None and not callback.setup(resolver):
|
|
220
|
+
if spec in self.items:
|
|
153
221
|
return
|
|
154
222
|
|
|
155
|
-
|
|
156
|
-
|
|
223
|
+
self.items.append(spec)
|
|
224
|
+
if spec.is_convention:
|
|
225
|
+
self.conventional_specs.add(spec.func)
|
|
226
|
+
return spec
|
|
157
227
|
|
|
158
|
-
|
|
159
|
-
self.items.insert(0, callback)
|
|
160
|
-
else:
|
|
161
|
-
self.items.append(callback)
|
|
162
|
-
return callback
|
|
163
|
-
|
|
164
|
-
def add(self, callbacks, **kwargs):
|
|
228
|
+
def add(self, callbacks, group: CallbackGroup, **kwargs):
|
|
165
229
|
if callbacks is None:
|
|
166
230
|
return self
|
|
167
231
|
|
|
168
232
|
unprepared = ensure_iterable(callbacks)
|
|
169
233
|
for func in unprepared:
|
|
170
|
-
self._add(func, **kwargs)
|
|
234
|
+
self._add(func, group=group, **kwargs)
|
|
171
235
|
|
|
172
236
|
return self
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class CallbackWrapper:
|
|
240
|
+
def __init__(
|
|
241
|
+
self,
|
|
242
|
+
callback: Callable,
|
|
243
|
+
condition: Callable,
|
|
244
|
+
meta: "CallbackSpec",
|
|
245
|
+
unique_key: str,
|
|
246
|
+
) -> None:
|
|
247
|
+
self._callback = callback
|
|
248
|
+
self._iscoro = iscoroutinefunction(callback)
|
|
249
|
+
self.condition = condition
|
|
250
|
+
self.meta = meta
|
|
251
|
+
self.unique_key = unique_key
|
|
252
|
+
self.expected_value = self.meta.expected_value
|
|
253
|
+
|
|
254
|
+
def __repr__(self):
|
|
255
|
+
return f"{type(self).__name__}({self.unique_key})"
|
|
256
|
+
|
|
257
|
+
def __str__(self):
|
|
258
|
+
return str(self.meta)
|
|
259
|
+
|
|
260
|
+
def __lt__(self, other):
|
|
261
|
+
return self.meta.priority < other.meta.priority
|
|
262
|
+
|
|
263
|
+
async def __call__(self, *args, **kwargs):
|
|
264
|
+
value = self._callback(*args, **kwargs)
|
|
265
|
+
if isawaitable(value):
|
|
266
|
+
value = await value
|
|
267
|
+
|
|
268
|
+
if self.expected_value is not None:
|
|
269
|
+
return bool(value) == self.expected_value
|
|
270
|
+
return value
|
|
271
|
+
|
|
272
|
+
def call(self, *args, **kwargs):
|
|
273
|
+
value = self._callback(*args, **kwargs)
|
|
274
|
+
if self.expected_value is not None:
|
|
275
|
+
return bool(value) == self.expected_value
|
|
276
|
+
return value
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class CallbacksExecutor:
|
|
280
|
+
"""A list of callbacks that can be executed in order."""
|
|
281
|
+
|
|
282
|
+
def __init__(self):
|
|
283
|
+
self.items: List[CallbackWrapper] = deque()
|
|
284
|
+
self.items_already_seen = set()
|
|
285
|
+
|
|
286
|
+
def __iter__(self):
|
|
287
|
+
return iter(self.items)
|
|
288
|
+
|
|
289
|
+
def __repr__(self):
|
|
290
|
+
return f"{type(self).__name__}({self.items!r})"
|
|
291
|
+
|
|
292
|
+
def __str__(self):
|
|
293
|
+
return ", ".join(str(c) for c in self)
|
|
294
|
+
|
|
295
|
+
def _add(self, spec: CallbackSpec, resolver: Callable):
|
|
296
|
+
for callback in spec.build(resolver):
|
|
297
|
+
if callback.unique_key in self.items_already_seen:
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
self.items_already_seen.add(callback.unique_key)
|
|
301
|
+
insort(self.items, callback)
|
|
302
|
+
|
|
303
|
+
def add(self, items: Iterable[CallbackSpec], resolver: Callable):
|
|
304
|
+
"""Validate configurations"""
|
|
305
|
+
for item in items:
|
|
306
|
+
self._add(item, resolver)
|
|
307
|
+
return self
|
|
308
|
+
|
|
309
|
+
async def async_call(self, *args, **kwargs):
|
|
310
|
+
return await asyncio.gather(
|
|
311
|
+
*(
|
|
312
|
+
callback(*args, **kwargs)
|
|
313
|
+
for callback in self
|
|
314
|
+
if callback.condition(*args, **kwargs)
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
async def async_all(self, *args, **kwargs):
|
|
319
|
+
coros = [condition(*args, **kwargs) for condition in self]
|
|
320
|
+
for coro in asyncio.as_completed(coros):
|
|
321
|
+
if not await coro:
|
|
322
|
+
return False
|
|
323
|
+
return True
|
|
324
|
+
|
|
325
|
+
def call(self, *args, **kwargs):
|
|
326
|
+
return [
|
|
327
|
+
callback.call(*args, **kwargs)
|
|
328
|
+
for callback in self
|
|
329
|
+
if callback.condition(*args, **kwargs)
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
def all(self, *args, **kwargs):
|
|
333
|
+
for condition in self:
|
|
334
|
+
if not condition.call(*args, **kwargs):
|
|
335
|
+
return False
|
|
336
|
+
return True
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class CallbacksRegistry:
|
|
340
|
+
def __init__(self) -> None:
|
|
341
|
+
self._registry: Dict[str, CallbacksExecutor] = defaultdict(CallbacksExecutor)
|
|
342
|
+
self.has_async_callbacks: bool = False
|
|
343
|
+
|
|
344
|
+
def clear(self):
|
|
345
|
+
self._registry.clear()
|
|
346
|
+
|
|
347
|
+
def __getitem__(self, key: str) -> CallbacksExecutor:
|
|
348
|
+
return self._registry[key]
|
|
349
|
+
|
|
350
|
+
def check(self, specs: CallbackSpecList):
|
|
351
|
+
for meta in specs:
|
|
352
|
+
if meta.is_convention:
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
if any(
|
|
356
|
+
callback for callback in self[meta.group.build_key(specs)] if callback.meta == meta
|
|
357
|
+
):
|
|
358
|
+
continue
|
|
359
|
+
raise AttrNotFound(
|
|
360
|
+
_("Did not found name '{}' from model or statemachine").format(meta.func)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def async_or_sync(self):
|
|
364
|
+
self.has_async_callbacks = any(
|
|
365
|
+
callback._iscoro for executor in self._registry.values() for callback in executor
|
|
366
|
+
)
|