python-statemachine 2.1.1__tar.gz → 2.2.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.1.1 → python_statemachine-2.2.0}/PKG-INFO +3 -7
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/pyproject.toml +49 -41
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/__init__.py +1 -1
- python_statemachine-2.2.0/statemachine/callbacks.py +285 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/contrib/diagram.py +20 -6
- python_statemachine-2.2.0/statemachine/dispatcher.py +156 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/factory.py +71 -7
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/graph.py +6 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/mixins.py +1 -3
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/signature.py +47 -13
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/state.py +93 -38
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/statemachine.py +74 -74
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/states.py +6 -4
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/transition.py +63 -40
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/utils.py +5 -5
- python_statemachine-2.1.1/setup.py +0 -27
- python_statemachine-2.1.1/statemachine/callbacks.py +0 -172
- python_statemachine-2.1.1/statemachine/dispatcher.py +0 -101
- python_statemachine-2.1.1/statemachine/locale/en/LC_MESSAGES/statemachine.mo +0 -0
- python_statemachine-2.1.1/statemachine/locale/pt_BR/LC_MESSAGES/statemachine.mo +0 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/LICENSE +0 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/README.md +0 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/contrib/__init__.py +0 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/event.py +0 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/event_data.py +0 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/events.py +0 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/exceptions.py +0 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/i18n.py +0 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/locale/en/LC_MESSAGES/statemachine.po +0 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +0 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/model.py +0 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/registry.py +0 -0
- {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/transition_list.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-statemachine
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.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,21 +8,17 @@ Author: Fernando Macedo
|
|
|
8
8
|
Author-email: fgmacedo@gmail.com
|
|
9
9
|
Maintainer: Fernando Macedo
|
|
10
10
|
Maintainer-email: fgmacedo@gmail.com
|
|
11
|
-
Requires-Python: >=3.
|
|
11
|
+
Requires-Python: >=3.9,<3.13
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
14
|
Classifier: Natural Language :: English
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
18
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
19
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
20
|
Classifier: Programming Language :: Python :: 3.7
|
|
24
21
|
Classifier: Programming Language :: Python :: 3.8
|
|
25
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
26
22
|
Classifier: Topic :: Software Development :: Libraries
|
|
27
23
|
Provides-Extra: diagrams
|
|
28
24
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "python-statemachine"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.2.0"
|
|
4
4
|
description = "Python Finite State Machines made easy."
|
|
5
5
|
authors = ["Fernando Macedo <fgmacedo@gmail.com>"]
|
|
6
6
|
maintainers = [
|
|
@@ -26,6 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Programming Language :: Python :: 3.9",
|
|
27
27
|
"Programming Language :: Python :: 3.10",
|
|
28
28
|
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
29
30
|
"Topic :: Software Development :: Libraries"
|
|
30
31
|
]
|
|
31
32
|
|
|
@@ -33,40 +34,41 @@ classifiers = [
|
|
|
33
34
|
diagrams = ["pydot"]
|
|
34
35
|
|
|
35
36
|
[tool.poetry.dependencies]
|
|
36
|
-
python = ">=3.
|
|
37
|
+
python = ">=3.9, <3.13"
|
|
37
38
|
|
|
38
39
|
[tool.poetry.group.dev.dependencies]
|
|
39
|
-
pytest = "^
|
|
40
|
-
pytest-cov = "^
|
|
41
|
-
pytest-sugar = "^0.
|
|
42
|
-
pydot = "^
|
|
43
|
-
ruff = "^0.
|
|
44
|
-
pre-commit = "^
|
|
45
|
-
mypy = "^0
|
|
46
|
-
black = "^22.12.0"
|
|
47
|
-
pdbpp = "^0.10.3"
|
|
40
|
+
pytest = "^8.1.1"
|
|
41
|
+
pytest-cov = "^5.0.0"
|
|
42
|
+
pytest-sugar = "^1.0.0"
|
|
43
|
+
pydot = "^2.0.0"
|
|
44
|
+
ruff = "^0.3.7"
|
|
45
|
+
pre-commit = "^3.7.0"
|
|
46
|
+
mypy = "^1.9.0"
|
|
48
47
|
pytest-mock = "^3.10.0"
|
|
48
|
+
pytest-profiling = "^1.7.0"
|
|
49
|
+
pytest-benchmark = "^4.0.0"
|
|
49
50
|
|
|
50
51
|
[tool.poetry.group.docs.dependencies]
|
|
51
|
-
Sphinx = "
|
|
52
|
-
sphinx-rtd-theme = "
|
|
53
|
-
myst-parser = "^0.
|
|
54
|
-
sphinx-gallery = "^0.
|
|
55
|
-
pillow = "^
|
|
56
|
-
sphinx-autobuild = "^
|
|
52
|
+
Sphinx = "7.2.6"
|
|
53
|
+
sphinx-rtd-theme = "2.0.0"
|
|
54
|
+
myst-parser = "^2.0.0"
|
|
55
|
+
sphinx-gallery = "^0.15.0"
|
|
56
|
+
pillow = "^10.3.0"
|
|
57
|
+
sphinx-autobuild = "^2024.4.16"
|
|
57
58
|
|
|
58
59
|
[build-system]
|
|
59
60
|
requires = ["poetry-core"]
|
|
60
61
|
build-backend = "poetry.core.masonry.api"
|
|
61
62
|
|
|
62
63
|
[tool.pytest.ini_options]
|
|
63
|
-
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"
|
|
64
|
+
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"
|
|
64
65
|
doctest_optionflags = "ELLIPSIS IGNORE_EXCEPTION_DETAIL NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL"
|
|
65
66
|
|
|
66
67
|
[tool.mypy]
|
|
67
|
-
python_version = "3.
|
|
68
|
+
python_version = "3.12"
|
|
68
69
|
warn_return_any = true
|
|
69
70
|
warn_unused_configs = true
|
|
71
|
+
disable_error_code = "annotation-unchecked"
|
|
70
72
|
|
|
71
73
|
[[tool.mypy.overrides]]
|
|
72
74
|
module = [
|
|
@@ -87,24 +89,8 @@ max-line-length = 99
|
|
|
87
89
|
[tool.ruff]
|
|
88
90
|
src = ["statemachine"]
|
|
89
91
|
|
|
90
|
-
# Enable Pyflakes and pycodestyle rules.
|
|
91
|
-
select = [
|
|
92
|
-
"E", # pycodestyle errors
|
|
93
|
-
"W", # pycodestyle warnings
|
|
94
|
-
"F", # pyflakes
|
|
95
|
-
"I", # isort
|
|
96
|
-
"UP", # pyupgrade
|
|
97
|
-
"C", # flake8-comprehensions
|
|
98
|
-
"B", # flake8-bugbear
|
|
99
|
-
"PT", # flake8-pytest-style
|
|
100
|
-
]
|
|
101
|
-
ignore = [
|
|
102
|
-
"UP006", # `use-pep585-annotation` Requires Python3.9+
|
|
103
|
-
"UP035", # `use-pep585-annotation` Requires Python3.9+
|
|
104
|
-
"UP038", # `use-pep585-annotation` Requires Python3.9+
|
|
105
|
-
]
|
|
106
|
-
|
|
107
92
|
line-length = 99
|
|
93
|
+
target-version = "py312"
|
|
108
94
|
|
|
109
95
|
# Exclude a variety of commonly ignored directories.
|
|
110
96
|
exclude = [
|
|
@@ -127,19 +113,41 @@ exclude = [
|
|
|
127
113
|
"venv",
|
|
128
114
|
]
|
|
129
115
|
|
|
116
|
+
[tool.ruff.lint]
|
|
117
|
+
|
|
118
|
+
# Enable Pyflakes and pycodestyle rules.
|
|
119
|
+
select = [
|
|
120
|
+
"E", # pycodestyle errors
|
|
121
|
+
"W", # pycodestyle warnings
|
|
122
|
+
"F", # pyflakes
|
|
123
|
+
"I", # isort
|
|
124
|
+
"UP", # pyupgrade
|
|
125
|
+
"C", # flake8-comprehensions
|
|
126
|
+
"B", # flake8-bugbear
|
|
127
|
+
"PT", # flake8-pytest-style
|
|
128
|
+
]
|
|
129
|
+
ignore = [
|
|
130
|
+
"UP006", # `use-pep585-annotation` Requires Python3.9+
|
|
131
|
+
"UP035", # `use-pep585-annotation` Requires Python3.9+
|
|
132
|
+
"UP038", # `use-pep585-annotation` Requires Python3.9+
|
|
133
|
+
]
|
|
134
|
+
|
|
130
135
|
# Allow unused variables when underscore-prefixed.
|
|
131
136
|
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
132
137
|
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
[tool.ruff.lint.per-file-ignores]
|
|
139
|
+
# Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`.
|
|
140
|
+
"__init__.py" = ["E402"]
|
|
141
|
+
"path/to/file.py" = ["E402"]
|
|
142
|
+
"tests/examples/**.py" = ["B018"]
|
|
135
143
|
|
|
136
|
-
[tool.ruff.mccabe]
|
|
144
|
+
[tool.ruff.lint.mccabe]
|
|
137
145
|
max-complexity = 6
|
|
138
146
|
|
|
139
|
-
[tool.ruff.isort]
|
|
147
|
+
[tool.ruff.lint.isort]
|
|
140
148
|
force-single-line = true
|
|
141
149
|
|
|
142
|
-
[tool.ruff.pydocstyle]
|
|
150
|
+
[tool.ruff.lint.pydocstyle]
|
|
143
151
|
# Use Google-style docstrings.
|
|
144
152
|
convention = "google"
|
|
145
153
|
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
from bisect import insort
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from collections import deque
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
from typing import Callable
|
|
6
|
+
from typing import Dict
|
|
7
|
+
from typing import Generator
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
from .exceptions import AttrNotFound
|
|
11
|
+
from .i18n import _
|
|
12
|
+
from .utils import ensure_iterable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CallbackPriority(IntEnum):
|
|
16
|
+
GENERIC = 0
|
|
17
|
+
INLINE = 10
|
|
18
|
+
DECORATOR = 20
|
|
19
|
+
NAMING = 30
|
|
20
|
+
AFTER = 40
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def allways_true(*args, **kwargs):
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CallbackWrapper:
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
callback: Callable,
|
|
31
|
+
condition: Callable,
|
|
32
|
+
meta: "CallbackMeta",
|
|
33
|
+
unique_key: str,
|
|
34
|
+
) -> None:
|
|
35
|
+
self._callback = callback
|
|
36
|
+
self.condition = condition
|
|
37
|
+
self.meta = meta
|
|
38
|
+
self.unique_key = unique_key
|
|
39
|
+
|
|
40
|
+
def __repr__(self):
|
|
41
|
+
return f"{type(self).__name__}({self.unique_key})"
|
|
42
|
+
|
|
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
|
+
def __call__(self, *args, **kwargs):
|
|
50
|
+
return self._callback(*args, **kwargs)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CallbackMeta:
|
|
54
|
+
"""A thin wrapper that register info about actions and guards.
|
|
55
|
+
|
|
56
|
+
At first, `func` can be a string or a callable, and even if it's already
|
|
57
|
+
a callable, his signature can mismatch.
|
|
58
|
+
|
|
59
|
+
After instantiation, `.setup(resolver)` must be called before any real
|
|
60
|
+
call is performed, to allow the proper callback resolution.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
func,
|
|
66
|
+
suppress_errors=False,
|
|
67
|
+
cond=None,
|
|
68
|
+
priority: CallbackPriority = CallbackPriority.NAMING,
|
|
69
|
+
expected_value=None,
|
|
70
|
+
):
|
|
71
|
+
self.func = func
|
|
72
|
+
self.suppress_errors = suppress_errors
|
|
73
|
+
self.cond = cond
|
|
74
|
+
self.expected_value = expected_value
|
|
75
|
+
self.priority = priority
|
|
76
|
+
|
|
77
|
+
def __repr__(self):
|
|
78
|
+
return f"{type(self).__name__}({self.func!r}, suppress_errors={self.suppress_errors!r})"
|
|
79
|
+
|
|
80
|
+
def __str__(self):
|
|
81
|
+
return getattr(self.func, "__name__", self.func)
|
|
82
|
+
|
|
83
|
+
def __eq__(self, other):
|
|
84
|
+
return self.func == other.func
|
|
85
|
+
|
|
86
|
+
def __hash__(self):
|
|
87
|
+
return id(self)
|
|
88
|
+
|
|
89
|
+
def _update_func(self, func):
|
|
90
|
+
self.func = func
|
|
91
|
+
|
|
92
|
+
def _wrap_callable(self, func, _expected_value):
|
|
93
|
+
return func
|
|
94
|
+
|
|
95
|
+
def build(self, resolver) -> Generator["CallbackWrapper", None, None]:
|
|
96
|
+
"""
|
|
97
|
+
Resolves the `func` into a usable callable.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
resolver (callable): A method responsible to build and return a valid callable that
|
|
101
|
+
can receive arbitrary parameters like `*args, **kwargs`.
|
|
102
|
+
"""
|
|
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,
|
|
109
|
+
unique_key=callback.unique_key,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class BoolCallbackMeta(CallbackMeta):
|
|
114
|
+
"""A thin wrapper that register info about actions and guards.
|
|
115
|
+
|
|
116
|
+
At first, `func` can be a string or a callable, and even if it's already
|
|
117
|
+
a callable, his signature can mismatch.
|
|
118
|
+
|
|
119
|
+
After instantiation, `.setup(resolver)` must be called before any real
|
|
120
|
+
call is performed, to allow the proper callback resolution.
|
|
121
|
+
"""
|
|
122
|
+
|
|
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
|
+
)
|
|
134
|
+
|
|
135
|
+
def __str__(self):
|
|
136
|
+
name = super().__str__()
|
|
137
|
+
return name if self.expected_value else f"!{name}"
|
|
138
|
+
|
|
139
|
+
def _wrap_callable(self, func, expected_value):
|
|
140
|
+
def bool_wrapper(*args, **kwargs):
|
|
141
|
+
return bool(func(*args, **kwargs)) == expected_value
|
|
142
|
+
|
|
143
|
+
return bool_wrapper
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class CallbackMetaList:
|
|
147
|
+
"""List of `CallbackMeta` instances"""
|
|
148
|
+
|
|
149
|
+
def __init__(self, factory=CallbackMeta):
|
|
150
|
+
self.items: List[CallbackMeta] = []
|
|
151
|
+
self.factory = factory
|
|
152
|
+
|
|
153
|
+
def __repr__(self):
|
|
154
|
+
return f"{type(self).__name__}({self.items!r}, factory={self.factory!r})"
|
|
155
|
+
|
|
156
|
+
def __str__(self):
|
|
157
|
+
return ", ".join(str(c) for c in self)
|
|
158
|
+
|
|
159
|
+
def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
|
|
160
|
+
"""This list was a target for adding a func using decorator
|
|
161
|
+
`@<state|event>[.on|before|after|enter|exit]` syntax.
|
|
162
|
+
|
|
163
|
+
If we assign ``func`` directly as callable on the ``items`` list,
|
|
164
|
+
this will result in an `unbounded method error`, with `func` expecting a parameter
|
|
165
|
+
``self`` not defined.
|
|
166
|
+
|
|
167
|
+
The implemented solution is to resolve the collision giving the func a reference method.
|
|
168
|
+
To update It's callback when the name is resolved on the
|
|
169
|
+
:func:`StateMachineMetaclass.add_from_attributes`.
|
|
170
|
+
If the ``func`` is bounded It will be used directly, if not, it's ref will be replaced
|
|
171
|
+
by the given attr name and on `statemachine._setup()` the dynamic name will be resolved
|
|
172
|
+
properly.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
func (callable): The decorated method to add on the transitions occurs.
|
|
176
|
+
is_event (bool): If the func is also an event, we'll create a trigger and link the
|
|
177
|
+
event name to the transitions.
|
|
178
|
+
transitions (TransitionList): If ``is_event``, the transitions to be attached to the
|
|
179
|
+
event.
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
callback = self._add(func, **kwargs)
|
|
183
|
+
if not getattr(func, "_callbacks_to_update", None):
|
|
184
|
+
func._callbacks_to_update = set()
|
|
185
|
+
func._callbacks_to_update.add(callback._update_func)
|
|
186
|
+
func._is_event = is_event
|
|
187
|
+
func._transitions = transitions
|
|
188
|
+
|
|
189
|
+
return func
|
|
190
|
+
|
|
191
|
+
def __call__(self, callback):
|
|
192
|
+
"""Allows usage of the callback list as a decorator."""
|
|
193
|
+
return self._add_unbounded_callback(callback)
|
|
194
|
+
|
|
195
|
+
def __iter__(self):
|
|
196
|
+
return iter(self.items)
|
|
197
|
+
|
|
198
|
+
def clear(self):
|
|
199
|
+
self.items = []
|
|
200
|
+
|
|
201
|
+
def _add(self, func, **kwargs):
|
|
202
|
+
meta = self.factory(func, **kwargs)
|
|
203
|
+
|
|
204
|
+
if meta in self.items:
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
self.items.append(meta)
|
|
208
|
+
return meta
|
|
209
|
+
|
|
210
|
+
def add(self, callbacks, **kwargs):
|
|
211
|
+
if callbacks is None:
|
|
212
|
+
return self
|
|
213
|
+
|
|
214
|
+
unprepared = ensure_iterable(callbacks)
|
|
215
|
+
for func in unprepared:
|
|
216
|
+
self._add(func, **kwargs)
|
|
217
|
+
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class CallbacksExecutor:
|
|
222
|
+
"""A list of callbacks that can be executed in order."""
|
|
223
|
+
|
|
224
|
+
def __init__(self):
|
|
225
|
+
self.items: List[CallbackWrapper] = deque()
|
|
226
|
+
self.items_already_seen = set()
|
|
227
|
+
|
|
228
|
+
def __iter__(self):
|
|
229
|
+
return iter(self.items)
|
|
230
|
+
|
|
231
|
+
def __repr__(self):
|
|
232
|
+
return f"{type(self).__name__}({self.items!r})"
|
|
233
|
+
|
|
234
|
+
def __str__(self):
|
|
235
|
+
return ", ".join(str(c) for c in self)
|
|
236
|
+
|
|
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
|
|
241
|
+
|
|
242
|
+
self.items_already_seen.add(callback.unique_key)
|
|
243
|
+
insort(self.items, callback)
|
|
244
|
+
|
|
245
|
+
def add(self, items: CallbackMetaList, resolver: Callable):
|
|
246
|
+
"""Validate configurations"""
|
|
247
|
+
for item in items:
|
|
248
|
+
self._add(item, resolver)
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
def call(self, *args, **kwargs):
|
|
252
|
+
return [
|
|
253
|
+
callback(*args, **kwargs) for callback in self if callback.condition(*args, **kwargs)
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
def all(self, *args, **kwargs):
|
|
257
|
+
return all(condition(*args, **kwargs) for condition in self)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class CallbacksRegistry:
|
|
261
|
+
def __init__(self) -> None:
|
|
262
|
+
self._registry: Dict[CallbackMetaList, CallbacksExecutor] = defaultdict(CallbacksExecutor)
|
|
263
|
+
|
|
264
|
+
def register(self, meta_list: CallbackMetaList, resolver):
|
|
265
|
+
executor_list = self[meta_list]
|
|
266
|
+
executor_list.add(meta_list, resolver)
|
|
267
|
+
return executor_list
|
|
268
|
+
|
|
269
|
+
def clear(self):
|
|
270
|
+
self._registry.clear()
|
|
271
|
+
|
|
272
|
+
def __getitem__(self, meta_list: CallbackMetaList) -> CallbacksExecutor:
|
|
273
|
+
return self._registry[meta_list]
|
|
274
|
+
|
|
275
|
+
def check(self, meta_list: CallbackMetaList):
|
|
276
|
+
executor = self[meta_list]
|
|
277
|
+
for meta in meta_list:
|
|
278
|
+
if meta.suppress_errors:
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
if any(callback for callback in executor if callback.meta == meta):
|
|
282
|
+
continue
|
|
283
|
+
raise AttrNotFound(
|
|
284
|
+
_("Did not found name '{}' from model or statemachine").format(meta.func)
|
|
285
|
+
)
|
|
@@ -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}"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from collections import namedtuple
|
|
2
|
+
from operator import attrgetter
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import Generator
|
|
5
|
+
|
|
6
|
+
from .signature import SignatureAdapter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ObjectConfig(namedtuple("ObjectConfig", "obj skip_attrs resolver_id")):
|
|
10
|
+
"""Configuration for objects passed to resolver_factory.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
obj: Any object that will serve as lookup for attributes.
|
|
14
|
+
skip_attrs: Protected attrs that will be ignored on the search.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_obj(cls, obj, skip_attrs=None):
|
|
19
|
+
if isinstance(obj, ObjectConfig):
|
|
20
|
+
return obj
|
|
21
|
+
else:
|
|
22
|
+
return cls(obj, skip_attrs or set(), str(id(obj)))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class WrapSearchResult:
|
|
26
|
+
def __init__(self, attribute, resolver_id) -> None:
|
|
27
|
+
self.attribute = attribute
|
|
28
|
+
self.resolver_id = resolver_id
|
|
29
|
+
self._cache = None
|
|
30
|
+
self.unique_key = f"{attribute}@{resolver_id}"
|
|
31
|
+
|
|
32
|
+
def __repr__(self):
|
|
33
|
+
return f"{type(self).__name__}({self.unique_key})"
|
|
34
|
+
|
|
35
|
+
def wrap(self): # pragma: no cover
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def __call__(self, *args: Any, **kwds: Any) -> Any:
|
|
39
|
+
if self._cache is None:
|
|
40
|
+
self._cache = self.wrap()
|
|
41
|
+
assert self._cache
|
|
42
|
+
return self._cache(*args, **kwds)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CallableSearchResult(WrapSearchResult):
|
|
46
|
+
def __init__(self, attribute, a_callable, resolver_id) -> None:
|
|
47
|
+
self.a_callable = a_callable
|
|
48
|
+
super().__init__(attribute, resolver_id)
|
|
49
|
+
|
|
50
|
+
def wrap(self):
|
|
51
|
+
return SignatureAdapter.wrap(self.a_callable)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AttributeCallableSearchResult(WrapSearchResult):
|
|
55
|
+
def __init__(self, attribute, obj, resolver_id) -> None:
|
|
56
|
+
self.obj = obj
|
|
57
|
+
super().__init__(attribute, resolver_id)
|
|
58
|
+
|
|
59
|
+
def wrap(self):
|
|
60
|
+
# if `attr` is not callable, then it's an attribute or property,
|
|
61
|
+
# so `func` contains it's current value.
|
|
62
|
+
# we'll build a method that get's the fresh value for each call
|
|
63
|
+
getter = attrgetter(self.attribute)
|
|
64
|
+
|
|
65
|
+
def wrapper(*args, **kwargs):
|
|
66
|
+
return getter(self.obj)
|
|
67
|
+
|
|
68
|
+
return wrapper
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class EventSearchResult(WrapSearchResult):
|
|
72
|
+
def __init__(self, attribute, func, resolver_id) -> None:
|
|
73
|
+
self.func = func
|
|
74
|
+
super().__init__(attribute, resolver_id)
|
|
75
|
+
|
|
76
|
+
def wrap(self):
|
|
77
|
+
"Events already have the 'machine' parameter defined."
|
|
78
|
+
|
|
79
|
+
def wrapper(*args, **kwargs):
|
|
80
|
+
kwargs.pop("machine", None)
|
|
81
|
+
return self.func(*args, **kwargs)
|
|
82
|
+
|
|
83
|
+
return wrapper
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _search_callable_attr_is_property(
|
|
87
|
+
attr, configs: tuple[ObjectConfig]
|
|
88
|
+
) -> "WrapSearchResult | None":
|
|
89
|
+
# if the attr is a property, we'll try to find the object that has the
|
|
90
|
+
# property on the configs
|
|
91
|
+
attr_name = attr.fget.__name__
|
|
92
|
+
for obj, _skip_attrs, resolver_id in configs:
|
|
93
|
+
func = getattr(type(obj), attr_name, None)
|
|
94
|
+
if func is not None and func is attr:
|
|
95
|
+
return AttributeCallableSearchResult(attr_name, obj, resolver_id)
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _search_callable_attr_is_callable(attr, configs: tuple[ObjectConfig]) -> WrapSearchResult:
|
|
100
|
+
# if the attr is an unbounded method, we'll try to find the bounded method
|
|
101
|
+
# on the configs
|
|
102
|
+
if not hasattr(attr, "__self__"):
|
|
103
|
+
for obj, _skip_attrs, resolver_id in configs:
|
|
104
|
+
func = getattr(obj, attr.__name__, None)
|
|
105
|
+
if func is not None and func.__func__ is attr:
|
|
106
|
+
return CallableSearchResult(attr.__name__, func, resolver_id)
|
|
107
|
+
|
|
108
|
+
return CallableSearchResult(attr, attr, None)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _search_callable_in_configs(
|
|
112
|
+
attr, configs: tuple[ObjectConfig]
|
|
113
|
+
) -> Generator[WrapSearchResult, None, None]:
|
|
114
|
+
for obj, skip_attrs, resolver_id in configs:
|
|
115
|
+
if attr in skip_attrs:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
if not hasattr(obj, attr):
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
func = getattr(obj, attr)
|
|
122
|
+
if not callable(func):
|
|
123
|
+
yield AttributeCallableSearchResult(attr, obj, resolver_id)
|
|
124
|
+
|
|
125
|
+
if getattr(func, "_is_sm_event", False):
|
|
126
|
+
yield EventSearchResult(attr, func, resolver_id)
|
|
127
|
+
|
|
128
|
+
yield CallableSearchResult(attr, func, resolver_id)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def search_callable(attr, configs: tuple) -> Generator[WrapSearchResult, None, None]: # noqa: C901
|
|
132
|
+
if isinstance(attr, property):
|
|
133
|
+
result = _search_callable_attr_is_property(attr, configs)
|
|
134
|
+
if result is not None:
|
|
135
|
+
yield result
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if callable(attr):
|
|
139
|
+
yield _search_callable_attr_is_callable(attr, configs)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
yield from _search_callable_in_configs(attr, configs)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def resolver_factory(objects: tuple[ObjectConfig]):
|
|
146
|
+
"""Factory that returns a configured resolver."""
|
|
147
|
+
|
|
148
|
+
def wrapper(attr) -> Generator[WrapSearchResult, None, None]:
|
|
149
|
+
yield from search_callable(attr, objects)
|
|
150
|
+
|
|
151
|
+
return wrapper
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def resolver_factory_from_objects(*objects: tuple[Any]):
|
|
155
|
+
configs = tuple(ObjectConfig.from_obj(o) for o in objects)
|
|
156
|
+
return resolver_factory(configs)
|