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.
Files changed (33) hide show
  1. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/PKG-INFO +3 -7
  2. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/pyproject.toml +49 -41
  3. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/__init__.py +1 -1
  4. python_statemachine-2.2.0/statemachine/callbacks.py +285 -0
  5. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/contrib/diagram.py +20 -6
  6. python_statemachine-2.2.0/statemachine/dispatcher.py +156 -0
  7. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/factory.py +71 -7
  8. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/graph.py +6 -0
  9. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/mixins.py +1 -3
  10. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/signature.py +47 -13
  11. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/state.py +93 -38
  12. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/statemachine.py +74 -74
  13. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/states.py +6 -4
  14. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/transition.py +63 -40
  15. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/utils.py +5 -5
  16. python_statemachine-2.1.1/setup.py +0 -27
  17. python_statemachine-2.1.1/statemachine/callbacks.py +0 -172
  18. python_statemachine-2.1.1/statemachine/dispatcher.py +0 -101
  19. python_statemachine-2.1.1/statemachine/locale/en/LC_MESSAGES/statemachine.mo +0 -0
  20. python_statemachine-2.1.1/statemachine/locale/pt_BR/LC_MESSAGES/statemachine.mo +0 -0
  21. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/LICENSE +0 -0
  22. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/README.md +0 -0
  23. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/contrib/__init__.py +0 -0
  24. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/event.py +0 -0
  25. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/event_data.py +0 -0
  26. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/events.py +0 -0
  27. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/exceptions.py +0 -0
  28. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/i18n.py +0 -0
  29. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/locale/en/LC_MESSAGES/statemachine.po +0 -0
  30. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +0 -0
  31. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/model.py +0 -0
  32. {python_statemachine-2.1.1 → python_statemachine-2.2.0}/statemachine/registry.py +0 -0
  33. {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.1.1
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.7,<3.12
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.10
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.1.1"
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.7, <3.12"
37
+ python = ">=3.9, <3.13"
37
38
 
38
39
  [tool.poetry.group.dev.dependencies]
39
- pytest = "^7.2.0"
40
- pytest-cov = "^4.0.0"
41
- pytest-sugar = "^0.9.6"
42
- pydot = "^1.4.2"
43
- ruff = "^0.0.257"
44
- pre-commit = "^2.21.0"
45
- mypy = "^0.991"
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 = "4.5.0"
52
- sphinx-rtd-theme = "1.1.1"
53
- myst-parser = "^0.18.1"
54
- sphinx-gallery = "^0.11.1"
55
- pillow = "^9.4.0"
56
- sphinx-autobuild = "^2021.3.14"
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.11"
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
- # Assume Python 3.11.
134
- target-version = "py311"
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
 
@@ -3,6 +3,6 @@ from .statemachine import StateMachine
3
3
 
4
4
  __author__ = """Fernando Macedo"""
5
5
  __email__ = "fgmacedo@gmail.com"
6
- __version__ = "2.1.1"
6
+ __version__ = "2.2.0"
7
7
 
8
8
  __all__ = ["StateMachine", "State"]
@@ -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
- entry = ", ".join([str(action) for action in state.enter])
71
- exit = ", ".join([str(action) for action in state.exit])
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!s}"
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 exit:
81
- exit = f"exit / {exit}"
94
+ if exit_:
95
+ exit_ = f"exit / {exit_}"
82
96
 
83
- actions = "\n".join(x for x in [entry, exit, internal] if x)
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)