1+ import inspect
12import sys
23import traceback
34from collections .abc import Callable
5+ from contextlib import contextmanager , suppress
46from typing import Any , Generic , TypeVar , cast
57from weakref import WeakKeyDictionary , ref
68
911P = TypeVar ("P" )
1012
1113
14+ NoArgListener = Callable [[], None ]
15+ InstanceListener = Callable [[Any ], None ]
16+ InstanceValueListener = Callable [[Any , Any ], None ]
17+ InstanceNewOldListener = Callable [[Any , Any , Any ], None ]
18+ AnyListener = NoArgListener | InstanceListener | InstanceValueListener | InstanceNewOldListener
19+
20+
1221class _Obs (Generic [P ]):
1322 """
1423 Internal holder for Property value and change listeners
1524 """
1625
17- __slots__ = ("value" , "listeners " )
26+ __slots__ = ("value" , "_listeners " )
1827
1928 def __init__ (self , value : P ):
2029 self .value = value
2130 # This will keep any added listener even if it is not referenced anymore
2231 # and would be garbage collected
23- self .listeners : set [Callable [[Any , P ], Any ] | Callable [[], Any ]] = set ()
32+ self ._listeners : dict [AnyListener , InstanceNewOldListener ] = dict ()
33+
34+ def add (
35+ self ,
36+ callback : AnyListener ,
37+ ):
38+ """Add a callback to the list of listeners"""
39+ self ._listeners [callback ] = _Obs ._normalize_callback (callback )
40+
41+ def remove (self , callback ):
42+ """Remove a callback from the list of listeners"""
43+ if callback in self ._listeners :
44+ del self ._listeners [callback ]
45+
46+ @property
47+ def listeners (self ) -> list [InstanceNewOldListener ]:
48+ return list (self ._listeners .values ())
49+
50+ @staticmethod
51+ def _normalize_callback (callback ) -> InstanceNewOldListener :
52+ """Normalizes the callback so every callback can be invoked with the same signature."""
53+ signature = inspect .signature (callback )
54+
55+ with suppress (TypeError ):
56+ signature .bind (1 , 1 )
57+ return lambda instance , new , old : callback (instance , new )
58+
59+ with suppress (TypeError ):
60+ signature .bind (1 , 1 , 1 )
61+ return lambda instance , new , old : callback (instance , new , old )
62+
63+ with suppress (TypeError ):
64+ signature .bind (1 )
65+ return lambda instance , new , old : callback (instance )
66+
67+ with suppress (TypeError ):
68+ signature .bind ()
69+ return lambda instance , new , old : callback ()
70+
71+ raise TypeError ("Callback is not callable" )
2472
2573
2674class Property (Generic [P ]):
@@ -85,27 +133,23 @@ def set(self, instance, value):
85133 """Set value for owner instance"""
86134 obs = self ._get_obs (instance )
87135 if obs .value != value :
136+ old = obs .value
88137 obs .value = value
89- self .dispatch (instance , value )
138+ self .dispatch (instance , value , old )
90139
91- def dispatch (self , instance , value ):
140+ def dispatch (self , instance , value , old_value ):
92141 """Notifies every listener, which subscribed to the change event.
93142
94143 Args:
95144 instance: Property instance
96- value: new value to set
145+ value: new value set
146+ old_value: previous value
97147
98148 """
99149 obs = self ._get_obs (instance )
100150 for listener in obs .listeners :
101151 try :
102- try :
103- # FIXME if listener() raises an error, the invalid call will be
104- # also shown as an exception
105- listener (instance , value ) # type: ignore
106- except TypeError :
107- # If the listener does not accept arguments, we call it without it
108- listener () # type: ignore
152+ listener (instance , value , old_value )
109153 except Exception :
110154 print (
111155 f"Change listener for { instance } .{ self .name } = { value } raised an exception!" ,
@@ -126,7 +170,7 @@ def bind(self, instance, callback):
126170 # Instance methods are bound methods, which can not be referenced by normal `ref()`
127171 # if listeners would be a WeakSet, we would have to add listeners as WeakMethod
128172 # ourselves into `WeakSet.data`.
129- obs .listeners . add (callback )
173+ obs .add (callback )
130174
131175 def unbind (self , instance , callback ):
132176 """Unbinds a function from the change event of the property.
@@ -136,7 +180,7 @@ def unbind(self, instance, callback):
136180 callback: The callback to unbind.
137181 """
138182 obs = self ._get_obs (instance )
139- obs .listeners . remove (callback )
183+ obs .remove (callback )
140184
141185 def __set_name__ (self , owner , name ):
142186 self .name = name
@@ -232,45 +276,49 @@ def __init__(self, prop: Property, instance, *args):
232276 self .obj = ref (instance )
233277 super ().__init__ (* args )
234278
235- def dispatch (self ):
236- self .prop .dispatch (self .obj (), self )
279+ @contextmanager
280+ def _dispatch (self ):
281+ """This is a context manager which will dispatch the change event
282+ when the context is exited.
283+ """
284+ old_value = self .copy ()
285+ yield
286+ self .prop .dispatch (self .obj (), self , old_value )
237287
238288 @override
239289 def __setitem__ (self , key , value ):
240- dict . __setitem__ ( self , key , value )
241- self . dispatch ( )
290+ with self . _dispatch ():
291+ dict . __setitem__ ( self , key , value )
242292
243293 @override
244294 def __delitem__ (self , key ):
245- dict . __delitem__ ( self , key )
246- self . dispatch ( )
295+ with self . _dispatch ():
296+ dict . __delitem__ ( self , key )
247297
248298 @override
249299 def clear (self ):
250- dict . clear ( self )
251- self . dispatch ( )
300+ with self . _dispatch ():
301+ dict . clear ( self )
252302
253303 @override
254304 def pop (self , * args ):
255- result = dict .pop (self , * args )
256- self .dispatch ()
257- return result
305+ with self ._dispatch ():
306+ return dict .pop (self , * args )
258307
259308 @override
260309 def popitem (self ):
261- result = dict .popitem (self )
262- self .dispatch ()
263- return result
310+ with self ._dispatch ():
311+ return dict .popitem (self )
264312
265313 @override
266314 def setdefault (self , * args ):
267- dict . setdefault ( self , * args )
268- self . dispatch ( )
315+ with self . _dispatch ():
316+ return dict . setdefault ( self , * args )
269317
270318 @override
271319 def update (self , * args ):
272- dict . update ( self , * args )
273- self . dispatch ( )
320+ with self . _dispatch ():
321+ dict . update ( self , * args )
274322
275323
276324K = TypeVar ("K" )
@@ -309,80 +357,86 @@ def __init__(self, prop: Property, instance, *args):
309357 self .obj = ref (instance )
310358 super ().__init__ (* args )
311359
312- def dispatch (self ):
313- """Dispatches the change event."""
314- self .prop .dispatch (self .obj (), self )
360+ @contextmanager
361+ def _dispatch (self ):
362+ """Dispatches the change event.
363+ This is a context manager which will dispatch the change event
364+ when the context is exited.
365+ """
366+ old_value = self .copy ()
367+ yield
368+ self .prop .dispatch (self .obj (), self , old_value )
315369
316370 @override
317371 def __setitem__ (self , key , value ):
318- list . __setitem__ ( self , key , value )
319- self . dispatch ( )
372+ with self . _dispatch ():
373+ list . __setitem__ ( self , key , value )
320374
321375 @override
322376 def __delitem__ (self , key ):
323- list . __delitem__ ( self , key )
324- self . dispatch ( )
377+ with self . _dispatch ():
378+ list . __delitem__ ( self , key )
325379
326380 @override
327381 def __iadd__ (self , * args ):
328- list . __iadd__ ( self , * args )
329- self . dispatch ( )
382+ with self . _dispatch ():
383+ list . __iadd__ ( self , * args )
330384 return self
331385
332386 @override
333387 def __imul__ (self , * args ):
334- list . __imul__ ( self , * args )
335- self . dispatch ( )
388+ with self . _dispatch ():
389+ list . __imul__ ( self , * args )
336390 return self
337391
338392 @override
339393 def append (self , * args ):
340394 """Proxy for list.append() which dispatches the change event."""
341- list . append ( self , * args )
342- self . dispatch ( )
395+ with self . _dispatch ():
396+ list . append ( self , * args )
343397
344398 @override
345399 def clear (self ):
346400 """Proxy for list.clear() which dispatches the change event."""
347- list . clear ( self )
348- self . dispatch ( )
401+ with self . _dispatch ():
402+ list . clear ( self )
349403
350404 @override
351405 def remove (self , * args ):
352406 """Proxy for list.remove() which dispatches the change event."""
353- list . remove ( self , * args )
354- self . dispatch ( )
407+ with self . _dispatch ():
408+ list . remove ( self , * args )
355409
356410 @override
357411 def insert (self , * args ):
358412 """Proxy for list.insert() which dispatches the change event."""
359- list . insert ( self , * args )
360- self . dispatch ( )
413+ with self . _dispatch ():
414+ list . insert ( self , * args )
361415
362416 @override
363417 def pop (self , * args ):
364418 """Proxy for list.pop() which dispatches the change"""
365- result = list . pop ( self , * args )
366- self . dispatch ( )
419+ with self . _dispatch ():
420+ result = list . pop ( self , * args )
367421 return result
368422
369423 @override
370424 def extend (self , * args ):
371425 """Proxy for list.extend() which dispatches the change event."""
372- list . extend ( self , * args )
373- self . dispatch ( )
426+ with self . _dispatch ():
427+ list . extend ( self , * args )
374428
375429 @override
376430 def sort (self , ** kwargs ):
377431 """Proxy for list.sort() which dispatches the change event."""
378- list . sort ( self , ** kwargs )
379- self . dispatch ( )
432+ with self . _dispatch ():
433+ list . sort ( self , ** kwargs )
380434
381435 @override
382436 def reverse (self ):
383437 """Proxy for list.reverse() which dispatches the change event."""
384- list . reverse ( self )
385- self . dispatch ( )
438+ with self . _dispatch ():
439+ list . reverse ( self )
386440
387441
388442class ListProperty (Property [list [P ]], Generic [P ]):
0 commit comments