python-statemachine 2.3.5__tar.gz → 2.4.0__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.5 → python_statemachine-2.4.0}/PKG-INFO +9 -8
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/README.md +7 -7
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/pyproject.toml +22 -40
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/__init__.py +3 -2
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/callbacks.py +62 -10
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/contrib/diagram.py +0 -4
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/dispatcher.py +5 -3
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/engines/async_.py +1 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/engines/sync.py +1 -0
- python_statemachine-2.4.0/statemachine/event.py +131 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/event_data.py +2 -1
- python_statemachine-2.4.0/statemachine/events.py +40 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/exceptions.py +3 -2
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/factory.py +47 -12
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/locale/en/LC_MESSAGES/statemachine.po +27 -15
- python_statemachine-2.4.0/statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +93 -0
- python_statemachine-2.4.0/statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +93 -0
- python_statemachine-2.4.0/statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +93 -0
- python_statemachine-2.4.0/statemachine/spec_parser.py +79 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/state.py +7 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/statemachine.py +15 -23
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/states.py +2 -2
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/transition.py +1 -2
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/transition_list.py +5 -1
- python_statemachine-2.3.5/statemachine/event.py +0 -53
- python_statemachine-2.3.5/statemachine/events.py +0 -31
- python_statemachine-2.3.5/statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +0 -91
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/LICENSE +0 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/contrib/__init__.py +0 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/engines/__init__.py +0 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/graph.py +0 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/i18n.py +0 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/mixins.py +0 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/model.py +0 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/py.typed +0 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/registry.py +0 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/signature.py +0 -0
- {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-statemachine
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Python Finite State Machines made easy.
|
|
5
5
|
Home-page: https://github.com/fgmacedo/python-statemachine
|
|
6
6
|
License: MIT
|
|
@@ -23,6 +23,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
24
24
|
Classifier: Topic :: Software Development :: Libraries
|
|
25
25
|
Provides-Extra: diagrams
|
|
26
|
+
Requires-Dist: pydot (>=2.0.0) ; (python_full_version > "3.8.0") and (extra == "diagrams")
|
|
26
27
|
Description-Content-Type: text/markdown
|
|
27
28
|
|
|
28
29
|
# Python StateMachine
|
|
@@ -404,7 +405,7 @@ There's a lot more to cover, please take a look at our docs:
|
|
|
404
405
|
https://python-statemachine.readthedocs.io.
|
|
405
406
|
|
|
406
407
|
|
|
407
|
-
## Contributing
|
|
408
|
+
## Contributing
|
|
408
409
|
|
|
409
410
|
* <a class="github-button" href="https://github.com/fgmacedo/python-statemachine" data-icon="octicon-star" aria-label="Star fgmacedo/python-statemachine on GitHub">Star this project</a>
|
|
410
411
|
* <a class="github-button" href="https://github.com/fgmacedo/python-statemachine/issues" data-icon="octicon-issue-opened" aria-label="Issue fgmacedo/python-statemachine on GitHub">Open an Issue</a>
|
|
@@ -412,18 +413,18 @@ https://python-statemachine.readthedocs.io.
|
|
|
412
413
|
|
|
413
414
|
- If you found this project helpful, please consider giving it a star on GitHub.
|
|
414
415
|
|
|
415
|
-
- **Contribute code**: If you would like to contribute code
|
|
416
|
+
- **Contribute code**: If you would like to contribute code, please submit a pull
|
|
416
417
|
request. For more information on how to contribute, please see our [contributing.md](contributing.md) file.
|
|
417
418
|
|
|
418
|
-
- **Report bugs**: If you find any bugs
|
|
419
|
+
- **Report bugs**: If you find any bugs, please report them by opening an issue
|
|
419
420
|
on our GitHub issue tracker.
|
|
420
421
|
|
|
421
|
-
- **Suggest features**: If you have
|
|
422
|
-
an issue on our GitHub issue tracker.
|
|
422
|
+
- **Suggest features**: If you have an idea for a new feature, of feels something being harder than it should be,
|
|
423
|
+
please let us know by opening an issue on our GitHub issue tracker.
|
|
423
424
|
|
|
424
|
-
- **Documentation**: Help improve
|
|
425
|
+
- **Documentation**: Help improve documentation by submitting pull requests.
|
|
425
426
|
|
|
426
|
-
- **Promote the project**: Help spread the word
|
|
427
|
+
- **Promote the project**: Help spread the word by sharing on social media,
|
|
427
428
|
writing a blog post, or giving a talk about it. Tag me on Twitter
|
|
428
429
|
[@fgmacedo](https://twitter.com/fgmacedo) so I can share it too!
|
|
429
430
|
|
|
@@ -377,7 +377,7 @@ There's a lot more to cover, please take a look at our docs:
|
|
|
377
377
|
https://python-statemachine.readthedocs.io.
|
|
378
378
|
|
|
379
379
|
|
|
380
|
-
## Contributing
|
|
380
|
+
## Contributing
|
|
381
381
|
|
|
382
382
|
* <a class="github-button" href="https://github.com/fgmacedo/python-statemachine" data-icon="octicon-star" aria-label="Star fgmacedo/python-statemachine on GitHub">Star this project</a>
|
|
383
383
|
* <a class="github-button" href="https://github.com/fgmacedo/python-statemachine/issues" data-icon="octicon-issue-opened" aria-label="Issue fgmacedo/python-statemachine on GitHub">Open an Issue</a>
|
|
@@ -385,17 +385,17 @@ https://python-statemachine.readthedocs.io.
|
|
|
385
385
|
|
|
386
386
|
- If you found this project helpful, please consider giving it a star on GitHub.
|
|
387
387
|
|
|
388
|
-
- **Contribute code**: If you would like to contribute code
|
|
388
|
+
- **Contribute code**: If you would like to contribute code, please submit a pull
|
|
389
389
|
request. For more information on how to contribute, please see our [contributing.md](contributing.md) file.
|
|
390
390
|
|
|
391
|
-
- **Report bugs**: If you find any bugs
|
|
391
|
+
- **Report bugs**: If you find any bugs, please report them by opening an issue
|
|
392
392
|
on our GitHub issue tracker.
|
|
393
393
|
|
|
394
|
-
- **Suggest features**: If you have
|
|
395
|
-
an issue on our GitHub issue tracker.
|
|
394
|
+
- **Suggest features**: If you have an idea for a new feature, of feels something being harder than it should be,
|
|
395
|
+
please let us know by opening an issue on our GitHub issue tracker.
|
|
396
396
|
|
|
397
|
-
- **Documentation**: Help improve
|
|
397
|
+
- **Documentation**: Help improve documentation by submitting pull requests.
|
|
398
398
|
|
|
399
|
-
- **Promote the project**: Help spread the word
|
|
399
|
+
- **Promote the project**: Help spread the word by sharing on social media,
|
|
400
400
|
writing a blog post, or giving a talk about it. Tag me on Twitter
|
|
401
401
|
[@fgmacedo](https://twitter.com/fgmacedo) so I can share it too!
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "python-statemachine"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.4.0"
|
|
4
4
|
description = "Python Finite State Machines made easy."
|
|
5
5
|
authors = ["Fernando Macedo <fgmacedo@gmail.com>"]
|
|
6
|
-
maintainers = [
|
|
7
|
-
"Fernando Macedo <fgmacedo@gmail.com>",
|
|
8
|
-
]
|
|
6
|
+
maintainers = ["Fernando Macedo <fgmacedo@gmail.com>"]
|
|
9
7
|
license = "MIT license"
|
|
10
8
|
readme = "README.md"
|
|
11
9
|
homepage = "https://github.com/fgmacedo/python-statemachine"
|
|
12
|
-
packages = [
|
|
13
|
-
{include = "statemachine"},
|
|
14
|
-
{include = "statemachine/**/*.py" },
|
|
15
|
-
]
|
|
10
|
+
packages = [{ include = "statemachine" }, { include = "statemachine/**/*.py" }]
|
|
16
11
|
include = [
|
|
17
12
|
{ path = "statemachine/locale/**/*.po", format = "sdist" },
|
|
18
|
-
{ path = "statemachine/locale/**/*.mo", format = [
|
|
13
|
+
{ path = "statemachine/locale/**/*.mo", format = [
|
|
14
|
+
"sdist",
|
|
15
|
+
"wheel",
|
|
16
|
+
] },
|
|
19
17
|
]
|
|
20
18
|
classifiers = [
|
|
21
19
|
"Intended Audience :: Developers",
|
|
@@ -33,14 +31,14 @@ classifiers = [
|
|
|
33
31
|
"Intended Audience :: Developers",
|
|
34
32
|
]
|
|
35
33
|
|
|
36
|
-
[tool.poetry.extras]
|
|
37
|
-
diagrams = ["pydot"]
|
|
38
|
-
|
|
39
34
|
[tool.poetry.dependencies]
|
|
40
35
|
python = ">=3.7"
|
|
36
|
+
pydot = { version = ">=2.0.0", optional = true, python = ">3.8" }
|
|
37
|
+
|
|
38
|
+
[tool.poetry.extras]
|
|
39
|
+
diagrams = ["pydot"]
|
|
41
40
|
|
|
42
41
|
[tool.poetry.group.dev.dependencies]
|
|
43
|
-
pydot = "^2.0.0"
|
|
44
42
|
ruff = "^0.4.8"
|
|
45
43
|
pre-commit = "*"
|
|
46
44
|
mypy = "*"
|
|
@@ -59,7 +57,7 @@ pytest-django = { version = "^4.8.0", python = ">3.8" }
|
|
|
59
57
|
Sphinx = { version = "*", python = ">3.8" }
|
|
60
58
|
myst-parser = { version = "*", python = ">3.8" }
|
|
61
59
|
sphinx-gallery = { version = "*", python = ">3.8" }
|
|
62
|
-
pillow = { version ="*", python = ">3.8" }
|
|
60
|
+
pillow = { version = "*", python = ">3.8" }
|
|
63
61
|
sphinx-autobuild = { version = "*", python = ">3.8" }
|
|
64
62
|
furo = { version = "^2024.5.6", python = ">3.8" }
|
|
65
63
|
sphinx-copybutton = { version = "^0.5.2", python = ">3.8" }
|
|
@@ -72,9 +70,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
72
70
|
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"
|
|
73
71
|
doctest_optionflags = "ELLIPSIS IGNORE_EXCEPTION_DETAIL NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL"
|
|
74
72
|
asyncio_mode = "auto"
|
|
75
|
-
markers = [
|
|
76
|
-
"""slow: marks tests as slow (deselect with '-m "not slow"')""",
|
|
77
|
-
]
|
|
73
|
+
markers = ["""slow: marks tests as slow (deselect with '-m "not slow"')"""]
|
|
78
74
|
python_files = ["tests.py", "test_*.py", "*_tests.py"]
|
|
79
75
|
|
|
80
76
|
[tool.mypy]
|
|
@@ -85,19 +81,11 @@ disable_error_code = "annotation-unchecked"
|
|
|
85
81
|
mypy_path = "$MYPY_CONFIG_FILE_DIR/tests/django_project"
|
|
86
82
|
|
|
87
83
|
[[tool.mypy.overrides]]
|
|
88
|
-
module = [
|
|
89
|
-
'django.*',
|
|
90
|
-
'pytest.*',
|
|
91
|
-
'pydot.*',
|
|
92
|
-
'sphinx_gallery.*',
|
|
93
|
-
]
|
|
84
|
+
module = ['django.*', 'pytest.*', 'pydot.*', 'sphinx_gallery.*']
|
|
94
85
|
ignore_missing_imports = true
|
|
95
86
|
|
|
96
87
|
[tool.flake8]
|
|
97
|
-
ignore = [
|
|
98
|
-
"E231",
|
|
99
|
-
"W503",
|
|
100
|
-
]
|
|
88
|
+
ignore = ["E231", "W503"]
|
|
101
89
|
max-line-length = 99
|
|
102
90
|
|
|
103
91
|
[tool.ruff]
|
|
@@ -131,10 +119,10 @@ exclude = [
|
|
|
131
119
|
|
|
132
120
|
# Enable Pyflakes and pycodestyle rules.
|
|
133
121
|
select = [
|
|
134
|
-
"E",
|
|
135
|
-
"W",
|
|
136
|
-
"F",
|
|
137
|
-
"I",
|
|
122
|
+
"E", # pycodestyle errors
|
|
123
|
+
"W", # pycodestyle warnings
|
|
124
|
+
"F", # pyflakes
|
|
125
|
+
"I", # isort
|
|
138
126
|
"UP", # pyupgrade
|
|
139
127
|
"C", # flake8-comprehensions
|
|
140
128
|
"B", # flake8-bugbear
|
|
@@ -169,14 +157,8 @@ convention = "google"
|
|
|
169
157
|
branch = true
|
|
170
158
|
relative_files = true
|
|
171
159
|
data_file = ".coverage"
|
|
172
|
-
source = [
|
|
173
|
-
|
|
174
|
-
]
|
|
175
|
-
omit = [
|
|
176
|
-
"*test*.py",
|
|
177
|
-
"tmp/*",
|
|
178
|
-
"pytest_cov",
|
|
179
|
-
]
|
|
160
|
+
source = ["statemachine"]
|
|
161
|
+
omit = ["*test*.py", "tmp/*", "pytest_cov"]
|
|
180
162
|
[tool.coverage.report]
|
|
181
163
|
show_missing = true
|
|
182
164
|
exclude_lines = [
|
|
@@ -190,7 +172,7 @@ exclude_lines = [
|
|
|
190
172
|
# Don't complain if tests don't hit defensive assertion code:
|
|
191
173
|
"raise AssertionError",
|
|
192
174
|
"raise NotImplementedError",
|
|
193
|
-
|
|
175
|
+
"if TYPE_CHECKING",
|
|
194
176
|
]
|
|
195
177
|
|
|
196
178
|
[tool.coverage.html]
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
from .event import Event
|
|
1
2
|
from .state import State
|
|
2
3
|
from .statemachine import StateMachine
|
|
3
4
|
|
|
4
5
|
__author__ = """Fernando Macedo"""
|
|
5
6
|
__email__ = "fgmacedo@gmail.com"
|
|
6
|
-
__version__ = "2.
|
|
7
|
+
__version__ = "2.4.0"
|
|
7
8
|
|
|
8
|
-
__all__ = ["StateMachine", "State"]
|
|
9
|
+
__all__ = ["StateMachine", "State", "Event"]
|
|
@@ -5,19 +5,30 @@ from collections import deque
|
|
|
5
5
|
from enum import IntEnum
|
|
6
6
|
from enum import IntFlag
|
|
7
7
|
from enum import auto
|
|
8
|
+
from functools import partial
|
|
9
|
+
from functools import reduce
|
|
8
10
|
from inspect import isawaitable
|
|
9
11
|
from inspect import iscoroutinefunction
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
10
13
|
from typing import Callable
|
|
11
14
|
from typing import Dict
|
|
12
15
|
from typing import Generator
|
|
13
16
|
from typing import Iterable
|
|
14
17
|
from typing import List
|
|
18
|
+
from typing import Set
|
|
15
19
|
from typing import Type
|
|
16
20
|
|
|
17
21
|
from .exceptions import AttrNotFound
|
|
22
|
+
from .exceptions import InvalidDefinition
|
|
18
23
|
from .i18n import _
|
|
24
|
+
from .spec_parser import custom_and
|
|
25
|
+
from .spec_parser import operator_mapping
|
|
26
|
+
from .spec_parser import parse_boolean_expr
|
|
19
27
|
from .utils import ensure_iterable
|
|
20
28
|
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from statemachine.dispatcher import Listeners
|
|
31
|
+
|
|
21
32
|
|
|
22
33
|
class CallbackPriority(IntEnum):
|
|
23
34
|
GENERIC = 0
|
|
@@ -54,6 +65,17 @@ def allways_true(*args, **kwargs):
|
|
|
54
65
|
return True
|
|
55
66
|
|
|
56
67
|
|
|
68
|
+
def take_callback(name: str, resolver: "Listeners", not_found_handler: Callable) -> Callable:
|
|
69
|
+
callbacks = list(resolver.search_name(name))
|
|
70
|
+
if len(callbacks) == 0:
|
|
71
|
+
not_found_handler(name)
|
|
72
|
+
return allways_true
|
|
73
|
+
elif len(callbacks) == 1:
|
|
74
|
+
return callbacks[0]
|
|
75
|
+
else:
|
|
76
|
+
return reduce(custom_and, callbacks)
|
|
77
|
+
|
|
78
|
+
|
|
57
79
|
class CallbackSpec:
|
|
58
80
|
"""Specs about callbacks.
|
|
59
81
|
|
|
@@ -110,7 +132,16 @@ class CallbackSpec:
|
|
|
110
132
|
self.reference = SpecReference.CALLABLE
|
|
111
133
|
self.attr_name = attr_name
|
|
112
134
|
|
|
113
|
-
def
|
|
135
|
+
def _wrap(self, callback):
|
|
136
|
+
condition = self.cond if self.cond is not None else allways_true
|
|
137
|
+
return CallbackWrapper(
|
|
138
|
+
callback=callback,
|
|
139
|
+
condition=condition,
|
|
140
|
+
meta=self,
|
|
141
|
+
unique_key=callback.unique_key,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def build(self, resolver: "Listeners") -> Generator["CallbackWrapper", None, None]:
|
|
114
145
|
"""
|
|
115
146
|
Resolves the `func` into a usable callable.
|
|
116
147
|
|
|
@@ -118,14 +149,29 @@ class CallbackSpec:
|
|
|
118
149
|
resolver (callable): A method responsible to build and return a valid callable that
|
|
119
150
|
can receive arbitrary parameters like `*args, **kwargs`.
|
|
120
151
|
"""
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
152
|
+
if (
|
|
153
|
+
not self.is_convention
|
|
154
|
+
and self.group == CallbackGroup.COND
|
|
155
|
+
and self.reference == SpecReference.NAME
|
|
156
|
+
):
|
|
157
|
+
names_not_found: Set[str] = set()
|
|
158
|
+
take_callback_partial = partial(
|
|
159
|
+
take_callback, resolver=resolver, not_found_handler=names_not_found.add
|
|
128
160
|
)
|
|
161
|
+
try:
|
|
162
|
+
expression = parse_boolean_expr(self.func, take_callback_partial, operator_mapping)
|
|
163
|
+
except SyntaxError as err:
|
|
164
|
+
raise InvalidDefinition(
|
|
165
|
+
_("Failed to parse boolean expression '{}'").format(self.func)
|
|
166
|
+
) from err
|
|
167
|
+
if not expression or names_not_found:
|
|
168
|
+
self.names_not_found = names_not_found
|
|
169
|
+
return
|
|
170
|
+
yield self._wrap(expression)
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
for callback in resolver.search(self):
|
|
174
|
+
yield self._wrap(callback)
|
|
129
175
|
|
|
130
176
|
|
|
131
177
|
class SpecListGrouper:
|
|
@@ -292,7 +338,7 @@ class CallbacksExecutor:
|
|
|
292
338
|
def __str__(self):
|
|
293
339
|
return ", ".join(str(c) for c in self)
|
|
294
340
|
|
|
295
|
-
def _add(self, spec: CallbackSpec, resolver:
|
|
341
|
+
def _add(self, spec: CallbackSpec, resolver: "Listeners"):
|
|
296
342
|
for callback in spec.build(resolver):
|
|
297
343
|
if callback.unique_key in self.items_already_seen:
|
|
298
344
|
continue
|
|
@@ -300,7 +346,7 @@ class CallbacksExecutor:
|
|
|
300
346
|
self.items_already_seen.add(callback.unique_key)
|
|
301
347
|
insort(self.items, callback)
|
|
302
348
|
|
|
303
|
-
def add(self, items: Iterable[CallbackSpec], resolver:
|
|
349
|
+
def add(self, items: Iterable[CallbackSpec], resolver: "Listeners"):
|
|
304
350
|
"""Validate configurations"""
|
|
305
351
|
for item in items:
|
|
306
352
|
self._add(item, resolver)
|
|
@@ -356,6 +402,12 @@ class CallbacksRegistry:
|
|
|
356
402
|
callback for callback in self[meta.group.build_key(specs)] if callback.meta == meta
|
|
357
403
|
):
|
|
358
404
|
continue
|
|
405
|
+
if hasattr(meta, "names_not_found"):
|
|
406
|
+
raise AttrNotFound(
|
|
407
|
+
_("Did not found name '{}' from model or statemachine").format(
|
|
408
|
+
", ".join(meta.names_not_found)
|
|
409
|
+
),
|
|
410
|
+
)
|
|
359
411
|
raise AttrNotFound(
|
|
360
412
|
_("Did not found name '{}' from model or statemachine").format(meta.func)
|
|
361
413
|
)
|
|
@@ -166,10 +166,6 @@ def quickchart_write_svg(sm: StateMachine, path: str):
|
|
|
166
166
|
>>> sm = OrderControl()
|
|
167
167
|
>>> print(sm._graph().to_string())
|
|
168
168
|
digraph list {
|
|
169
|
-
fontname=Arial;
|
|
170
|
-
fontsize=10;
|
|
171
|
-
label=OrderControl;
|
|
172
|
-
rankdir=LR;
|
|
173
169
|
...
|
|
174
170
|
|
|
175
171
|
To give you an example, we included this method that will serialize the dot, request the graph
|
|
@@ -10,6 +10,7 @@ from typing import Tuple
|
|
|
10
10
|
|
|
11
11
|
from .callbacks import SPECS_ALL
|
|
12
12
|
from .callbacks import SpecReference
|
|
13
|
+
from .event import Event
|
|
13
14
|
from .signature import SignatureAdapter
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
@@ -75,7 +76,7 @@ class Listeners:
|
|
|
75
76
|
|
|
76
77
|
def search(self, spec: "CallbackSpec") -> Generator["Callable", None, None]:
|
|
77
78
|
if spec.reference is SpecReference.NAME:
|
|
78
|
-
yield from self.
|
|
79
|
+
yield from self.search_name(spec.func)
|
|
79
80
|
return
|
|
80
81
|
elif spec.reference is SpecReference.CALLABLE:
|
|
81
82
|
yield self._search_callable(spec)
|
|
@@ -111,7 +112,7 @@ class Listeners:
|
|
|
111
112
|
|
|
112
113
|
return callable_method(spec.attr_name, spec.func, None)
|
|
113
114
|
|
|
114
|
-
def
|
|
115
|
+
def search_name(self, name) -> Generator["Callable", None, None]:
|
|
115
116
|
for config in self.items:
|
|
116
117
|
if name not in config.all_attrs:
|
|
117
118
|
continue
|
|
@@ -121,7 +122,7 @@ class Listeners:
|
|
|
121
122
|
yield attr_method(name, config.obj, config.resolver_id)
|
|
122
123
|
continue
|
|
123
124
|
|
|
124
|
-
if
|
|
125
|
+
if isinstance(func, Event):
|
|
125
126
|
yield event_method(name, func, config.resolver_id)
|
|
126
127
|
continue
|
|
127
128
|
|
|
@@ -143,6 +144,7 @@ def attr_method(attribute, obj, resolver_id) -> Callable:
|
|
|
143
144
|
return getter(obj)
|
|
144
145
|
|
|
145
146
|
method.unique_key = f"{attribute}@{resolver_id}" # type: ignore[attr-defined]
|
|
147
|
+
method.__name__ = attribute
|
|
146
148
|
return method
|
|
147
149
|
|
|
148
150
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from inspect import isawaitable
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
from typing import List
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from statemachine.utils import run_async_from_sync
|
|
7
|
+
|
|
8
|
+
from .event_data import TriggerData
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .statemachine import StateMachine
|
|
12
|
+
from .transition_list import TransitionList
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_event_data_kwargs = {
|
|
16
|
+
"event_data",
|
|
17
|
+
"machine",
|
|
18
|
+
"event",
|
|
19
|
+
"model",
|
|
20
|
+
"transition",
|
|
21
|
+
"state",
|
|
22
|
+
"source",
|
|
23
|
+
"target",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Event(str):
|
|
28
|
+
"""An event is triggers a signal that something has happened.
|
|
29
|
+
|
|
30
|
+
They are send to a state machine and allow the state machine to react.
|
|
31
|
+
|
|
32
|
+
An event starts a :ref:`Transition`, which can be thought of as a “cause” that initiates a
|
|
33
|
+
change in the state of the system.
|
|
34
|
+
|
|
35
|
+
See also :ref:`events`.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
id: str
|
|
39
|
+
"""The event identifier."""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
"""The event name."""
|
|
43
|
+
|
|
44
|
+
_sm: "StateMachine | None" = None
|
|
45
|
+
"""The state machine instance."""
|
|
46
|
+
|
|
47
|
+
_transitions: "TransitionList | None" = None
|
|
48
|
+
_has_real_id = False
|
|
49
|
+
|
|
50
|
+
def __new__(
|
|
51
|
+
cls,
|
|
52
|
+
transitions: "str | TransitionList | None" = None,
|
|
53
|
+
id: "str | None" = None,
|
|
54
|
+
name: "str | None" = None,
|
|
55
|
+
_sm: "StateMachine | None" = None,
|
|
56
|
+
):
|
|
57
|
+
if isinstance(transitions, str):
|
|
58
|
+
id = transitions
|
|
59
|
+
transitions = None
|
|
60
|
+
|
|
61
|
+
_has_real_id = id is not None
|
|
62
|
+
id = str(id) if _has_real_id else f"__event__{uuid4().hex}"
|
|
63
|
+
|
|
64
|
+
instance = super().__new__(cls, id)
|
|
65
|
+
instance.id = id
|
|
66
|
+
if name:
|
|
67
|
+
instance.name = name
|
|
68
|
+
elif _has_real_id:
|
|
69
|
+
instance.name = str(id).replace("_", " ").capitalize()
|
|
70
|
+
else:
|
|
71
|
+
instance.name = ""
|
|
72
|
+
if transitions:
|
|
73
|
+
instance._transitions = transitions
|
|
74
|
+
instance._has_real_id = _has_real_id
|
|
75
|
+
instance._sm = _sm
|
|
76
|
+
return instance
|
|
77
|
+
|
|
78
|
+
def __repr__(self):
|
|
79
|
+
return f"{type(self).__name__}({self.id!r})"
|
|
80
|
+
|
|
81
|
+
def is_same_event(self, *_args, event: "str | None" = None, **_kwargs) -> bool:
|
|
82
|
+
return self == event
|
|
83
|
+
|
|
84
|
+
def __get__(self, instance, owner):
|
|
85
|
+
"""By implementing this method `Event` can be used as a property descriptor
|
|
86
|
+
|
|
87
|
+
When attached to a SM class, if the user tries to get the `Event` instance,
|
|
88
|
+
we intercept here and return a `BoundEvent` instance, so the user can call
|
|
89
|
+
it as a method with the correct SM instance.
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
if instance is None:
|
|
93
|
+
return self
|
|
94
|
+
return BoundEvent(id=self.id, name=self.name, _sm=instance)
|
|
95
|
+
|
|
96
|
+
def __call__(self, *args, **kwargs):
|
|
97
|
+
"""Send this event to the current state machine.
|
|
98
|
+
|
|
99
|
+
Triggering an event on a state machine means invoking or sending a signal, initiating the
|
|
100
|
+
process that may result in executing a transition.
|
|
101
|
+
"""
|
|
102
|
+
# The `__call__` is declared here to help IDEs knowing that an `Event`
|
|
103
|
+
# can be called as a method. But it is not meant to be called without
|
|
104
|
+
# an SM instance. Such SM instance is provided by `__get__` method when
|
|
105
|
+
# used as a property descriptor.
|
|
106
|
+
|
|
107
|
+
machine = self._sm
|
|
108
|
+
kwargs = {k: v for k, v in kwargs.items() if k not in _event_data_kwargs}
|
|
109
|
+
trigger_data = TriggerData(
|
|
110
|
+
machine=machine,
|
|
111
|
+
event=self,
|
|
112
|
+
args=args,
|
|
113
|
+
kwargs=kwargs,
|
|
114
|
+
)
|
|
115
|
+
machine._put_nonblocking(trigger_data)
|
|
116
|
+
result = machine._processing_loop()
|
|
117
|
+
if not isawaitable(result):
|
|
118
|
+
return result
|
|
119
|
+
return run_async_from_sync(result)
|
|
120
|
+
|
|
121
|
+
def split( # type: ignore[override]
|
|
122
|
+
self, sep: "str | None" = None, maxsplit: int = -1
|
|
123
|
+
) -> List["Event"]:
|
|
124
|
+
result = super().split(sep, maxsplit)
|
|
125
|
+
if len(result) == 1:
|
|
126
|
+
return [self]
|
|
127
|
+
return [Event(event) for event in result]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class BoundEvent(Event):
|
|
131
|
+
pass
|
|
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
|
+
from .event import Event
|
|
7
8
|
from .state import State
|
|
8
9
|
from .statemachine import StateMachine
|
|
9
10
|
from .transition import Transition
|
|
@@ -13,7 +14,7 @@ if TYPE_CHECKING:
|
|
|
13
14
|
class TriggerData:
|
|
14
15
|
machine: "StateMachine"
|
|
15
16
|
|
|
16
|
-
event:
|
|
17
|
+
event: "Event"
|
|
17
18
|
"""The Event that was triggered."""
|
|
18
19
|
|
|
19
20
|
model: Any = field(init=False)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from statemachine.event import Event
|
|
2
|
+
|
|
3
|
+
from .utils import ensure_iterable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Events:
|
|
7
|
+
"""A collection of event names."""
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self._items: list[Event] = []
|
|
11
|
+
|
|
12
|
+
def __repr__(self):
|
|
13
|
+
sep = " " if len(self._items) > 1 else ""
|
|
14
|
+
return sep.join(item for item in self._items)
|
|
15
|
+
|
|
16
|
+
def __iter__(self):
|
|
17
|
+
return iter(self._items)
|
|
18
|
+
|
|
19
|
+
def add(self, events):
|
|
20
|
+
if events is None:
|
|
21
|
+
return self
|
|
22
|
+
|
|
23
|
+
unprepared = ensure_iterable(events)
|
|
24
|
+
for events in unprepared:
|
|
25
|
+
for event in events.split(" "):
|
|
26
|
+
if event in self._items:
|
|
27
|
+
continue
|
|
28
|
+
if isinstance(event, Event):
|
|
29
|
+
self._items.append(event)
|
|
30
|
+
else:
|
|
31
|
+
self._items.append(Event(id=event, name=event))
|
|
32
|
+
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def match(self, event: str):
|
|
36
|
+
return any(e == event for e in self)
|
|
37
|
+
|
|
38
|
+
def _replace(self, old, new):
|
|
39
|
+
self._items.remove(old)
|
|
40
|
+
self._items.append(new)
|
|
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
|
|
|
3
3
|
from .i18n import _
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
|
+
from .event import Event
|
|
6
7
|
from .state import State
|
|
7
8
|
|
|
8
9
|
|
|
@@ -31,8 +32,8 @@ class AttrNotFound(InvalidDefinition):
|
|
|
31
32
|
class TransitionNotAllowed(StateMachineError):
|
|
32
33
|
"Raised when there's no transition that can run from the current :ref:`state`."
|
|
33
34
|
|
|
34
|
-
def __init__(self, event:
|
|
35
|
+
def __init__(self, event: "Event", state: "State"):
|
|
35
36
|
self.event = event
|
|
36
37
|
self.state = state
|
|
37
|
-
msg = _("Can't {} when in {}.").format(self.event, self.state.name)
|
|
38
|
+
msg = _("Can't {} when in {}.").format(self.event.name, self.state.name)
|
|
38
39
|
super().__init__(msg)
|