python-statemachine 2.1.1__py3-none-any.whl → 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -0,0 +1,28 @@
1
+ statemachine/__init__.py,sha256=nhqSQwRfWBVrcn4h-aLlxLbzeSsjt6lE_SSFnFWK0FY,192
2
+ statemachine/callbacks.py,sha256=QMh564YuNVNV74VxlIyvlF_eW6njsmV9VHauQH9JR1U,8704
3
+ statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ statemachine/contrib/diagram.py,sha256=CTlNRtAHEzCCzRUlxkiyzJOCOp1CohihdD0_7pHrx4c,7004
5
+ statemachine/dispatcher.py,sha256=i9ChhdomQpVbTg9vNS3xoCHUAw3Z32B0UPzK47nb1-A,4924
6
+ statemachine/event.py,sha256=fvWxyFcrbPVxNW94p66rGJ5I1UZUK9mDJz68udkz2JI,2106
7
+ statemachine/event_data.py,sha256=nxOfypngK-78A77gj4pS-oxLx9zl1S9wnSt_rTiCyZA,2257
8
+ statemachine/events.py,sha256=rUbiTX-QGoo7zzmQMEeWeLrIVnp9zhicdRIm34CoAVI,731
9
+ statemachine/exceptions.py,sha256=uzvDbHkzCZkmFI_02L0yC9gd6d0SBn9GmsI8ekc0Hjk,952
10
+ statemachine/factory.py,sha256=m2TeJ19eDIymoGADDp0G0ItrrcCRgIJoJYFJ5lzqmWE,8015
11
+ statemachine/graph.py,sha256=KtwB1CYckaLjTgQD9tEeuaEzJje9q3fPVpBViW5TgSk,487
12
+ statemachine/i18n.py,sha256=NLvGseaORmQ0G-V_J8tkjoxh_piWMOm2CI6mBQpLamc,362
13
+ statemachine/locale/en/LC_MESSAGES/statemachine.po,sha256=MfODCwzML6DzhOpHbno2Pdhsb26AWboR5Ka1JCNvJIA,1685
14
+ statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=KsR9rlnhw17EZG-IbVx3F0wJAJSK0B-OByGmUmDHDUM,2375
15
+ statemachine/mixins.py,sha256=B8WB3EGyZpMWAQg4Nw3kUMCfswgmLCChDpOQdxR28eE,1017
16
+ statemachine/model.py,sha256=OylI3FjMiHpYyDl9mtK1zEJMeSvemaN4giQDonpc8kI,211
17
+ statemachine/registry.py,sha256=RnVBRS3I_6Tm2OMgXMB_ewX7zQaslqEfhXFOhbqIkG4,959
18
+ statemachine/signature.py,sha256=9KItptchQlP2VpNmBiIFXLNAWNWY3Bs_ymUd7OOgDD4,7507
19
+ statemachine/state.py,sha256=bZki0DyemZa-aVAfollQCmpXxesWONzUmE28eDfqz3o,8315
20
+ statemachine/statemachine.py,sha256=U0rLIUhvsCivth1uNb0pt2xWVUnDbzhXF982yJH-bg0,11659
21
+ statemachine/states.py,sha256=gdGemJYF9k-cifs9Tk0Pe_-1u1Lanf3l08o0t8wAMgg,4203
22
+ statemachine/transition.py,sha256=ebRhJsjOoLya7oYZVjRAKvjq1EsC9ImpExgHANekVMA,5388
23
+ statemachine/transition_list.py,sha256=DatsmMWgK0YK30Nrj-josVvlTgeGapKutzYur9-puF8,5949
24
+ statemachine/utils.py,sha256=fV-Hz1gnyl4tAs9nPr2u9I9zfxq62mokPV8mUpIJc1k,458
25
+ python_statemachine-2.2.0.dist-info/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
26
+ python_statemachine-2.2.0.dist-info/METADATA,sha256=qH4SxonFNJStkKVocZABu-RLiYD54DcON2gNGjBPGGQ,11454
27
+ python_statemachine-2.2.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
28
+ python_statemachine-2.2.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.4.0
2
+ Generator: poetry-core 1.6.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
statemachine/__init__.py CHANGED
@@ -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"]
statemachine/callbacks.py CHANGED
@@ -1,11 +1,57 @@
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
+
1
10
  from .exceptions import AttrNotFound
