python-statemachine 2.3.0__tar.gz → 2.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/PKG-INFO +4 -24
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/README.md +3 -23
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/pyproject.toml +11 -6
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/__init__.py +1 -1
- python_statemachine-2.3.2/statemachine/callbacks.py +362 -0
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/contrib/diagram.py +7 -4
- python_statemachine-2.3.2/statemachine/dispatcher.py +151 -0
- python_statemachine-2.3.2/statemachine/engines/__init__.py +0 -0
- python_statemachine-2.3.2/statemachine/engines/async_.py +136 -0
- python_statemachine-2.3.2/statemachine/engines/sync.py +139 -0
- python_statemachine-2.3.2/statemachine/event.py +53 -0
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/event_data.py +2 -4
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/events.py +2 -2
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/factory.py +10 -11
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/mixins.py +10 -4
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/registry.py +5 -8
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/signature.py +6 -5
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/state.py +13 -17
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/statemachine.py +111 -167
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/states.py +4 -3
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/transition.py +22 -45
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/transition_list.py +6 -8
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/utils.py +11 -5
- python_statemachine-2.3.0/statemachine/callbacks.py +0 -290
- python_statemachine-2.3.0/statemachine/dispatcher.py +0 -159
- python_statemachine-2.3.0/statemachine/event.py +0 -72
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/LICENSE +0 -0
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/contrib/__init__.py +0 -0
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/exceptions.py +0 -0
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/graph.py +0 -0
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/i18n.py +0 -0
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/locale/en/LC_MESSAGES/statemachine.po +0 -0
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +0 -0
- {python_statemachine-2.3.0 → python_statemachine-2.3.2}/statemachine/model.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-statemachine
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: Python Finite State Machines made easy.
|
|
5
5
|
Home-page: https://github.com/fgmacedo/python-statemachine
|
|
6
6
|
License: MIT
|
|
@@ -43,7 +43,7 @@ Python [finite-state machines](https://en.wikipedia.org/wiki/Finite-state_machin
|
|
|
43
43
|
</div>
|
|
44
44
|
|
|
45
45
|
Welcome to python-statemachine, an intuitive and powerful state machine library designed for a
|
|
46
|
-
great developer experience. We provide
|
|
46
|
+
great developer experience. We provide a _pythonic_ and expressive API for implementing state
|
|
47
47
|
machines in sync or asynchonous Python codebases.
|
|
48
48
|
|
|
49
49
|
## Features
|
|
@@ -103,7 +103,7 @@ Define your state machine:
|
|
|
103
103
|
... | red.to(green)
|
|
104
104
|
... )
|
|
105
105
|
...
|
|
106
|
-
...
|
|
106
|
+
... def before_cycle(self, event: str, source: State, target: State, message: str = ""):
|
|
107
107
|
... message = ". " + message if message else ""
|
|
108
108
|
... return f"Running {event} from {source.id} to {target.id}{message}"
|
|
109
109
|
...
|
|
@@ -146,26 +146,6 @@ Then start sending events to your new state machine:
|
|
|
146
146
|
|
|
147
147
|
```
|
|
148
148
|
|
|
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
149
|
**That's it.** This is all an external object needs to know about your state machine: How to send events.
|
|
170
150
|
Ideally, all states, transitions, and actions should be kept internally and not checked externally to avoid unnecessary coupling.
|
|
171
151
|
|
|
@@ -253,7 +233,7 @@ callback method.
|
|
|
253
233
|
Note how `before_cycle` was declared:
|
|
254
234
|
|
|
255
235
|
```py
|
|
256
|
-
|
|
236
|
+
def before_cycle(self, event: str, source: State, target: State, message: str = ""):
|
|
257
237
|
message = ". " + message if message else ""
|
|
258
238
|
return f"Running {event} from {source.id} to {target.id}{message}"
|
|
259
239
|
```
|
|
@@ -17,7 +17,7 @@ Python [finite-state machines](https://en.wikipedia.org/wiki/Finite-state_machin
|
|
|
17
17
|
</div>
|
|
18
18
|
|
|
19
19
|
Welcome to python-statemachine, an intuitive and powerful state machine library designed for a
|
|
20
|
-
great developer experience. We provide
|
|
20
|
+
great developer experience. We provide a _pythonic_ and expressive API for implementing state
|
|
21
21
|
machines in sync or asynchonous Python codebases.
|
|
22
22
|
|
|
23
23
|
## Features
|
|
@@ -77,7 +77,7 @@ Define your state machine:
|
|
|
77
77
|
... | red.to(green)
|
|
78
78
|
... )
|
|
79
79
|
...
|
|
80
|
-
...
|
|
80
|
+
... def before_cycle(self, event: str, source: State, target: State, message: str = ""):
|
|
81
81
|
... message = ". " + message if message else ""
|
|
82
82
|
... return f"Running {event} from {source.id} to {target.id}{message}"
|
|
83
83
|
...
|
|
@@ -120,26 +120,6 @@ Then start sending events to your new state machine:
|
|
|
120
120
|
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
You can use the exactly same state machine from an async codebase:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
```py
|
|
127
|
-
>>> async def run_sm():
|
|
128
|
-
... asm = TrafficLightMachine()
|
|
129
|
-
... results = []
|
|
130
|
-
... for _i in range(4):
|
|
131
|
-
... result = await asm.send("cycle")
|
|
132
|
-
... results.append(result)
|
|
133
|
-
... return results
|
|
134
|
-
|
|
135
|
-
>>> asyncio.run(run_sm())
|
|
136
|
-
Don't move.
|
|
137
|
-
Go ahead!
|
|
138
|
-
['Running cycle from green to yellow', 'Running cycle from yellow to red', ...
|
|
139
|
-
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
|
|
143
123
|
**That's it.** This is all an external object needs to know about your state machine: How to send events.
|
|
144
124
|
Ideally, all states, transitions, and actions should be kept internally and not checked externally to avoid unnecessary coupling.
|
|
145
125
|
|
|
@@ -227,7 +207,7 @@ callback method.
|
|
|
227
207
|
Note how `before_cycle` was declared:
|
|
228
208
|
|
|
229
209
|
```py
|
|
230
|
-
|
|
210
|
+
def before_cycle(self, event: str, source: State, target: State, message: str = ""):
|
|
231
211
|
message = ". " + message if message else ""
|
|
232
212
|
return f"Running {event} from {source.id} to {target.id}{message}"
|
|
233
213
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "python-statemachine"
|
|
3
|
-
version = "2.3.
|
|
3
|
+
version = "2.3.2"
|
|
4
4
|
description = "Python Finite State Machines made easy."
|
|
5
5
|
authors = ["Fernando Macedo <fgmacedo@gmail.com>"]
|
|
6
6
|
maintainers = [
|
|
@@ -39,18 +39,21 @@ diagrams = ["pydot"]
|
|
|
39
39
|
python = ">=3.7"
|
|
40
40
|
|
|
41
41
|
[tool.poetry.group.dev.dependencies]
|
|
42
|
-
pytest = "*"
|
|
43
|
-
pytest-cov = "*"
|
|
44
|
-
pytest-sugar = "^1.0.0"
|
|
45
42
|
pydot = "^2.0.0"
|
|
46
43
|
ruff = "^0.4.8"
|
|
47
44
|
pre-commit = "*"
|
|
48
45
|
mypy = "*"
|
|
46
|
+
|
|
47
|
+
[tool.poetry.group.tests.dependencies]
|
|
48
|
+
pytest = "*"
|
|
49
|
+
pytest-cov = "*"
|
|
50
|
+
pytest-sugar = "^1.0.0"
|
|
49
51
|
pytest-mock = "^3.10.0"
|
|
50
52
|
pytest-profiling = "^1.7.0"
|
|
51
53
|
pytest-benchmark = "^4.0.0"
|
|
52
54
|
pytest-asyncio = "*"
|
|
53
|
-
|
|
55
|
+
django = { version = "^5.0.3", python = ">3.10" }
|
|
56
|
+
pytest-django = { version = "^4.8.0", python = ">3.8" }
|
|
54
57
|
|
|
55
58
|
[tool.poetry.group.docs.dependencies]
|
|
56
59
|
Sphinx = "*"
|
|
@@ -65,18 +68,20 @@ requires = ["poetry-core"]
|
|
|
65
68
|
build-backend = "poetry.core.masonry.api"
|
|
66
69
|
|
|
67
70
|
[tool.pytest.ini_options]
|
|
68
|
-
addopts = "--ignore=docs/conf.py --ignore=docs/auto_examples/ --ignore=docs/_build/ --ignore=tests/examples/ --cov --cov-config .coveragerc --doctest-glob='*.md' --doctest-modules --doctest-continue-on-failure --benchmark-autosave"
|
|
71
|
+
addopts = "--ignore=docs/conf.py --ignore=docs/auto_examples/ --ignore=docs/_build/ --ignore=tests/examples/ --cov --cov-config .coveragerc --doctest-glob='*.md' --doctest-modules --doctest-continue-on-failure --benchmark-autosave --benchmark-group-by=name"
|
|
69
72
|
doctest_optionflags = "ELLIPSIS IGNORE_EXCEPTION_DETAIL NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL"
|
|
70
73
|
asyncio_mode = "auto"
|
|
71
74
|
markers = [
|
|
72
75
|
"""slow: marks tests as slow (deselect with '-m "not slow"')""",
|
|
73
76
|
]
|
|
77
|
+
python_files = ["tests.py", "test_*.py", "*_tests.py"]
|
|
74
78
|
|
|
75
79
|
[tool.mypy]
|
|
76
80
|
python_version = "3.12"
|
|
77
81
|
warn_return_any = true
|
|
78
82
|
warn_unused_configs = true
|
|
79
83
|
disable_error_code = "annotation-unchecked"
|
|
84
|
+
mypy_path = "$MYPY_CONFIG_FILE_DIR/tests/django_project"
|
|
80
85
|
|
|
81
86
|
[[tool.mypy.overrides]]
|
|
82
87
|
module = [
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from bisect import insort
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from collections import deque
|
|
6
|
+
from enum import IntEnum
|
|
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
|
+
|
|
17
|
+
from .exceptions import AttrNotFound
|
|
18
|
+
from .i18n import _
|
|
19
|
+
from .utils import ensure_iterable
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CallbackPriority(IntEnum):
|
|
23
|
+
GENERIC = 0
|
|
24
|
+
INLINE = 10
|
|
25
|
+
DECORATOR = 20
|
|
26
|
+
NAMING = 30
|
|
27
|
+
AFTER = 40
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SpecReference(IntEnum):
|
|
31
|
+
NAME = 1
|
|
32
|
+
CALLABLE = 2
|
|
33
|
+
PROPERTY = 3
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CallbackGroup(IntEnum):
|
|
37
|
+
ENTER = auto()
|
|
38
|
+
EXIT = auto()
|
|
39
|
+
VALIDATOR = auto()
|
|
40
|
+
BEFORE = auto()
|
|
41
|
+
ON = auto()
|
|
42
|
+
AFTER = auto()
|
|
43
|
+
COND = auto()
|
|
44
|
+
|
|
45
|
+
def build_key(self, specs: "CallbackSpecList") -> str:
|
|
46
|
+
return f"{self.name}@{id(specs)}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def allways_true(*args, **kwargs):
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CallbackSpec:
|
|
54
|
+
"""Specs about callbacks.
|
|
55
|
+
|
|
56
|
+
At first, `func` can be a name (string), a property or a callable.
|
|
57
|
+
|
|
58
|
+
Names, properties and unbounded callables should be resolved to a callable
|
|
59
|
+
before any real call is performed.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
func,
|
|
65
|
+
group: CallbackGroup,
|
|
66
|
+
is_convention=False,
|
|
67
|
+
cond=None,
|
|
68
|
+
priority: CallbackPriority = CallbackPriority.NAMING,
|
|
69
|
+
expected_value=None,
|
|
70
|
+
):
|
|
71
|
+
self.func = func
|
|
72
|
+
self.group = group
|
|
73
|
+
self.is_convention = is_convention
|
|
74
|
+
self.cond = cond
|
|
75
|
+
self.expected_value = expected_value
|
|
76
|
+
self.priority = priority
|
|
77
|
+
|
|
78
|
+
if isinstance(func, property):
|
|
79
|
+
self.reference = SpecReference.PROPERTY
|
|
80
|
+
self.attr_name: str = func and func.fget and func.fget.__name__ or ""
|
|
81
|
+
elif callable(func):
|
|
82
|
+
self.reference = SpecReference.CALLABLE
|
|
83
|
+
self.is_bounded = hasattr(func, "__self__")
|
|
84
|
+
self.attr_name = func.__name__
|
|
85
|
+
else:
|
|
86
|
+
self.reference = SpecReference.NAME
|
|
87
|
+
self.attr_name = func
|
|
88
|
+
|
|
89
|
+
def __repr__(self):
|
|
90
|
+
return f"{type(self).__name__}({self.func!r}, is_convention={self.is_convention!r})"
|
|
91
|
+
|
|
92
|
+
def __str__(self):
|
|
93
|
+
name = getattr(self.func, "__name__", self.func)
|
|
94
|
+
if self.expected_value is False:
|
|
95
|
+
name = f"!{name}"
|
|
96
|
+
return name
|
|
97
|
+
|
|
98
|
+
def __eq__(self, other):
|
|
99
|
+
return self.func == other.func and self.group == other.group
|
|
100
|
+
|
|
101
|
+
def __hash__(self):
|
|
102
|
+
return id(self)
|
|
103
|
+
|
|
104
|
+
def _update_func(self, func: Callable, attr_name: str):
|
|
105
|
+
self.func = func
|
|
106
|
+
self.reference = SpecReference.CALLABLE
|
|
107
|
+
self.attr_name = attr_name
|
|
108
|
+
|
|
109
|
+
def build(self, resolver) -> Generator["CallbackWrapper", None, None]:
|
|
110
|
+
"""
|
|
111
|
+
Resolves the `func` into a usable callable.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
resolver (callable): A method responsible to build and return a valid callable that
|
|
115
|
+
can receive arbitrary parameters like `*args, **kwargs`.
|
|
116
|
+
"""
|
|
117
|
+
for callback in resolver.search(self):
|
|
118
|
+
condition = self.cond if self.cond is not None else allways_true
|
|
119
|
+
yield CallbackWrapper(
|
|
120
|
+
callback=callback,
|
|
121
|
+
condition=condition,
|
|
122
|
+
meta=self,
|
|
123
|
+
unique_key=callback.unique_key,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class SpecListGrouper:
|
|
128
|
+
def __init__(
|
|
129
|
+
self, list: "CallbackSpecList", group: CallbackGroup, factory=CallbackSpec
|
|
130
|
+
) -> None:
|
|
131
|
+
self.list = list
|
|
132
|
+
self.group = group
|
|
133
|
+
self.factory = factory
|
|
134
|
+
self.key = group.build_key(list)
|
|
135
|
+
|
|
136
|
+
def add(self, callbacks, **kwargs):
|
|
137
|
+
self.list.add(callbacks, group=self.group, factory=self.factory, **kwargs)
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
def __call__(self, callback):
|
|
141
|
+
return self.list._add_unbounded_callback(callback, group=self.group, factory=self.factory)
|
|
142
|
+
|
|
143
|
+
def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
|
|
144
|
+
self.list._add_unbounded_callback(
|
|
145
|
+
func,
|
|
146
|
+
is_event=is_event,
|
|
147
|
+
transitions=transitions,
|
|
148
|
+
group=self.group,
|
|
149
|
+
factory=self.factory,
|
|
150
|
+
**kwargs,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def __iter__(self):
|
|
154
|
+
return (item for item in self.list if item.group == self.group)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class CallbackSpecList:
|
|
158
|
+
"""List of {ref}`CallbackSpec` instances"""
|
|
159
|
+
|
|
160
|
+
def __init__(self, factory=CallbackSpec):
|
|
161
|
+
self.items: List[CallbackSpec] = []
|
|
162
|
+
self.conventional_specs = set()
|
|
163
|
+
self.factory = factory
|
|
164
|
+
|
|
165
|
+
def __repr__(self):
|
|
166
|
+
return f"{type(self).__name__}({self.items!r}, factory={self.factory!r})"
|
|
167
|
+
|
|
168
|
+
def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
|
|
169
|
+
"""This list was a target for adding a func using decorator
|
|
170
|
+
`@<state|event>[.on|before|after|enter|exit]` syntax.
|
|
171
|
+
|
|
172
|
+
If we assign ``func`` directly as callable on the ``items`` list,
|
|
173
|
+
this will result in an `unbounded method error`, with `func` expecting a parameter
|
|
174
|
+
``self`` not defined.
|
|
175
|
+
|
|
176
|
+
The implemented solution is to resolve the collision giving the func a reference method.
|
|
177
|
+
To update It's callback when the name is resolved on the
|
|
178
|
+
:func:`StateMachineMetaclass.add_from_attributes`.
|
|
179
|
+
If the ``func`` is bounded It will be used directly, if not, it's ref will be replaced
|
|
180
|
+
by the given attr name and on `statemachine._setup()` the dynamic name will be resolved
|
|
181
|
+
properly.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
func (callable): The decorated method to add on the transitions occurs.
|
|
185
|
+
is_event (bool): If the func is also an event, we'll create a trigger and link the
|
|
186
|
+
event name to the transitions.
|
|
187
|
+
transitions (TransitionList): If ``is_event``, the transitions to be attached to the
|
|
188
|
+
event.
|
|
189
|
+
|
|
190
|
+
"""
|
|
191
|
+
spec = self._add(func, **kwargs)
|
|
192
|
+
if not getattr(func, "_specs_to_update", None):
|
|
193
|
+
func._specs_to_update = set()
|
|
194
|
+
if is_event:
|
|
195
|
+
func._specs_to_update.add(spec._update_func)
|
|
196
|
+
func._transitions = transitions
|
|
197
|
+
|
|
198
|
+
return func
|
|
199
|
+
|
|
200
|
+
def __iter__(self):
|
|
201
|
+
return iter(self.items)
|
|
202
|
+
|
|
203
|
+
def clear(self):
|
|
204
|
+
self.items = []
|
|
205
|
+
|
|
206
|
+
def grouper(
|
|
207
|
+
self, group: CallbackGroup, factory: Type[CallbackSpec] = CallbackSpec
|
|
208
|
+
) -> SpecListGrouper:
|
|
209
|
+
return SpecListGrouper(self, group, factory=factory)
|
|
210
|
+
|
|
211
|
+
def _add(self, func, group: CallbackGroup, factory=None, **kwargs):
|
|
212
|
+
if factory is None:
|
|
213
|
+
factory = self.factory
|
|
214
|
+
spec = factory(func, group, **kwargs)
|
|
215
|
+
|
|
216
|
+
if spec in self.items:
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
self.items.append(spec)
|
|
220
|
+
if spec.is_convention:
|
|
221
|
+
self.conventional_specs.add(spec.func)
|
|
222
|
+
return spec
|
|
223
|
+
|
|
224
|
+
def add(self, callbacks, group: CallbackGroup, **kwargs):
|
|
225
|
+
if callbacks is None:
|
|
226
|
+
return self
|
|
227
|
+
|
|
228
|
+
unprepared = ensure_iterable(callbacks)
|
|
229
|
+
for func in unprepared:
|
|
230
|
+
self._add(func, group=group, **kwargs)
|
|
231
|
+
|
|
232
|
+
return self
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class CallbackWrapper:
|
|
236
|
+
def __init__(
|
|
237
|
+
self,
|
|
238
|
+
callback: Callable,
|
|
239
|
+
condition: Callable,
|
|
240
|
+
meta: "CallbackSpec",
|
|
241
|
+
unique_key: str,
|
|
242
|
+
) -> None:
|
|
243
|
+
self._callback = callback
|
|
244
|
+
self._iscoro = iscoroutinefunction(callback)
|
|
245
|
+
self.condition = condition
|
|
246
|
+
self.meta = meta
|
|
247
|
+
self.unique_key = unique_key
|
|
248
|
+
self.expected_value = self.meta.expected_value
|
|
249
|
+
|
|
250
|
+
def __repr__(self):
|
|
251
|
+
return f"{type(self).__name__}({self.unique_key})"
|
|
252
|
+
|
|
253
|
+
def __str__(self):
|
|
254
|
+
return str(self.meta)
|
|
255
|
+
|
|
256
|
+
def __lt__(self, other):
|
|
257
|
+
return self.meta.priority < other.meta.priority
|
|
258
|
+
|
|
259
|
+
async def __call__(self, *args, **kwargs):
|
|
260
|
+
value = self._callback(*args, **kwargs)
|
|
261
|
+
if isawaitable(value):
|
|
262
|
+
value = await value
|
|
263
|
+
|
|
264
|
+
if self.expected_value is not None:
|
|
265
|
+
return bool(value) == self.expected_value
|
|
266
|
+
return value
|
|
267
|
+
|
|
268
|
+
def call(self, *args, **kwargs):
|
|
269
|
+
value = self._callback(*args, **kwargs)
|
|
270
|
+
if self.expected_value is not None:
|
|
271
|
+
return bool(value) == self.expected_value
|
|
272
|
+
return value
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class CallbacksExecutor:
|
|
276
|
+
"""A list of callbacks that can be executed in order."""
|
|
277
|
+
|
|
278
|
+
def __init__(self):
|
|
279
|
+
self.items: List[CallbackWrapper] = deque()
|
|
280
|
+
self.items_already_seen = set()
|
|
281
|
+
|
|
282
|
+
def __iter__(self):
|
|
283
|
+
return iter(self.items)
|
|
284
|
+
|
|
285
|
+
def __repr__(self):
|
|
286
|
+
return f"{type(self).__name__}({self.items!r})"
|
|
287
|
+
|
|
288
|
+
def __str__(self):
|
|
289
|
+
return ", ".join(str(c) for c in self)
|
|
290
|
+
|
|
291
|
+
def _add(self, spec: CallbackSpec, resolver: Callable):
|
|
292
|
+
for callback in spec.build(resolver):
|
|
293
|
+
if callback.unique_key in self.items_already_seen:
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
self.items_already_seen.add(callback.unique_key)
|
|
297
|
+
insort(self.items, callback)
|
|
298
|
+
|
|
299
|
+
def add(self, items: Iterable[CallbackSpec], resolver: Callable):
|
|
300
|
+
"""Validate configurations"""
|
|
301
|
+
for item in items:
|
|
302
|
+
self._add(item, resolver)
|
|
303
|
+
return self
|
|
304
|
+
|
|
305
|
+
async def async_call(self, *args, **kwargs):
|
|
306
|
+
return await asyncio.gather(
|
|
307
|
+
*(
|
|
308
|
+
callback(*args, **kwargs)
|
|
309
|
+
for callback in self
|
|
310
|
+
if callback.condition(*args, **kwargs)
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
async def async_all(self, *args, **kwargs):
|
|
315
|
+
coros = [condition(*args, **kwargs) for condition in self]
|
|
316
|
+
for coro in asyncio.as_completed(coros):
|
|
317
|
+
if not await coro:
|
|
318
|
+
return False
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
def call(self, *args, **kwargs):
|
|
322
|
+
return [
|
|
323
|
+
callback.call(*args, **kwargs)
|
|
324
|
+
for callback in self
|
|
325
|
+
if callback.condition(*args, **kwargs)
|
|
326
|
+
]
|
|
327
|
+
|
|
328
|
+
def all(self, *args, **kwargs):
|
|
329
|
+
for condition in self:
|
|
330
|
+
if not condition.call(*args, **kwargs):
|
|
331
|
+
return False
|
|
332
|
+
return True
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class CallbacksRegistry:
|
|
336
|
+
def __init__(self) -> None:
|
|
337
|
+
self._registry: Dict[str, CallbacksExecutor] = defaultdict(CallbacksExecutor)
|
|
338
|
+
self._method_types: Counter = Counter()
|
|
339
|
+
|
|
340
|
+
def clear(self):
|
|
341
|
+
self._registry.clear()
|
|
342
|
+
|
|
343
|
+
def __getitem__(self, key: str) -> CallbacksExecutor:
|
|
344
|
+
return self._registry[key]
|
|
345
|
+
|
|
346
|
+
def check(self, specs: CallbackSpecList):
|
|
347
|
+
for meta in specs:
|
|
348
|
+
if meta.is_convention:
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
if any(
|
|
352
|
+
callback for callback in self[meta.group.build_key(specs)] if callback.meta == meta
|
|
353
|
+
):
|
|
354
|
+
continue
|
|
355
|
+
raise AttrNotFound(
|
|
356
|
+
_("Did not found name '{}' from model or statemachine").format(meta.func)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def async_or_sync(self):
|
|
360
|
+
self._method_types.update(
|
|
361
|
+
callback._iscoro for executor in self._registry.values() for callback in executor
|
|
362
|
+
)
|
|
@@ -69,12 +69,15 @@ class DotGraphMachine:
|
|
|
69
69
|
def _actions_getter(self):
|
|
70
70
|
if isinstance(self.machine, StateMachine):
|
|
71
71
|
|
|
72
|
-
def getter(
|
|
73
|
-
return self.machine.
|
|
72
|
+
def getter(grouper):
|
|
73
|
+
return self.machine._get_callbacks(grouper.key)
|
|
74
74
|
else:
|
|
75
75
|
|
|
76
|
-
def getter(
|
|
77
|
-
|
|
76
|
+
def getter(grouper):
|
|
77
|
+
all_names = set(dir(self.machine))
|
|
78
|
+
return ", ".join(
|
|
79
|
+
str(c) for c in grouper if not c.is_convention or c.func in all_names
|
|
80
|
+
)
|
|
78
81
|
|
|
79
82
|
return getter
|
|
80
83
|
|