1#----------------------------------------------------------------------------- 2# Copyright (c) 2012 - 2021, Anaconda, Inc., and Bokeh Contributors. 3# All rights reserved. 4# 5# The full license is in the file LICENSE.txt, distributed with this software. 6#----------------------------------------------------------------------------- 7''' 8 9''' 10 11#----------------------------------------------------------------------------- 12# Boilerplate 13#----------------------------------------------------------------------------- 14import logging # isort:skip 15log = logging.getLogger(__name__) 16 17#----------------------------------------------------------------------------- 18# Imports 19#----------------------------------------------------------------------------- 20 21# Standard library imports 22from collections import OrderedDict 23from collections.abc import Sequence 24from contextlib import contextmanager 25 26# Bokeh imports 27from ..document.document import Document 28from ..model import Model, collect_models 29from ..settings import settings 30from ..util.serialization import make_globally_unique_id 31 32#----------------------------------------------------------------------------- 33# Globals and constants 34#----------------------------------------------------------------------------- 35 36__all__ = ( 37 'FromCurdoc', 38 'OutputDocumentFor', 39 'RenderItem', 40 'RenderRoot', 41 'RenderRoots', 42 'standalone_docs_json', 43 'standalone_docs_json_and_render_items', 44 'submodel_has_python_callbacks', 45) 46 47#----------------------------------------------------------------------------- 48# General API 49#----------------------------------------------------------------------------- 50 51#----------------------------------------------------------------------------- 52# Dev API 53#----------------------------------------------------------------------------- 54 55class FromCurdoc: 56 ''' This class merely provides a non-None default value for ``theme`` 57 arguments, since ``None`` itself is a meaningful value for users to pass. 58 59 ''' 60 pass 61 62@contextmanager 63def OutputDocumentFor(objs, apply_theme=None, always_new=False): 64 ''' Find or create a (possibly temporary) Document to use for serializing 65 Bokeh content. 66 67 Typical usage is similar to: 68 69 .. code-block:: python 70 71 with OutputDocumentFor(models): 72 (docs_json, [render_item]) = standalone_docs_json_and_render_items(models) 73 74 Inside the context manager, the models will be considered to be part of a single 75 Document, with any theme specified, which can thus be serialized as a unit. Where 76 possible, OutputDocumentFor attempts to use an existing Document. However, this is 77 not possible in three cases: 78 79 * If passed a series of models that have no Document at all, a new Document will 80 be created, and all the models will be added as roots. After the context manager 81 exits, the new Document will continue to be the models' document. 82 83 * If passed a subset of Document.roots, then OutputDocumentFor temporarily "re-homes" 84 the models in a new bare Document that is only available inside the context manager. 85 86 * If passed a list of models that have different documents, then OutputDocumentFor 87 temporarily "re-homes" the models in a new bare Document that is only available 88 inside the context manager. 89 90 OutputDocumentFor will also perfom document validation before yielding, if 91 ``settings.perform_document_validation()`` is True. 92 93 94 objs (seq[Model]) : 95 a sequence of Models that will be serialized, and need a common document 96 97 apply_theme (Theme or FromCurdoc or None, optional): 98 Sets the theme for the doc while inside this context manager. (default: None) 99 100 If None, use whatever theme is on the document that is found or created 101 102 If FromCurdoc, use curdoc().theme, restoring any previous theme afterwards 103 104 If a Theme instance, use that theme, restoring any previous theme afterwards 105 106 always_new (bool, optional) : 107 Always return a new document, even in cases where it is otherwise possible 108 to use an existing document on models. 109 110 Yields: 111 Document 112 113 ''' 114 # Note: Comms handling relies on the fact that the new_doc returned 115 # has models with the same IDs as they were started with 116 117 if not isinstance(objs, Sequence) or len(objs) == 0 or not all(isinstance(x, Model) for x in objs): 118 raise ValueError("OutputDocumentFor expects a sequence of Models") 119 120 def finish(): pass 121 122 docs = {x.document for x in objs} 123 docs.discard(None) 124 125 if always_new: 126 def finish(): # NOQA 127 _dispose_temp_doc(objs) 128 doc = _create_temp_doc(objs) 129 130 else: 131 if len(docs) == 0: 132 doc = Document() 133 for model in objs: 134 doc.add_root(model) 135 136 # handle a single shared document 137 elif len(docs) == 1: 138 doc = docs.pop() 139 140 # we are not using all the roots, make a quick clone for outputting purposes 141 if set(objs) != set(doc.roots): 142 def finish(): # NOQA 143 _dispose_temp_doc(objs) 144 doc = _create_temp_doc(objs) 145 146 # we are using all the roots of a single doc, just use doc as-is 147 pass # lgtm [py/unnecessary-pass] 148 149 # models have mixed docs, just make a quick clone 150 else: 151 def finish(): # NOQA 152 _dispose_temp_doc(objs) 153 doc = _create_temp_doc(objs) 154 155 if settings.perform_document_validation(): 156 doc.validate() 157 158 _set_temp_theme(doc, apply_theme) 159 160 yield doc 161 162 _unset_temp_theme(doc) 163 164 finish() 165 166 167class RenderItem: 168 def __init__(self, docid=None, token=None, elementid=None, roots=None, use_for_title=None): 169 if (docid is None and token is None) or (docid is not None and token is not None): 170 raise ValueError("either docid or sessionid must be provided") 171 172 if roots is None: 173 roots = OrderedDict() 174 elif isinstance(roots, list): 175 roots = OrderedDict([ (root, make_globally_unique_id()) for root in roots ]) 176 177 self.docid = docid 178 self.token = token 179 self.elementid = elementid 180 self.roots = RenderRoots(roots) 181 self.use_for_title = use_for_title 182 183 def to_json(self): 184 json = {} 185 186 if self.docid is not None: 187 json["docid"] = self.docid 188 else: 189 json["token"] = self.token 190 191 if self.elementid is not None: 192 json["elementid"] = self.elementid 193 194 if self.roots: 195 json["roots"] = self.roots.to_json() 196 json["root_ids"] = [root.id for root in self.roots] 197 198 if self.use_for_title is not None: 199 json["use_for_title"] = self.use_for_title 200 201 return json 202 203 def __eq__(self, other): 204 if not isinstance(other, self.__class__): 205 return False 206 else: 207 return self.to_json() == other.to_json() 208 209 210class RenderRoot: 211 def __init__(self, elementid, id, name=None, tags=None): 212 self.elementid = elementid 213 self.id = id 214 self.name = name or "" 215 self.tags = tags or [] 216 217 def __eq__(self, other): 218 if not isinstance(other, self.__class__): 219 return False 220 else: 221 return self.elementid == other.elementid 222 223 224class RenderRoots: 225 def __init__(self, roots): 226 self._roots = roots 227 228 def __len__(self): 229 return len(self._roots.items()) 230 231 def __getitem__(self, key): 232 if isinstance(key, int): 233 (root, elementid) = list(self._roots.items())[key] 234 else: 235 for root, elementid in self._roots.items(): 236 if root.name == key: 237 break 238 else: 239 raise ValueError("root with '%s' name not found" % key) 240 241 return RenderRoot(elementid, root.id, root.name, root.tags) 242 243 def __getattr__(self, key): 244 return self.__getitem__(key) 245 246 def to_json(self): 247 return OrderedDict([ (root.id, elementid) for root, elementid in self._roots.items() ]) 248 249def standalone_docs_json(models): 250 ''' 251 252 ''' 253 docs_json, render_items = standalone_docs_json_and_render_items(models) 254 return docs_json 255 256def standalone_docs_json_and_render_items(models, suppress_callback_warning=False): 257 ''' 258 259 ''' 260 if isinstance(models, (Model, Document)): 261 models = [models] 262 263 if not (isinstance(models, Sequence) and all(isinstance(x, (Model, Document)) for x in models)): 264 raise ValueError("Expected a Model, Document, or Sequence of Models or Documents") 265 266 if submodel_has_python_callbacks(models) and not suppress_callback_warning: 267 log.warning(_CALLBACKS_WARNING) 268 269 docs = {} 270 for model_or_doc in models: 271 if isinstance(model_or_doc, Document): 272 model = None 273 doc = model_or_doc 274 else: 275 model = model_or_doc 276 doc = model.document 277 278 if doc is None: 279 raise ValueError("A Bokeh Model must be part of a Document to render as standalone content") 280 281 if doc not in docs: 282 docs[doc] = (make_globally_unique_id(), OrderedDict()) 283 284 (docid, roots) = docs[doc] 285 286 if model is not None: 287 roots[model] = make_globally_unique_id() 288 else: 289 for model in doc.roots: 290 roots[model] = make_globally_unique_id() 291 292 docs_json = {} 293 for doc, (docid, _) in docs.items(): 294 docs_json[docid] = doc.to_json() 295 296 render_items = [] 297 for _, (docid, roots) in docs.items(): 298 render_items.append(RenderItem(docid, roots=roots)) 299 300 return (docs_json, render_items) 301 302def submodel_has_python_callbacks(models): 303 ''' Traverses submodels to check for Python (event) callbacks 304 305 ''' 306 has_python_callback = False 307 for model in collect_models(models): 308 if len(model._callbacks) > 0 or len(model._event_callbacks) > 0: 309 has_python_callback = True 310 break 311 312 return has_python_callback 313 314#----------------------------------------------------------------------------- 315# Private API 316#----------------------------------------------------------------------------- 317 318_CALLBACKS_WARNING = """ 319You are generating standalone HTML/JS output, but trying to use real Python 320callbacks (i.e. with on_change or on_event). This combination cannot work. 321 322Only JavaScript callbacks may be used with standalone output. For more 323information on JavaScript callbacks with Bokeh, see: 324 325 https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html 326 327Alternatively, to use real Python callbacks, a Bokeh server application may 328be used. For more information on building and running Bokeh applications, see: 329 330 https://docs.bokeh.org/en/latest/docs/user_guide/server.html 331""" 332 333def _create_temp_doc(models): 334 doc = Document() 335 for m in models: 336 doc._all_models[m.id] = m 337 m._temp_document = doc 338 for ref in m.references(): 339 doc._all_models[ref.id] = ref 340 ref._temp_document = doc 341 doc._roots = models 342 return doc 343 344def _dispose_temp_doc(models): 345 for m in models: 346 m._temp_document = None 347 for ref in m.references(): 348 ref._temp_document = None 349 350def _set_temp_theme(doc, apply_theme): 351 doc._old_theme = doc.theme 352 if apply_theme is FromCurdoc: 353 from ..io import curdoc; curdoc 354 doc.theme = curdoc().theme 355 elif apply_theme is not None: 356 doc.theme = apply_theme 357 358def _unset_temp_theme(doc): 359 if not hasattr(doc, "_old_theme"): 360 return 361 doc.theme = doc._old_theme 362 del doc._old_theme 363 364#----------------------------------------------------------------------------- 365# Code 366#----------------------------------------------------------------------------- 367