2
- from .exceptions import InvalidDefinition
3
11
  from .i18n import _
4
12
  from .utils import ensure_iterable
5
13
 
6
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
+
7
27
  class CallbackWrapper:
8
- """A thin wrapper that ensures the target callback is a proper callable.
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.
9
55
 
10
56
  At first, `func` can be a string or a callable, and even if it's already
11
57
  a callable, his signature can mismatch.
@@ -14,21 +60,28 @@ class CallbackWrapper:
14
60
  call is performed, to allow the proper callback resolution.
15
61
  """
16
62
 
17
- def __init__(self, func, suppress_errors=False, cond=None):
63
+ def __init__(
64
+ self,
65
+ func,
66
+ suppress_errors=False,
67
+ cond=None,
68
+ priority: CallbackPriority = CallbackPriority.NAMING,
69
+ expected_value=None,
70
+ ):
18
71
  self.func = func
19
72
  self.suppress_errors = suppress_errors
20
- self.cond = Callbacks(factory=ConditionWrapper).add(cond)
21
- self._callback = None
22
- self._resolver_id = None
73
+ self.cond = cond
74
+ self.expected_value = expected_value
75
+ self.priority = priority
23
76
 
24
77
  def __repr__(self):
25
- return f"{type(self).__name__}({self.func!r})"
78
+ return f"{type(self).__name__}({self.func!r}, suppress_errors={self.suppress_errors!r})"
26
79
 
27
80
  def __str__(self):
28
81
  return getattr(self.func, "__name__", self.func)
29
82
 
30
83
  def __eq__(self, other):
31
- return self.func == other.func and self._resolver_id == other._resolver_id
84
+ return self.func == other.func
32
85
 
33
86
  def __hash__(self):
34
87
  return id(self)
@@ -36,7 +89,10 @@ class CallbackWrapper:
36
89
  def _update_func(self, func):
37
90
  self.func = func
38
91
 
39
- def setup(self, resolver):
92
+ def _wrap_callable(self, func, _expected_value):
93
+ return func
94
+
95
+ def build(self, resolver) -> Generator["CallbackWrapper", None, None]:
40
96
  """
41
97
  Resolves the `func` into a usable callable.
42
98
 
@@ -44,41 +100,54 @@ class CallbackWrapper:
44
100
  resolver (callable): A method responsible to build and return a valid callable that
45
101
  can receive arbitrary parameters like `*args, **kwargs`.
46
102
  """
47
- self.cond.setup(resolver)
48
- try:
49
- self._resolver_id = getattr(resolver, "id", id(resolver))
50
- self._callback = resolver(self.func)
51
- return True
52
- except AttrNotFound:
53
- if not self.suppress_errors:
54
- raise
55
- return False
56
-
57
- def __call__(self, *args, **kwargs):
58
- if self._callback is None:
59
- raise InvalidDefinition(
60
- _("Callback {!r} not property configured.").format(self)
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,
61
110
  )
62
- return self._callback(*args, **kwargs)
63
111
 
64
112
 
65
- class ConditionWrapper(CallbackWrapper):
66
- def __init__(self, func, suppress_errors=False, expected_value=True):
67
- super().__init__(func, suppress_errors)
68
- self.expected_value = expected_value
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
+ )
69
134
 
70
135
  def __str__(self):
71
136
  name = super().__str__()
72
137
  return name if self.expected_value else f"!{name}"
73
138
 
74
- def __call__(self, *args, **kwargs):
75
- return bool(super().__call__(*args, **kwargs)) == self.expected_value
139
+ def _wrap_callable(self, func, expected_value):
140
+ def bool_wrapper(*args, **kwargs):
141
+ return bool(func(*args, **kwargs)) == expected_value
76
142
 
