22from collections import defaultdict
33from collections import deque
44from enum import IntEnum
5+ from enum import auto
56from typing import Callable
67from typing import Dict
78from typing import Generator
9+ from typing import Iterable
810from typing import List
11+ from typing import Type
912
1013from .exceptions import AttrNotFound
1114from .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+
221295class 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
265339class 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