1616 Generic ,
1717 Type ,
1818 TypeVar ,
19+ cast ,
1920 overload ,
2021)
2122
2223import rich .repr
2324
2425from . import events
2526from ._callback import count_parameters
26- from ._types import MessageTarget , WatchCallbackType
27+ from ._types import (
28+ MessageTarget ,
29+ WatchCallbackBothValuesType ,
30+ WatchCallbackNewValueType ,
31+ WatchCallbackNoArgsType ,
32+ WatchCallbackType ,
33+ )
2734
2835if TYPE_CHECKING :
2936 from .dom import DOMNode
@@ -42,6 +49,43 @@ class TooManyComputesError(ReactiveError):
4249 """Raised when an attribute has public and private compute methods."""
4350
4451
52+ async def await_watcher (obj : Reactable , awaitable : Awaitable [object ]) -> None :
53+ """Coroutine to await an awaitable returned from a watcher"""
54+ _rich_traceback_omit = True
55+ await awaitable
56+ # Watcher may have changed the state, so run compute again
57+ obj .post_message (events .Callback (callback = partial (Reactive ._compute , obj )))
58+
59+
60+ def invoke_watcher (
61+ watcher_object : Reactable ,
62+ watch_function : WatchCallbackType ,
63+ old_value : object ,
64+ value : object ,
65+ ) -> None :
66+ """Invoke a watch function.
67+
68+ Args:
69+ watcher_object: The object watching for the changes.
70+ watch_function: A watch function, which may be sync or async.
71+ old_value: The old value of the attribute.
72+ value: The new value of the attribute.
73+ """
74+ _rich_traceback_omit = True
75+ param_count = count_parameters (watch_function )
76+ if param_count == 2 :
77+ watch_result = cast (WatchCallbackBothValuesType , watch_function )(
78+ old_value , value
79+ )
80+ elif param_count == 1 :
81+ watch_result = cast (WatchCallbackNewValueType , watch_function )(value )
82+ else :
83+ watch_result = cast (WatchCallbackNoArgsType , watch_function )()
84+ if isawaitable (watch_result ):
85+ # Result is awaitable, so we need to await it within an async context
86+ watcher_object .call_next (partial (await_watcher , watcher_object , watch_result ))
87+
88+
4589@rich .repr .auto
4690class Reactive (Generic [ReactiveType ]):
4791 """Reactive descriptor.
@@ -239,7 +283,7 @@ def __set__(self, obj: Reactable, value: ReactiveType) -> None:
239283 obj .refresh (repaint = self ._repaint , layout = self ._layout )
240284
241285 @classmethod
242- def _check_watchers (cls , obj : Reactable , name : str , old_value : Any ):
286+ def _check_watchers (cls , obj : Reactable , name : str , old_value : Any ) -> None :
243287 """Check watchers, and call watch methods / computes
244288
245289 Args:
@@ -252,39 +296,6 @@ def _check_watchers(cls, obj: Reactable, name: str, old_value: Any):
252296 internal_name = f"_reactive_{ name } "
253297 value = getattr (obj , internal_name )
254298
255- async def await_watcher (awaitable : Awaitable ) -> None :
256- """Coroutine to await an awaitable returned from a watcher"""
257- _rich_traceback_omit = True
258- await awaitable
259- # Watcher may have changed the state, so run compute again
260- obj .post_message (events .Callback (callback = partial (Reactive ._compute , obj )))
261-
262- def invoke_watcher (
263- watcher_object : Reactable ,
264- watch_function : Callable ,
265- old_value : object ,
266- value : object ,
267- ) -> None :
268- """Invoke a watch function.
269-
270- Args:
271- watcher_object: The object watching for the changes.
272- watch_function: A watch function, which may be sync or async.
273- old_value: The old value of the attribute.
274- value: The new value of the attribute.
275- """
276- _rich_traceback_omit = True
277- param_count = count_parameters (watch_function )
278- if param_count == 2 :
279- watch_result = watch_function (old_value , value )
280- elif param_count == 1 :
281- watch_result = watch_function (value )
282- else :
283- watch_result = watch_function ()
284- if isawaitable (watch_result ):
285- # Result is awaitable, so we need to await it within an async context
286- watcher_object .call_next (partial (await_watcher , watch_result ))
287-
288299 private_watch_function = getattr (obj , f"_watch_{ name } " , None )
289300 if callable (private_watch_function ):
290301 invoke_watcher (obj , private_watch_function , old_value , value )
@@ -294,7 +305,7 @@ def invoke_watcher(
294305 invoke_watcher (obj , public_watch_function , old_value , value )
295306
296307 # Process "global" watchers
297- watchers : list [tuple [Reactable , Callable ]]
308+ watchers : list [tuple [Reactable , WatchCallbackType ]]
298309 watchers = getattr (obj , "__watchers" , {}).get (name , [])
299310 # Remove any watchers for reactables that have since closed
300311 if watchers :
@@ -404,11 +415,13 @@ def _watch(
404415 """
405416 if not hasattr (obj , "__watchers" ):
406417 setattr (obj , "__watchers" , {})
407- watchers : dict [str , list [tuple [Reactable , Callable ]]] = getattr (obj , "__watchers" )
418+ watchers : dict [str , list [tuple [Reactable , WatchCallbackType ]]] = getattr (
419+ obj , "__watchers"
420+ )
408421 watcher_list = watchers .setdefault (attribute_name , [])
409- if callback in watcher_list :
422+ if any ( callback == callback_from_list for _ , callback_from_list in watcher_list ) :
410423 return
411- watcher_list .append ((node , callback ))
412424 if init :
413425 current_value = getattr (obj , attribute_name , None )
414- Reactive ._check_watchers (obj , attribute_name , current_value )
426+ invoke_watcher (obj , callback , current_value , current_value )
427+ watcher_list .append ((node , callback ))
0 commit comments