143
+ return bool_wrapper
77
144
 
78
- class Callbacks:
79
- def __init__(self, resolver=None, factory=CallbackWrapper):
80
- self.items = []
81
- self._resolver = resolver
145
+
146
+ class CallbackMetaList:
147
+ """List of `CallbackMeta` instances"""
148
+
149
+ def __init__(self, factory=CallbackMeta):
150
+ self.items: List[CallbackMeta] = []
82
151
  self.factory = factory
83
152
 
84
153
  def __repr__(self):
@@ -87,13 +156,6 @@ class Callbacks:
87
156
  def __str__(self):
88
157
  return ", ".join(str(c) for c in self)
89
158
 
90
- def setup(self, resolver):
91
- """Validate configurations"""
92
- self._resolver = resolver
93
- self.items = [
94
- callback for callback in self.items if callback.setup(self._resolver)
95
- ]
96
-
97
159
  def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
98
160
  """This list was a target for adding a func using decorator
99
161
  `@<state|event>[.on|before|after|enter|exit]` syntax.
@@ -127,6 +189,7 @@ class Callbacks:
127
189
  return func
128
190
 
129
191
  def __call__(self, callback):
192
+ """Allows usage of the callback list as a decorator."""
130
193
  return self._add_unbounded_callback(callback)
131
194
 
132
195
  def __iter__(self):
@@ -135,31 +198,14 @@ class Callbacks:
135
198
  def clear(self):
136
199
  self.items = []
137
200
 
138
- def call(self, *args, **kwargs):
139
- return [
140
- callback(*args, **kwargs)
141
- for callback in self.items
142
- if callback.cond.all(*args, **kwargs)
143
- ]
144
-
145
- def all(self, *args, **kwargs):
146
- return all(condition(*args, **kwargs) for condition in self)
147
-
148
- def _add(self, func, resolver=None, prepend=False, **kwargs):
149
- resolver = resolver or self._resolver
201
+ def _add(self, func, **kwargs):
202
+ meta = self.factory(func, **kwargs)
150
203
 
151
- callback = self.factory(func, **kwargs)
152
- if resolver is not None and not callback.setup(resolver):
204
+ if meta in self.items:
153
205
  return
154
206
 
155
- if callback in self.items:
156
- return
157
-
158
- if prepend:
159
- self.items.insert(0, callback)
160
- else:
161
- self.items.append(callback)
162
- return callback
207
+ self.items.append(meta)
208
+ return meta
163
209
 
164
210
  def add(self, callbacks, **kwargs):
165
211
  if callbacks is None:
@@ -170,3 +216,70 @@ class Callbacks:
170
216
  self._add(func, **kwargs)
171
217
 
172
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}"
@@ -1,13 +1,12 @@
1
1
  from collections import namedtuple
2
- from functools import wraps
3
2
  from operator import attrgetter
3
+ from typing import Any
4
+ from typing import Generator
4
5
 
5
- from .exceptions import AttrNotFound
6
- from .i18n import _
7
6
  from .signature import SignatureAdapter
8
7
 
9
8
 
10
- class ObjectConfig(namedtuple("ObjectConfig", "obj skip_attrs")):
9
+ class ObjectConfig(namedtuple("ObjectConfig", "obj skip_attrs resolver_id")):
11
10
  """Configuration for objects passed to resolver_factory.
12
11
 
13
12
  Args:
