#-----------------------------------------------------------------------------# Copyright (c) 2012 - 2022, Anaconda, Inc., and Bokeh Contributors.# All rights reserved.## The full license is in the file LICENSE.txt, distributed with this software.#-----------------------------------------------------------------------------''' Provides ``PropertyCallbackManager`` and ``EventCallbackManager``mixin classes for adding ``on_change`` and ``on_event`` callbackinterfaces to classes.'''#-----------------------------------------------------------------------------# Boilerplate#-----------------------------------------------------------------------------from__future__importannotationsimportlogging# isort:skiplog=logging.getLogger(__name__)#-----------------------------------------------------------------------------# Imports#-----------------------------------------------------------------------------# Standard library importsfromcollectionsimportdefaultdictfrominspectimportsignaturefromtypingimport(TYPE_CHECKING,Any,Callable,Sequence,Type,Union,cast,)# External importsfromtyping_extensionsimportTypeAlias# Bokeh importsfrom..eventsimportEvent,ModelEventfrom..util.functionsimportget_param_infoifTYPE_CHECKING:from..core.has_propsimportSetterfrom..core.typesimportIDfrom..document.documentimportDocumentfrom..document.eventsimportDocumentPatchedEvent#-----------------------------------------------------------------------------# Globals and constants#-----------------------------------------------------------------------------__all__=('EventCallbackManager','PropertyCallbackManager',)#-----------------------------------------------------------------------------# General API#-----------------------------------------------------------------------------# TODO (bev) the situation with no-argument Button callbacks is a mess. We# should migrate to all callbacks receving the event as the param, even if that# means auto-magically wrapping user-supplied callbacks for awhile.EventCallbackWithEvent:TypeAlias=Callable[[Event],None]EventCallbackWithoutEvent:TypeAlias=Callable[[],None]EventCallback:TypeAlias=Union[EventCallbackWithEvent,EventCallbackWithoutEvent]PropertyCallback:TypeAlias=Callable[[str,Any,Any],None]
[docs]classEventCallbackManager:''' A mixin class to provide an interface for registering and triggering event callbacks on the Python side. '''document:Document|Noneid:IDsubscribed_events:set[str]_event_callbacks:dict[str,list[EventCallback]]def__init__(self,*args:Any,**kw:Any)->None:super().__init__(*args,**kw)self._event_callbacks=defaultdict(list)
[docs]defon_event(self,event:str|Type[Event],*callbacks:EventCallback)->None:''' Run callbacks when the specified event occurs on this Model Not all Events are supported for all Models. See specific Events in :ref:`bokeh.events` for more information on which Models are able to trigger them. '''ifnotisinstance(event,str)andissubclass(event,Event):event=event.event_nameforcallbackincallbacks:if_nargs(callback)!=0:_check_callback(callback,('event',),what='Event callback')self._event_callbacks[event].append(callback)self.subscribed_events.add(event)
[docs]classPropertyCallbackManager:''' A mixin class to provide an interface for registering and triggering callbacks. '''document:Document|None_callbacks:dict[str,list[PropertyCallback]]def__init__(self,*args:Any,**kw:Any)->None:super().__init__(*args,**kw)self._callbacks={}
[docs]defon_change(self,attr:str,*callbacks:PropertyCallback)->None:''' Add a callback on this object to trigger when ``attr`` changes. Args: attr (str) : an attribute name on this object callback (callable) : a callback function to register Returns: None '''iflen(callbacks)==0:raiseValueError("on_change takes an attribute name and one or more callbacks, got only one parameter")_callbacks=self._callbacks.setdefault(attr,[])forcallbackincallbacks:ifcallbackin_callbacks:continue_check_callback(callback,('attr','old','new'))_callbacks.append(callback)
[docs]defremove_on_change(self,attr:str,*callbacks:PropertyCallback)->None:''' Remove a callback from this object '''iflen(callbacks)==0:raiseValueError("remove_on_change takes an attribute name and one or more callbacks, got only one parameter")_callbacks=self._callbacks.setdefault(attr,[])forcallbackincallbacks:_callbacks.remove(callback)
[docs]deftrigger(self,attr:str,old:Any,new:Any,hint:DocumentPatchedEvent|None=None,setter:Setter|None=None)->None:''' Trigger callbacks for ``attr`` on this object. Args: attr (str) : old (object) : new (object) : Returns: None '''definvoke()->None:callbacks=self._callbacks.get(attr)ifcallbacks:forcallbackincallbacks:callback(attr,old,new)ifself.documentisnotNone:from..modelimportModelself.document.callbacks.notify_change(cast(Model,self),attr,old,new,hint,setter,invoke)else:invoke()
#-----------------------------------------------------------------------------# Dev API#-----------------------------------------------------------------------------#-----------------------------------------------------------------------------# Private API#-----------------------------------------------------------------------------def_nargs(fn:Callable[...,Any])->int:sig=signature(fn)all_names,default_values=get_param_info(sig)returnlen(all_names)-len(default_values)def_check_callback(callback:Callable[...,Any],fargs:Sequence[str],what:str="Callback functions")->None:'''Bokeh-internal function to check callback signature'''sig=signature(callback)formatted_args=str(sig)error_msg=what+" must have signature func(%s), got func%s"all_names,default_values=get_param_info(sig)nargs=len(all_names)-len(default_values)ifnargs!=len(fargs):raiseValueError(error_msg%(", ".join(fargs),formatted_args))#-----------------------------------------------------------------------------# Code#-----------------------------------------------------------------------------