#-----------------------------------------------------------------------------# Copyright (c) Anaconda, Inc., and Bokeh Contributors.# All rights reserved.## The full license is in the file LICENSE.txt, distributed with this software.#-----------------------------------------------------------------------------''' Provide a base class for objects that can have declarative, typed,serializable properties... note:: These classes form part of the very low-level machinery that implements the Bokeh model and property system. It is unlikely that any of these classes or their methods will be applicable to any standard usage or to anyone who is not directly developing on Bokeh's own infrastructure.'''#-----------------------------------------------------------------------------# Boilerplate#-----------------------------------------------------------------------------from__future__importannotationsimportlogging# isort:skiplog=logging.getLogger(__name__)#-----------------------------------------------------------------------------# Imports#-----------------------------------------------------------------------------# Standard library importsimportdifflibfromtypingimport(TYPE_CHECKING,Any,Callable,ClassVar,Iterable,Literal,NoReturn,TypedDict,TypeVar,Union,overload,)fromweakrefimportWeakSetifTYPE_CHECKING:F=TypeVar("F",bound=Callable[...,Any])deflru_cache(arg:int|None)->Callable[[F],F]:...else:fromfunctoolsimportlru_cacheifTYPE_CHECKING:fromtyping_extensionsimportSelf# Bokeh importsfrom..settingsimportsettingsfrom..util.stringsimportappend_docstring,nice_joinfrom..util.warningsimportwarnfrom.property.descriptor_factoryimportPropertyDescriptorFactoryfrom.property.descriptorsimportPropertyDescriptor,UnsetValueErrorfrom.property.overrideimportOverridefrom.property.singletonsimportIntrinsic,Undefinedfrom.property.wrappersimportPropertyValueContainerfrom.serializationimport(ObjectRep,Ref,Serializable,Serializer,)from.typesimportIDifTYPE_CHECKING:fromtyping_extensionsimportNotRequired,TypeAliasfrom..client.sessionimportClientSessionfrom..server.sessionimportServerSessionfrom.property.basesimportPropertyfrom.property.dataspecimportDataSpec#-----------------------------------------------------------------------------# Globals and constants#-----------------------------------------------------------------------------__all__=('abstract','HasProps','MetaHasProps','NonQualified','Qualified',)#-----------------------------------------------------------------------------# General API#-----------------------------------------------------------------------------#-----------------------------------------------------------------------------# Dev API#-----------------------------------------------------------------------------ifTYPE_CHECKING:Setter:TypeAlias=Union[ClientSession,ServerSession]C=TypeVar("C",bound=type["HasProps"])_abstract_classes:WeakSet[type[HasProps]]=WeakSet()
[docs]defabstract(cls:C)->C:''' A decorator to mark abstract base classes derived from |HasProps|. '''ifnotissubclass(cls,HasProps):raiseTypeError(f"{cls.__name__} is not a subclass of HasProps")_abstract_classes.add(cls)cls.__doc__=append_docstring(cls.__doc__,_ABSTRACT_ADMONITION)returncls
defis_abstract(cls:type[HasProps])->bool:returnclsin_abstract_classesdefis_DataModel(cls:type[HasProps])->bool:from..modelimportDataModelreturnissubclass(cls,HasProps)andgetattr(cls,"__data_model__",False)andcls!=DataModeldef_overridden_defaults(class_dict:dict[str,Any])->dict[str,Any]:overridden_defaults:dict[str,Any]={}forname,propintuple(class_dict.items()):ifisinstance(prop,Override):delclass_dict[name]ifprop.default_overridden:overridden_defaults[name]=prop.defaultreturnoverridden_defaultsdef_generators(class_dict:dict[str,Any]):generators:dict[str,PropertyDescriptorFactory[Any]]={}forname,generatorintuple(class_dict.items()):ifisinstance(generator,PropertyDescriptorFactory):delclass_dict[name]generators[name]=generatorreturngeneratorsclass_ModelResolver:""" """_known_models:dict[str,type[HasProps]]def__init__(self)->None:self._known_models={}defadd(self,cls:type[HasProps])->None:ifnot(issubclass(cls,Local)orcls.__name__.startswith("_")):# update the mapping of view model names to classes, checking for any duplicatesprevious=self._known_models.get(cls.__qualified_model__,None)ifpreviousisnotNoneandnothasattr(cls,"__implementation__"):raiseWarning(f"Duplicate qualified model declaration of '{cls.__qualified_model__}'. Previous definition: {previous}")self._known_models[cls.__qualified_model__]=clsdefremove(self,cls:type[HasProps])->None:delself._known_models[cls.__qualified_model__]@propertydefknown_models(self)->dict[str,type[HasProps]]:returndict(self._known_models)defclear_extensions(self)->None:defis_extension(obj:type[HasProps])->bool:returngetattr(obj,"__implementation__",None)isnotNoneor \
getattr(obj,"__javascript__",None)isnotNoneor \
getattr(obj,"__css__",None)isnotNoneself._known_models={key:valforkey,valinself._known_models.items()ifnotis_extension(val)}_default_resolver=_ModelResolver()
[docs]classMetaHasProps(type):''' Specialize the construction of |HasProps| classes. This class is a `metaclass`_ for |HasProps| that is responsible for creating and adding the ``PropertyDescriptor`` instances that delegate validation and serialization to |Property| attributes. .. _metaclass: https://docs.python.org/3/reference/datamodel.html#metaclasses '''__properties__:dict[str,Property[Any]]__overridden_defaults__:dict[str,Any]__themed_values__:dict[str,Any]def__new__(cls,class_name:str,bases:tuple[type,...],class_dict:dict[str,Any]):''' '''overridden_defaults=_overridden_defaults(class_dict)generators=_generators(class_dict)properties={}forname,generatoringenerators.items():descriptors=generator.make_descriptors(name)fordescriptorindescriptors:name=descriptor.nameifnameinclass_dict:raiseRuntimeError(f"Two property generators both created {class_name}.{name}")class_dict[name]=descriptorproperties[name]=descriptor.propertyclass_dict["__properties__"]=propertiesclass_dict["__overridden_defaults__"]=overridden_defaultsreturnsuper().__new__(cls,class_name,bases,class_dict)def__init__(cls,class_name:str,bases:tuple[type,...],_)->None:# HasProps itself may not have any properties definedifclass_name=="HasProps":return# Check for improperly redeclared a Property attribute.base_properties:dict[str,Any]={}forbasein(xforxinbasesifissubclass(x,HasProps)):base_properties.update(base.properties(_with_props=True))own_properties={k:vfork,vincls.__dict__.items()ifisinstance(v,PropertyDescriptor)}redeclared=own_properties.keys()&base_properties.keys()ifredeclared:warn(f"Properties {redeclared!r} in class {cls.__name__} were previously declared on a parent ""class. It never makes sense to do this. Redundant properties should be deleted here, or on ""the parent class. Override() can be used to change a default value of a base class property.",RuntimeWarning)# Check for no-op Overridesunused_overrides=cls.__overridden_defaults__.keys()-cls.properties(_with_props=True).keys()ifunused_overrides:warn(f"Overrides of {unused_overrides} in class {cls.__name__} does not override anything.",RuntimeWarning)@propertydefmodel_class_reverse_map(cls)->dict[str,type[HasProps]]:return_default_resolver.known_models
classLocal:"""Don't register this class in model registry. """
[docs]classQualified:"""Resolve this class by a fully qualified name. """
[docs]classNonQualified:"""Resolve this class by a non-qualified name. """
[docs]classHasProps(Serializable,metaclass=MetaHasProps):''' Base class for all class types that have Bokeh properties. '''_initialized:bool=False_property_values:dict[str,Any]_unstable_default_values:dict[str,Any]_unstable_themed_values:dict[str,Any]__view_model__:ClassVar[str]__view_module__:ClassVar[str]__qualified_model__:ClassVar[str]__implementation__:ClassVar[Any]# TODO: specific type__data_model__:ClassVar[bool]@classmethoddef__init_subclass__(cls):super().__init_subclass__()# use an explicitly provided view model name if there is oneif"__view_model__"notincls.__dict__:cls.__view_model__=cls.__qualname__.replace("<locals>.","")if"__view_module__"notincls.__dict__:cls.__view_module__=cls.__module__if"__qualified_model__"notincls.__dict__:defqualified():module=cls.__view_module__model=cls.__view_model__ifissubclass(cls,NonQualified):returnmodelifnotissubclass(cls,Qualified):head=module.split(".")[0]ifhead=="bokeh"orhead=="__main__"or"__implementation__"incls.__dict__:returnmodelreturnf"{module}.{model}"cls.__qualified_model__=qualified()_default_resolver.add(cls)
[docs]def__init__(self,**properties:Any)->None:''' '''super().__init__()self._property_values={}self._unstable_default_values={}self._unstable_themed_values={}forname,valueinproperties.items():# TODO: this would be better to handle in descriptorsifvalueisUndefinedorvalueisIntrinsic:continuesetattr(self,name,value)initialized=set(properties.keys())fornameinself.properties(_with_props=True):# avoid set[] for deterministic behaviorifnameininitialized:continuedesc=self.lookup(name)ifdesc.has_unstable_default(self):desc._get(self)# this fills-in `_unstable_*_values`self._initialized=True
def__setattr__(self,name:str,value:Any)->None:''' Intercept attribute setting on HasProps in order to special case a few situations: * short circuit all property machinery for ``_private`` attributes * suggest similar attribute names on attribute errors Args: name (str) : the name of the attribute to set on this object value (obj) : the value to set Returns: None '''ifname.startswith("_"):returnsuper().__setattr__(name,value)properties=self.properties(_with_props=True)ifnameinproperties:returnsuper().__setattr__(name,value)descriptor=getattr(self.__class__,name,None)ifisinstance(descriptor,property):# Python propertyreturnsuper().__setattr__(name,value)self._raise_attribute_error_with_matches(name,properties)def__getattr__(self,name:str)->Any:''' Intercept attribute setting on HasProps in order to special case a few situations: * short circuit all property machinery for ``_private`` attributes * suggest similar attribute names on attribute errors Args: name (str) : the name of the attribute to set on this object Returns: Any '''ifname.startswith("_"):returnsuper().__getattribute__(name)properties=self.properties(_with_props=True)ifnameinproperties:returnsuper().__getattribute__(name)descriptor=getattr(self.__class__,name,None)ifisinstance(descriptor,property):# Python propertyreturnsuper().__getattribute__(name)self._raise_attribute_error_with_matches(name,properties)def_raise_attribute_error_with_matches(self,name:str,properties:Iterable[str])->NoReturn:matches,text=difflib.get_close_matches(name.lower(),properties),"similar"ifnotmatches:matches,text=sorted(properties),"possible"raiseAttributeError(f"unexpected attribute {name!r} to {self.__class__.__name__}, {text} attributes are {nice_join(matches)}")def__str__(self)->str:name=self.__class__.__name__returnf"{name}(...)"__repr__=__str__# Unfortunately we cannot implement __eq__. We rely on the default __hash__# based on object identity, in order to put HasProps instances in sets.# Implementing __eq__ as structural equality would necessitate a __hash__# that returns the same value different HasProps instances that compare# equal [1], and this would break many things.## [1] https://docs.python.org/3/reference/datamodel.html#object.__hash__#
[docs]defequals(self,other:HasProps)->bool:''' Structural equality of models. Args: other (HasProps) : the other instance to compare to Returns: True, if properties are structurally equal, otherwise False '''ifnotisinstance(other,self.__class__):returnFalseelse:returnself.properties_with_values()==other.properties_with_values()
# FQ type name required to suppress Sphinx error "more than one target found for cross-reference 'JSON'"
[docs]defset_from_json(self,name:str,value:Any,*,setter:Setter|None=None)->None:''' Set a property value on this object from JSON. Args: name: (str) : name of the attribute to set json: (JSON-value) : value to set to the attribute to models (dict or None, optional) : Mapping of model ids to models (default: None) This is needed in cases where the attributes to update also have values that have references. setter(ClientSession or ServerSession or None, optional) : This is used to prevent "boomerang" updates to Bokeh apps. In the context of a Bokeh server application, incoming updates to properties will be annotated with the session that is doing the updating. This value is propagated through any subsequent change notifications that the update triggers. The session can compare the event setter to itself, and suppress any updates that originate from itself. Returns: None '''ifnameinself.properties(_with_props=True):log.trace(f"Patching attribute {name!r} of {self!r} with {value!r}")# type: ignore # TODO: log.trace()descriptor=self.lookup(name)descriptor.set_from_json(self,value,setter=setter)else:log.warning("JSON had attr %r on obj %r, which is a client-only or invalid attribute that shouldn't have been sent",name,self)
[docs]defupdate(self,**kwargs:Any)->None:''' Updates the object's properties from the given keyword arguments. Returns: None Examples: The following are equivalent: .. code-block:: python from bokeh.models import Range1d r = Range1d # set properties individually: r.start = 10 r.end = 20 # update properties together: r.update(start=10, end=20) '''fork,vinkwargs.items():setattr(self,k,v)
[docs]@classmethoddeflookup(cls,name:str,*,raises:bool=True)->PropertyDescriptor[Any]|None:''' Find the ``PropertyDescriptor`` for a Bokeh property on a class, given the property name. Args: name (str) : name of the property to search for raises (bool) : whether to raise or return None if missing Returns: PropertyDescriptor : descriptor for property named ``name`` '''attr=getattr(cls,name,None)ifattrisnotNoneor(attrisNoneandnotraises):returnattrraiseAttributeError(f"{cls.__name__}.{name} property descriptor does not exist")
[docs]@classmethod@lru_cache(None)defproperties(cls,*,_with_props:bool=False)->set[str]|dict[str,Property[Any]]:''' Collect the names of properties on this class. .. warning:: In a future version of Bokeh, this method will return a dictionary mapping property names to property objects. To future-proof this current usage of this method, wrap the return value in ``list``. Returns: property names '''props:dict[str,Property[Any]]={}forcinreversed(cls.__mro__):props.update(getattr(c,"__properties__",{}))ifnot_with_props:returnset(props)returnprops
[docs]@classmethod@lru_cache(None)defdescriptors(cls)->list[PropertyDescriptor[Any]]:""" List of property descriptors in the order of definition. """return[cls.lookup(name)forname,_incls.properties(_with_props=True).items()]
[docs]@classmethod@lru_cache(None)defproperties_with_refs(cls)->dict[str,Property[Any]]:''' Collect the names of all properties on this class that also have references. This method *always* traverses the class hierarchy and includes properties defined on any parent classes. Returns: set[str] : names of properties that have references '''return{k:vfork,vincls.properties(_with_props=True).items()ifv.has_ref}
[docs]@classmethod@lru_cache(None)defdataspecs(cls)->dict[str,DataSpec]:''' Collect the names of all ``DataSpec`` properties on this class. This method *always* traverses the class hierarchy and includes properties defined on any parent classes. Returns: set[str] : names of ``DataSpec`` properties '''from.property.dataspecimportDataSpec# avoid circular importreturn{k:vfork,vincls.properties(_with_props=True).items()ifisinstance(v,DataSpec)}
[docs]defproperties_with_values(self,*,include_defaults:bool=True,include_undefined:bool=False)->dict[str,Any]:''' Collect a dict mapping property names to their values. This method *always* traverses the class hierarchy and includes properties defined on any parent classes. Non-serializable properties are skipped and property values are in "serialized" format which may be slightly different from the values you would normally read from the properties; the intent of this method is to return the information needed to losslessly reconstitute the object instance. Args: include_defaults (bool, optional) : Whether to include properties that haven't been explicitly set since the object was created. (default: True) Returns: dict : mapping from property names to their values '''returnself.query_properties_with_values(lambdaprop:prop.serialized,include_defaults=include_defaults,include_undefined=include_undefined)
@classmethoddef_overridden_defaults(cls)->dict[str,Any]:''' Returns a dictionary of defaults that have been overridden. .. note:: This is an implementation detail of ``Property``. '''defaults:dict[str,Any]={}forcinreversed(cls.__mro__):defaults.update(getattr(c,"__overridden_defaults__",{}))returndefaults
[docs]defquery_properties_with_values(self,query:Callable[[PropertyDescriptor[Any]],bool],*,include_defaults:bool=True,include_undefined:bool=False)->dict[str,Any]:''' Query the properties values of |HasProps| instances with a predicate. Args: query (callable) : A callable that accepts property descriptors and returns True or False include_defaults (bool, optional) : Whether to include properties that have not been explicitly set by a user (default: True) Returns: dict : mapping of property names and values for matching properties '''themed_keys:set[str]=set()result:dict[str,Any]={}keys=self.properties(_with_props=True)ifinclude_defaults:selected_keys=set(keys)else:# TODO (bev) For now, include unstable default values. Things rely on Instances# always getting serialized, even defaults, and adding unstable defaults here# accomplishes that. Unmodified defaults for property value containers will be# weeded out below.selected_keys=set(self._property_values.keys())|set(self._unstable_default_values.keys())themed_values=self.themed_values()ifthemed_valuesisnotNone:themed_keys=set(themed_values.keys())selected_keys|=themed_keysforkeyinkeys:descriptor=self.lookup(key)ifnotquery(descriptor):continuetry:value=descriptor.get_value(self)exceptUnsetValueError:ifinclude_undefined:value=Undefinedelse:raiseelse:# TODO: this should happen before get_value(), however there's currently# no reliable way of checking if a property is unset without actually# getting the value.ifkeynotinselected_keys:continueifnotinclude_defaultsandkeynotinthemed_keys:ifisinstance(value,PropertyValueContainer)andkeyinself._unstable_default_values:continueresult[key]=valuereturnresult
[docs]defthemed_values(self)->dict[str,Any]|None:''' Get any theme-provided overrides. Results are returned as a dict from property name to value, or ``None`` if no theme overrides any values for this instance. Returns: dict or None '''returngetattr(self,'__themed_values__',None)
[docs]defapply_theme(self,property_values:dict[str,Any])->None:''' Apply a set of theme values which will be used rather than defaults, but will not override application-set values. The passed-in dictionary may be kept around as-is and shared with other instances to save memory (so neither the caller nor the |HasProps| instance should modify it). Args: property_values (dict) : theme values to use in place of defaults Returns: None '''old_dict=self.themed_values()# if the same theme is set again, it should reuse the same dictifold_dictisproperty_values:# lgtm [py/comparison-using-is]returnremoved:set[str]=set()# we're doing a little song-and-dance to avoid storing __themed_values__ or# an empty dict, if there's no theme that applies to this HasProps instance.ifold_dictisnotNone:removed.update(set(old_dict.keys()))added=set(property_values.keys())old_values:dict[str,Any]={}forkinadded.union(removed):old_values[k]=getattr(self,k)iflen(property_values)>0:setattr(self,'__themed_values__',property_values)elifhasattr(self,'__themed_values__'):delattr(self,'__themed_values__')# Property container values might be cached even if unmodified. Invalidate# any cached values that are not modified at this point.fork,vinold_values.items():ifkinself._unstable_themed_values:delself._unstable_themed_values[k]# Emit any change notifications that resultfork,vinold_values.items():descriptor=self.lookup(k)ifisinstance(descriptor,PropertyDescriptor):descriptor.trigger_if_changed(self,v)
[docs]defunapply_theme(self)->None:''' Remove any themed values and restore defaults. Returns: None '''self.apply_theme(property_values={})
[docs]defclone(self,**overrides:Any)->Self:''' Duplicate a ``HasProps`` object. This creates a shallow clone of the original model, i.e. any mutable containers or child models will not be duplicated. Allows to override particular properties while cloning. '''attrs=self.properties_with_values(include_defaults=False,include_undefined=True)existing={key:valforkey,valinattrs.items()ifvalisnotUndefined}properties={**existing,**overrides}returnself.__class__(**properties)
KindRef=Any# TODOclassPropertyDef(TypedDict):name:strkind:KindRefdefault:NotRequired[Any]classOverrideDef(TypedDict):name:strdefault:AnyclassModelDef(TypedDict):type:Literal["model"]name:strextends:NotRequired[Ref|None]properties:NotRequired[list[PropertyDef]]overrides:NotRequired[list[OverrideDef]]def_HasProps_to_serializable(cls:type[HasProps],serializer:Serializer)->Ref|ModelDef:from..modelimportDataModel,Modelref=Ref(id=ID(cls.__qualified_model__))serializer.add_ref(cls,ref)ifnotis_DataModel(cls):returnref# TODO: consider supporting mixin modelsbases:list[type[HasProps]]=[baseforbaseincls.__bases__ifissubclass(base,Model)andbase!=DataModel]iflen(bases)==0:extends=Noneeliflen(bases)==1:[base]=basesextends=serializer.encode(base)else:serializer.error("multiple bases are not supported")properties:list[PropertyDef]=[]overrides:list[OverrideDef]=[]# TODO: don't use unordered setsforprop_nameincls.__properties__:descriptor=cls.lookup(prop_name)kind="Any"# TODO: serialize kindsdefault=descriptor.property._defaultifdefaultisUndefined:prop_def=PropertyDef(name=prop_name,kind=kind)else:ifdescriptor.is_unstable(default):default=default()prop_def=PropertyDef(name=prop_name,kind=kind,default=serializer.encode(default))properties.append(prop_def)forprop_name,defaultingetattr(cls,"__overridden_defaults__",{}).items():overrides.append(OverrideDef(name=prop_name,default=serializer.encode(default)))modeldef=ModelDef(type="model",name=cls.__qualified_model__,)ifextendsisnotNone:modeldef["extends"]=extendsifproperties:modeldef["properties"]=propertiesifoverrides:modeldef["overrides"]=overridesreturnmodeldefSerializer.register(MetaHasProps,_HasProps_to_serializable)#-----------------------------------------------------------------------------# Private API#-----------------------------------------------------------------------------_ABSTRACT_ADMONITION=''' .. note:: This is an abstract base class used to help organize the hierarchy of Bokeh model types. **It is not useful to instantiate on its own.**'''#-----------------------------------------------------------------------------# Code#-----------------------------------------------------------------------------