#-----------------------------------------------------------------------------# Copyright (c) Anaconda, Inc., and Bokeh Contributors.# All rights reserved.## The full license is in the file LICENSE.txt, distributed with this software.#-----------------------------------------------------------------------------''''''#-----------------------------------------------------------------------------# Boilerplate#-----------------------------------------------------------------------------from__future__importannotationsimportlogging# isort:skiplog=logging.getLogger(__name__)#-----------------------------------------------------------------------------# Imports#-----------------------------------------------------------------------------# Standard library importsimportrefromcontextlibimportcontextmanagerfromtypingimport(TYPE_CHECKING,Any,Iterator,Sequence,)fromweakrefimportWeakKeyDictionary# Bokeh importsfrom..core.typesimportIDfrom..document.documentimportDocumentfrom..modelimportModel,collect_modelsfrom..settingsimportsettingsfrom..themes.themeimportThemefrom..util.dataclassesimportdataclass,fieldfrom..util.serializationimport(make_globally_unique_css_safe_id,make_globally_unique_id,)ifTYPE_CHECKING:from..document.documentimportDocJson#-----------------------------------------------------------------------------# Globals and constants#-----------------------------------------------------------------------------__all__=('contains_tex_string','FromCurdoc','is_tex_string','OutputDocumentFor','RenderItem','RenderRoot','RenderRoots','standalone_docs_json','standalone_docs_json_and_render_items','submodel_has_python_callbacks',)#-----------------------------------------------------------------------------# General API#-----------------------------------------------------------------------------#-----------------------------------------------------------------------------# Dev API#-----------------------------------------------------------------------------classFromCurdoc:''' This class merely provides a non-None default value for ``theme`` arguments, since ``None`` itself is a meaningful value for users to pass. '''pass@contextmanagerdefOutputDocumentFor(objs:Sequence[Model],apply_theme:Theme|type[FromCurdoc]|None=None,always_new:bool=False)->Iterator[Document]:''' Find or create a (possibly temporary) Document to use for serializing Bokeh content. Typical usage is similar to: .. code-block:: python with OutputDocumentFor(models): (docs_json, [render_item]) = standalone_docs_json_and_render_items(models) Inside the context manager, the models will be considered to be part of a single Document, with any theme specified, which can thus be serialized as a unit. Where possible, OutputDocumentFor attempts to use an existing Document. However, this is not possible in three cases: * If passed a series of models that have no Document at all, a new Document will be created, and all the models will be added as roots. After the context manager exits, the new Document will continue to be the models' document. * If passed a subset of Document.roots, then OutputDocumentFor temporarily "re-homes" the models in a new bare Document that is only available inside the context manager. * If passed a list of models that have different documents, then OutputDocumentFor temporarily "re-homes" the models in a new bare Document that is only available inside the context manager. OutputDocumentFor will also perfom document validation before yielding, if ``settings.perform_document_validation()`` is True. objs (seq[Model]) : a sequence of Models that will be serialized, and need a common document apply_theme (Theme or FromCurdoc or None, optional): Sets the theme for the doc while inside this context manager. (default: None) If None, use whatever theme is on the document that is found or created If FromCurdoc, use curdoc().theme, restoring any previous theme afterwards If a Theme instance, use that theme, restoring any previous theme afterwards always_new (bool, optional) : Always return a new document, even in cases where it is otherwise possible to use an existing document on models. Yields: Document '''# Note: Comms handling relies on the fact that the new_doc returned# has models with the same IDs as they were started withifnotisinstance(objs,Sequence)orlen(objs)==0ornotall(isinstance(x,Model)forxinobjs):raiseValueError("OutputDocumentFor expects a non-empty sequence of Models")deffinish()->None:passdocs={obj.documentforobjinobjsifobj.documentisnotNone}ifalways_new:deffinish()->None:_dispose_temp_doc(objs)doc=_create_temp_doc(objs)else:iflen(docs)==0:doc=_new_doc()formodelinobjs:doc.add_root(model)# handle a single shared documenteliflen(docs)==1:doc=docs.pop()# we are not using all the roots, make a quick clone for outputting purposesifset(objs)!=set(doc.roots):deffinish()->None:_dispose_temp_doc(objs)doc=_create_temp_doc(objs)# we are using all the roots of a single doc, just use doc as-ispass# lgtm [py/unnecessary-pass]# models have mixed docs, just make a quick cloneelse:deffinish():_dispose_temp_doc(objs)doc=_create_temp_doc(objs)ifsettings.perform_document_validation():doc.validate()_set_temp_theme(doc,apply_theme)yielddoc_unset_temp_theme(doc)finish()classRenderItem:def__init__(self,docid:ID|None=None,token:str|None=None,elementid:ID|None=None,roots:list[Model]|dict[Model,ID]|None=None,use_for_title:bool|None=None):if(docidisNoneandtokenisNone)or(docidisnotNoneandtokenisnotNone):raiseValueError("either docid or sessionid must be provided")ifrootsisNone:roots=dict()elifisinstance(roots,list):roots={root:make_globally_unique_id()forrootinroots}self.docid=docidself.token=tokenself.elementid=elementidself.roots=RenderRoots(roots)self.use_for_title=use_for_titledefto_json(self)->dict[str,Any]:json:dict[str,Any]={}ifself.docidisnotNone:json["docid"]=self.docidelse:json["token"]=self.tokenifself.elementidisnotNone:json["elementid"]=self.elementidifself.roots:json["roots"]=self.roots.to_json()json["root_ids"]=[root.idforrootinself.roots]ifself.use_for_titleisnotNone:json["use_for_title"]=self.use_for_titlereturnjsondef__eq__(self,other:Any)->bool:ifnotisinstance(other,self.__class__):returnFalseelse:returnself.to_json()==other.to_json()
[docs]@dataclassclassRenderRoot:""" Encapsulate data needed for embedding a Bokeh document root. Values for ``name`` or ``tags`` are optional. They may be useful for querying a collection of roots to find a specific one to embed. """#: A unique ID to use for the DOM elementelementid:ID#: The Bokeh model ID for this rootid:ID=field(compare=False)#: An optional user-supplied name for this rootname:str|None=field(default="",compare=False)#: A list of any user-supplied tag values for this roottags:list[Any]=field(default_factory=list,compare=False)def__post_init__(self):# Model.name is nullable, and field() won't enforce the default when name=Noneself.name=self.nameor""
classRenderRoots:def__init__(self,roots:dict[Model,ID])->None:self._roots=rootsdef__iter__(self)->Iterator[RenderRoot]:foriinrange(0,len(self)):yieldself[i]def__len__(self):returnlen(self._roots.items())def__getitem__(self,key:int|str)->RenderRoot:ifisinstance(key,int):(root,elementid)=list(self._roots.items())[key]else:forroot,elementidinself._roots.items():ifroot.name==key:breakelse:raiseValueError(f"root with {key!r} name not found")returnRenderRoot(elementid,root.id,root.name,root.tags)def__getattr__(self,key:str)->RenderRoot:returnself.__getitem__(key)defto_json(self)->dict[ID,ID]:return{root.id:elementidforroot,elementidinself._roots.items()}def__repr__(self)->str:returnrepr(self._roots)defstandalone_docs_json(models:Sequence[Model|Document])->dict[ID,DocJson]:''' '''docs_json,_=standalone_docs_json_and_render_items(models)returndocs_jsondefstandalone_docs_json_and_render_items(models:Model|Document|Sequence[Model|Document],*,suppress_callback_warning:bool=False)->tuple[dict[ID,DocJson],list[RenderItem]]:''' '''ifisinstance(models,(Model,Document)):models=[models]ifnot(isinstance(models,Sequence)andall(isinstance(x,(Model,Document))forxinmodels)):raiseValueError("Expected a Model, Document, or Sequence of Models or Documents")ifsubmodel_has_python_callbacks(models)andnotsuppress_callback_warning:log.warning(_CALLBACKS_WARNING)docs:dict[Document,tuple[ID,dict[Model,ID]]]={}formodel_or_docinmodels:ifisinstance(model_or_doc,Document):model=Nonedoc=model_or_docelse:model=model_or_docdoc=model.documentifdocisNone:raiseValueError("A Bokeh Model must be part of a Document to render as standalone content")ifdocnotindocs:docs[doc]=(make_globally_unique_id(),dict())(docid,roots)=docs[doc]ifmodelisnotNone:roots[model]=make_globally_unique_css_safe_id()else:formodelindoc.roots:roots[model]=make_globally_unique_css_safe_id()docs_json:dict[ID,DocJson]={}fordoc,(docid,_)indocs.items():docs_json[docid]=doc.to_json(deferred=False)render_items:list[RenderItem]=[]for_,(docid,roots)indocs.items():render_items.append(RenderItem(docid,roots=roots))return(docs_json,render_items)defsubmodel_has_python_callbacks(models:Sequence[Model|Document])->bool:''' Traverses submodels to check for Python (event) callbacks '''has_python_callback=Falseformodelincollect_models(models):iflen(model._callbacks)>0orlen(model._event_callbacks)>0:has_python_callback=Truebreakreturnhas_python_callbackdefis_tex_string(text:str)->bool:''' Whether a string begins and ends with MathJax default delimiters Args: text (str): String to check Returns: bool: True if string begins and ends with delimiters, False if not '''dollars=r"^\$\$.*?\$\$$"braces=r"^\\\[.*?\\\]$"parens=r"^\\\(.*?\\\)$"pat=re.compile(f"{dollars}|{braces}|{parens}",flags=re.S)returnpat.match(text)isnotNonedefcontains_tex_string(text:str)->bool:''' Whether a string contains any pair of MathJax default delimiters Args: text (str): String to check Returns: bool: True if string contains delimiters, False if not '''# these are non-greedydollars=r"\$\$.*?\$\$"braces=r"\\\[.*?\\\]"parens=r"\\\(.*?\\\)"pat=re.compile(f"{dollars}|{braces}|{parens}",flags=re.S)returnpat.search(text)isnotNone#-----------------------------------------------------------------------------# Private API#-----------------------------------------------------------------------------_CALLBACKS_WARNING="""You are generating standalone HTML/JS output, but trying to use real Pythoncallbacks (i.e. with on_change or on_event). This combination cannot work.Only JavaScript callbacks may be used with standalone output. For moreinformation on JavaScript callbacks with Bokeh, see: https://docs.bokeh.org/en/latest/docs/user_guide/interaction/js_callbacks.htmlAlternatively, to use real Python callbacks, a Bokeh server application maybe used. For more information on building and running Bokeh applications, see: https://docs.bokeh.org/en/latest/docs/user_guide/server.html"""def_new_doc()->Document:# TODO: embed APIs need to actually respect the existing document's# configuration, but for now this is better than nothing.from..ioimportcurdocdoc=Document()callbacks=curdoc().callbacks._js_event_callbacksdoc.callbacks._js_event_callbacks.update(callbacks)returndocdef_create_temp_doc(models:Sequence[Model])->Document:doc=_new_doc()forminmodels:doc.models[m.id]=mm._temp_document=docforrefinm.references():doc.models[ref.id]=refref._temp_document=docdoc._roots=list(models)returndocdef_dispose_temp_doc(models:Sequence[Model])->None:forminmodels:m._temp_document=Noneforrefinm.references():ref._temp_document=None_themes:WeakKeyDictionary[Document,Theme]=WeakKeyDictionary()def_set_temp_theme(doc:Document,apply_theme:Theme|type[FromCurdoc]|None)->None:_themes[doc]=doc.themeifapply_themeisFromCurdoc:from..ioimportcurdocdoc.theme=curdoc().themeelifisinstance(apply_theme,Theme):doc.theme=apply_themedef_unset_temp_theme(doc:Document)->None:ifdocnotin_themes:returndoc.theme=_themes[doc]del_themes[doc]#-----------------------------------------------------------------------------# Code#-----------------------------------------------------------------------------