@@ -16,86 +15,142 @@ class ObjectConfig(namedtuple("ObjectConfig", "obj skip_attrs")):
16
15
  """
17
16
 
18
17
  @classmethod
19
- def from_obj(cls, obj):
18
+ def from_obj(cls, obj, skip_attrs=None):
20
19
  if isinstance(obj, ObjectConfig):
21
20
  return obj
22
21
  else:
23
- return cls(obj, set())
22
+ return cls(obj, skip_attrs or set(), str(id(obj)))
24
23
 
25
24
 
26
- def _get_func_by_attr(attr, *configs):
27
- for config in configs:
28
- if attr in config.skip_attrs:
29
- continue
30
- func = getattr(config.obj, attr, None)
31
- if func is not None:
32
- break
33
- else:
34
- raise AttrNotFound(
35
- _("Did not found name '{}' from model or statemachine").format(attr)
36
- )
37
- return func, config.obj
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}"
38
31
 
32
+ def __repr__(self):
33
+ return f"{type(self).__name__}({self.unique_key})"
39
34
 
40
- def _build_attr_wrapper(attr: str, obj):
41
- # if `attr` is not callable, then it's an attribute or property,
42
- # so `func` contains it's current value.
43
- # we'll build a method that get's the fresh value for each call
44
- getter = attrgetter(attr)
35
+ def wrap(self): # pragma: no cover
36
+ pass
45
37
 
46
- def wrapper(*args, **kwargs):
47
- return getter(obj)
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)
48
43
 
49
- return wrapper
50
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)
51
49
 
52
- def _build_sm_event_wrapper(func):
53
- "Events already have the 'machine' parameter defined."
50
+ def wrap(self):
51
+ return SignatureAdapter.wrap(self.a_callable)
54
52
 
55
- def wrapper(*args, **kwargs):
56
- kwargs.pop("machine", None)
57
- return func(*args, **kwargs)
58
53
 
59
- return wrapper
54
+ class AttributeCallableSearchResult(WrapSearchResult):
55
+ def __init__(self, attribute, obj, resolver_id) -> None:
56
+ self.obj = obj
57
+ super().__init__(attribute, resolver_id)
60
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)
61
64
 
62
- def ensure_callable(attr, *objects):
63
- """Ensure that `attr` is a callable, if not, tries to retrieve one from any of the given
64
- `objects`.
65
+ def wrapper(*args, **kwargs):
66
+ return getter(self.obj)
65
67
 
66
- Args:
67
- attr (str or callable): A property/method name or a callable.
68
- objects: A list of objects instances that will serve as lookup for the given attr.
69
- The result `callable`, if any, will be a wrapper to the first object's attr that
70
- has the given ``attr``.
71
- """
72
- if callable(attr) or isinstance(attr, property):
73
- return SignatureAdapter.wrap(attr)
68
+ return wrapper
74
69
 
75
- # Setup configuration if not present to normalize the internal API
76
- configs = [ObjectConfig.from_obj(obj) for obj in objects]
77
70
 
78
- func, obj = _get_func_by_attr(attr, *configs)
71
+ class EventSearchResult(WrapSearchResult):
72
+ def __init__(self, attribute, func, resolver_id) -> None:
73
+ self.func = func
74
+ super().__init__(attribute, resolver_id)
79
75
 
80
- if not callable(func):
81
- return _build_attr_wrapper(attr, obj)
76
+ def wrap(self):
77
+ "Events already have the 'machine' parameter defined."
82
78
 
83
- if getattr(func, "_is_sm_event", False):
84
- return _build_sm_event_wrapper(func)
79
+ def wrapper(*args, **kwargs):
80
+ kwargs.pop("machine", None)
81
+ return self.func(*args, **kwargs)
85
82
 
86
- return SignatureAdapter.wrap(func)
83
+ return wrapper
87
84
 
88
85
 
89
- def resolver_factory(*objects):
90
- """Factory that returns a configured resolver."""
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
91
117
 
92
- objects = [ObjectConfig.from_obj(obj) for obj in objects]
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
93
137
 
94
- @wraps(ensure_callable)
95
- def wrapper(attr):
96
- return ensure_callable(attr, *objects)
138
+ if callable(attr):
139
+ yield _search_callable_attr_is_callable(attr, configs)
140
+ return
97
141
 
98
- resolver_id = ".".join(str(id(obj.obj)) for obj in objects)
99
- wrapper.id = resolver_id
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)
100
150
 
101
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)