Skip to content

Commit ee3885a

Browse files
authored
Improve callback registry (#442)
* chore: 15% faster setup with attr by name comparison * refac: Renaming callbacks methods * refac: Renaming callbacks to specs when refering to metadata * refac: Removing resolver_factory
1 parent b18ea56 commit ee3885a

14 files changed

Lines changed: 481 additions & 342 deletions
-3.02 KB
Loading

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ requires = ["poetry-core"]
6565
build-backend = "poetry.core.masonry.api"
6666

6767
[tool.pytest.ini_options]
68-
addopts = "--ignore=docs/conf.py --ignore=docs/auto_examples/ --ignore=docs/_build/ --ignore=tests/examples/ --cov --cov-config .coveragerc --doctest-glob='*.md' --doctest-modules --doctest-continue-on-failure --benchmark-autosave"
68+
addopts = "--ignore=docs/conf.py --ignore=docs/auto_examples/ --ignore=docs/_build/ --ignore=tests/examples/ --cov --cov-config .coveragerc --doctest-glob='*.md' --doctest-modules --doctest-continue-on-failure --benchmark-autosave --benchmark-group-by=name"
6969
doctest_optionflags = "ELLIPSIS IGNORE_EXCEPTION_DETAIL NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL"
7070
asyncio_mode = "auto"
7171
markers = [

statemachine/callbacks.py

Lines changed: 148 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
from collections import defaultdict
33
from collections import deque
44
from enum import IntEnum
5+
from enum import auto
56
from typing import Callable
67
from typing import Dict
78
from typing import Generator
9+
from typing import Iterable
810
from typing import List
11+
from typing import Type
912

1013
from .exceptions import AttrNotFound
1114
from .i18n import _
@@ -20,74 +23,81 @@ class CallbackPriority(IntEnum):
2023
AFTER = 40
2124

2225

23-
async def allways_true(*args, **kwargs):
24-
return True
25-
26+
class SpecReference(IntEnum):
27+
NAME = 1
28+
CALLABLE = 2
29+
PROPERTY = 3
2630

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
3931

40-
def __repr__(self):
41-
return f"{type(self).__name__}({self.unique_key})"
32+
class CallbackGroup(IntEnum):
33+
ENTER = auto()
34+
EXIT = auto()
35+
VALIDATOR = auto()
36+
BEFORE = auto()
37+
ON = auto()
38+
AFTER = auto()
39+
COND = auto()
4240

43-
def __str__(self):
44-
return str(self.meta)
41+
def build_key(self, specs: "CallbackSpecList") -> str:
42+
return f"{self.name}@{id(specs)}"
4543

46-
def __lt__(self, other):
47-
return self.meta.priority < other.meta.priority
4844

49-
async def __call__(self, *args, **kwargs):
50-
return await self._callback(*args, **kwargs)
45+
async def allways_true(*args, **kwargs):
46+
return True
5147

5248

53-
class CallbackMeta:
54-
"""A thin wrapper that register info about actions and guards.
49+
class CallbackSpec:
50+
"""Specs about callbacks.
5551
56-
At first, `func` can be a string or a callable, and even if it's already
57-
a callable, his signature can mismatch.
52+
At first, `func` can be a name (string), a property or a callable.
5853
59-
After instantiation, `.setup(resolver)` must be called before any real
60-
call is performed, to allow the proper callback resolution.
54+
Names, properties and unbounded callables should be resolved to a callable
55+
before any real call is performed.
6156
"""
6257

6358
def __init__(
6459
self,
6560
func,
66-
suppress_errors=False,
61+
group: CallbackGroup,
62+
is_convention=False,
6763
cond=None,
6864
priority: CallbackPriority = CallbackPriority.NAMING,
6965
expected_value=None,
7066
):
7167
self.func = func
72-
self.suppress_errors = suppress_errors
68+
self.group = group
69+
self.is_convention = is_convention
7370
self.cond = cond
7471
self.expected_value = expected_value
7572
self.priority = priority
7673

74+
if isinstance(func, property):
75+
self.reference = SpecReference.PROPERTY
76+
self.attr_name: str = func and func.fget and func.fget.__name__ or ""
77+
elif callable(func):
78+
self.reference = SpecReference.CALLABLE
79+
self.is_bounded = hasattr(func, "__self__")
80+
self.attr_name = func.__name__
81+
else:
82+
self.reference = SpecReference.NAME
83+
self.attr_name = func
84+
7785
def __repr__(self):
78-
return f"{type(self).__name__}({self.func!r}, suppress_errors={self.suppress_errors!r})"
86+
return f"{type(self).__name__}({self.func!r}, is_convention={self.is_convention!r})"
7987

8088
def __str__(self):
8189
return getattr(self.func, "__name__", self.func)
8290

8391
def __eq__(self, other):
84-
return self.func == other.func
92+
return self.func == other.func and self.group == other.group
8593

8694
def __hash__(self):
8795
return id(self)
8896

89-
def _update_func(self, func):
97+
def _update_func(self, func: Callable, attr_name: str):
9098
self.func = func
99+
self.reference = SpecReference.CALLABLE
100+
self.attr_name = attr_name
91101

92102
def _wrap_callable(self, func, _expected_value):
93103
return func
@@ -100,8 +110,12 @@ def build(self, resolver) -> Generator["CallbackWrapper", None, None]:
100110
resolver (callable): A method responsible to build and return a valid callable that
101111
can receive arbitrary parameters like `*args, **kwargs`.
102112
"""
103-
for callback in resolver(self.func):
104-
condition = next(resolver(self.cond)) if self.cond is not None else allways_true
113+
for callback in resolver.search(self):
114+
condition = (
115+
next(resolver.search(CallbackSpec(self.cond, CallbackGroup.COND)))
116+
if self.cond is not None
117+
else allways_true
118+
)
105119
yield CallbackWrapper(
106120
callback=self._wrap_callable(callback, self.expected_value),
107121
condition=condition,
@@ -110,7 +124,7 @@ def build(self, resolver) -> Generator["CallbackWrapper", None, None]:
110124
)
111125

112126

113-
class BoolCallbackMeta(CallbackMeta):
127+
class BoolCallbackSpec(CallbackSpec):
114128
"""A thin wrapper that register info about actions and guards.
115129
116130
At first, `func` can be a string or a callable, and even if it's already
@@ -123,13 +137,14 @@ class BoolCallbackMeta(CallbackMeta):
123137
def __init__(
124138
self,
125139
func,
126-
suppress_errors=False,
140+
group: CallbackGroup,
141+
is_convention=False,
127142
cond=None,
128143
priority: CallbackPriority = CallbackPriority.NAMING,
129144
expected_value=True,
130145
):
131146
super().__init__(
132-
func, suppress_errors, cond, priority=priority, expected_value=expected_value
147+
func, group, is_convention, cond, priority=priority, expected_value=expected_value
133148
)
134149

135150
def __str__(self):
@@ -143,19 +158,47 @@ async def bool_wrapper(*args, **kwargs):
143158
return bool_wrapper
144159

145160

146-
class CallbackMetaList:
147-
"""List of `CallbackMeta` instances"""
161+
class SpecListGrouper:
162+
def __init__(
163+
self, list: "CallbackSpecList", group: CallbackGroup, factory=CallbackSpec
164+
) -> None:
165+
self.list = list
166+
self.group = group
167+
self.factory = factory
168+
self.key = group.build_key(list)
169+
170+
def add(self, callbacks, **kwargs):
171+
self.list.add(callbacks, group=self.group, factory=self.factory, **kwargs)
172+
return self
173+
174+
def __call__(self, callback):
175+
return self.list._add_unbounded_callback(callback, group=self.group, factory=self.factory)
148176

149-
def __init__(self, factory=CallbackMeta):
150-
self.items: List[CallbackMeta] = []
177+
def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
178+
self.list._add_unbounded_callback(
179+
func,
180+
is_event=is_event,
181+
transitions=transitions,
182+
group=self.group,
183+
factory=self.factory,
184+
**kwargs,
185+
)
186+
187+
def __iter__(self):
188+
return (item for item in self.list if item.group == self.group)
189+
190+
191+
class CallbackSpecList:
192+
"""List of {ref}`CallbackSpec` instances"""
193+
194+
def __init__(self, factory=CallbackSpec):
195+
self.items: List[CallbackSpec] = []
196+
self.conventional_specs = set()
151197
self.factory = factory
152198

153199
def __repr__(self):
154200
return f"{type(self).__name__}({self.items!r}, factory={self.factory!r})"
155201

156-
def __str__(self):
157-
return ", ".join(str(c) for c in self)
158-
159202
def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
160203
"""This list was a target for adding a func using decorator
161204
`@<state|event>[.on|before|after|enter|exit]` syntax.
@@ -179,45 +222,76 @@ def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwar
179222
event.
180223
181224
"""
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
225+
spec = self._add(func, **kwargs)
226+
if not getattr(func, "_specs_to_update", None):
227+
func._specs_to_update = set()
228+
if is_event:
229+
func._specs_to_update.add(spec._update_func)
187230
func._transitions = transitions
188231

189232
return func
190233

191-
def __call__(self, callback):
192-
"""Allows usage of the callback list as a decorator."""
193-
return self._add_unbounded_callback(callback)
194-
195234
def __iter__(self):
196235
return iter(self.items)
197236

198237
def clear(self):
199238
self.items = []
200239

201-
def _add(self, func, **kwargs):
202-
meta = self.factory(func, **kwargs)
240+
def grouper(
241+
self, group: CallbackGroup, factory: Type[CallbackSpec] = CallbackSpec
242+
) -> SpecListGrouper:
243+
return SpecListGrouper(self, group, factory=factory)
244+
245+
def _add(self, func, group: CallbackGroup, factory=None, **kwargs):
246+
if factory is None:
247+
factory = self.factory
248+
spec = factory(func, group, **kwargs)
203249

204-
if meta in self.items:
250+
if spec in self.items:
205251
return
206252

207-
self.items.append(meta)
208-
return meta
253+
self.items.append(spec)
254+
if spec.is_convention:
255+
self.conventional_specs.add(spec.func)
256+
return spec
209257

210-
def add(self, callbacks, **kwargs):
258+
def add(self, callbacks, group: CallbackGroup, **kwargs):
211259
if callbacks is None:
212260
return self
213261

214262
unprepared = ensure_iterable(callbacks)
215263
for func in unprepared:
216-
self._add(func, **kwargs)
264+
self._add(func, group=group, **kwargs)
217265

218266
return self
219267

220268

269+
class CallbackWrapper:
270+
def __init__(
271+
self,
272+
callback: Callable,
273+
condition: Callable,
274+
meta: "CallbackSpec",
275+
unique_key: str,
276+
) -> None:
277+
self._callback = callback
278+
self.condition = condition
279+
self.meta = meta
280+
self.unique_key = unique_key
281+
282+
def __repr__(self):
283+
return f"{type(self).__name__}({self.unique_key})"
284+
285+
def __str__(self):
286+
return str(self.meta)
287+
288+
def __lt__(self, other):
289+
return self.meta.priority < other.meta.priority
290+
291+
async def __call__(self, *args, **kwargs):
292+
return await self._callback(*args, **kwargs)
293+
294+
221295
class CallbacksExecutor:
222296
"""A list of callbacks that can be executed in order."""
223297

@@ -234,15 +308,15 @@ def __repr__(self):
234308
def __str__(self):
235309
return ", ".join(str(c) for c in self)
236310

237-
def _add(self, callback_meta: CallbackMeta, resolver: Callable):
238-
for callback in callback_meta.build(resolver):
311+
def _add(self, spec: CallbackSpec, resolver: Callable):
312+
for callback in spec.build(resolver):
239313
if callback.unique_key in self.items_already_seen:
240314
continue
241315

242316
self.items_already_seen.add(callback.unique_key)
243317
insort(self.items, callback)
244318

245-
def add(self, items: CallbackMetaList, resolver: Callable):
319+
def add(self, items: Iterable[CallbackSpec], resolver: Callable):
246320
"""Validate configurations"""
247321
for item in items:
248322
self._add(item, resolver)
@@ -264,26 +338,22 @@ async def all(self, *args, **kwargs):
264338

265339
class CallbacksRegistry:
266340
def __init__(self) -> None:
267-
self._registry: Dict[CallbackMetaList, CallbacksExecutor] = defaultdict(CallbacksExecutor)
268-
269-
def register(self, meta_list: CallbackMetaList, resolver):
270-
executor_list = self[meta_list]
271-
executor_list.add(meta_list, resolver)
272-
return executor_list
341+
self._registry: Dict[str, CallbacksExecutor] = defaultdict(CallbacksExecutor)
273342

274343
def clear(self):
275344
self._registry.clear()
276345

277-
def __getitem__(self, meta_list: CallbackMetaList) -> CallbacksExecutor:
278-
return self._registry[meta_list]
346+
def __getitem__(self, key: str) -> CallbacksExecutor:
347+
return self._registry[key]
279348

280-
def check(self, meta_list: CallbackMetaList):
281-
executor = self[meta_list]
282-
for meta in meta_list:
283-
if meta.suppress_errors:
349+
def check(self, specs: CallbackSpecList):
350+
for meta in specs:
351+
if meta.is_convention:
284352
continue
285353

286-
if any(callback for callback in executor if callback.meta == meta):
354+
if any(
355+
callback for callback in self[meta.group.build_key(specs)] if callback.meta == meta
356+
):
287357
continue
288358
raise AttrNotFound(
289359
_("Did not found name '{}' from model or statemachine").format(meta.func)

0 commit comments

Comments
 (0)