File: C:/Users/fred/anaconda3/Lib/site-packages/bokeh/embed/bundle.py
#-----------------------------------------------------------------------------
# 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__ import annotations
import logging # isort:skip
log = logging.getLogger(__name__)
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
# Standard library imports
import hashlib
import json
import os
from dataclasses import dataclass
from os.path import normpath
from pathlib import Path
from typing import (
TYPE_CHECKING,
Callable,
Iterator,
Sequence,
TypedDict,
)
from urllib.parse import urljoin
# Bokeh imports
from ..core.has_props import HasProps
from ..core.templates import CSS_RESOURCES, JS_RESOURCES
from ..document.document import Document
from ..resources import Resources
from ..settings import settings
from ..util.compiler import bundle_models
from .util import contains_tex_string
if TYPE_CHECKING:
from typing_extensions import NotRequired
from ..resources import Hashes
#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------
__all__ = (
'Bundle',
'bundle_for_objs_and_resources',
)
#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------
class Artifact:
pass
class ScriptRef(Artifact):
def __init__(self, url: str, type: str = "text/javascript") -> None:
self.url = URL(url)
self.type = type
class Script(Artifact):
def __init__(self, content: str, type: str = "text/javascript") -> None:
self.content = content
self.type = type
class StyleRef(Artifact):
def __init__(self, url: str) -> None:
self.url = URL(url)
class Style(Artifact):
def __init__(self, content: str) -> None:
self.content = content
class Bundle:
js_files: list[URL]
js_raw: list[str]
css_files: list[URL]
css_raw: list[str]
hashes: Hashes
def __init__(self, js_files: list[URL] = [], js_raw: list[str] = [],
css_files: list[URL] = [], css_raw: list[str] = [], hashes: Hashes = {}):
self.js_files = js_files[:]
self.js_raw = js_raw[:]
self.css_files = css_files[:]
self.css_raw = css_raw[:]
self.hashes = {**hashes}
def __iter__(self) -> Iterator[str]:
yield self._render_js()
yield self._render_css()
def _render_js(self) -> str:
return JS_RESOURCES.render(js_files=self.js_files, js_raw=self.js_raw, hashes=self.hashes)
def _render_css(self) -> str:
return CSS_RESOURCES.render(css_files=self.css_files, css_raw=self.css_raw)
def scripts(self, tag: bool = True) -> str:
if tag:
return JS_RESOURCES.render(js_raw=self.js_raw, js_files=[])
else:
return "\n".join(self.js_raw)
@property
def js_urls(self) -> list[URL]:
return self.js_files
@property
def css_urls(self) -> list[URL]:
return self.css_files
def add(self, artifact: Artifact) -> None:
if isinstance(artifact, ScriptRef):
self.js_files.append(artifact.url)
elif isinstance(artifact, Script):
self.js_raw.append(artifact.content)
elif isinstance(artifact, StyleRef):
self.css_files.append(artifact.url)
elif isinstance(artifact, Style):
self.css_raw.append(artifact.content)
def bundle_for_objs_and_resources(objs: Sequence[HasProps | Document] | None, resources: Resources | None) -> Bundle:
''' Generate rendered CSS and JS resources suitable for the given
collection of Bokeh objects
Args:
objs (seq[HasProps or Document]) :
resources (Resources)
Returns:
Bundle
'''
if objs is not None:
all_objs = _all_objs(objs)
use_widgets = _use_widgets(all_objs)
use_tables = _use_tables(all_objs)
use_gl = _use_gl(all_objs)
use_mathjax = _use_mathjax(all_objs)
else:
# XXX: force all components on server and in notebook, because we don't know in advance what will be used
all_objs = None
use_widgets = True
use_tables = True
use_gl = True
use_mathjax = True
js_files: list[URL] = []
js_raw: list[str] = []
css_files: list[URL] = []
css_raw: list[str] = []
if resources is not None:
components = list(resources.components)
if not use_widgets: components.remove("bokeh-widgets")
if not use_tables: components.remove("bokeh-tables")
if not use_gl: components.remove("bokeh-gl")
if not use_mathjax: components.remove("bokeh-mathjax")
resources = resources.clone(components=components)
js_files.extend(map(URL, resources.js_files))
js_raw.extend(resources.js_raw)
css_files.extend(map(URL, resources.css_files))
css_raw.extend(resources.css_raw)
extensions = _bundle_extensions(all_objs if objs else None, resources)
mode = resources.mode
if mode == "inline":
js_raw.extend([ Resources._inline(bundle.artifact_path) for bundle in extensions ])
elif mode == "server":
js_files.extend([ bundle.server_url for bundle in extensions ])
elif mode == "cdn":
for bundle in extensions:
if bundle.cdn_url is not None:
js_files.append(bundle.cdn_url)
else:
js_raw.append(Resources._inline(bundle.artifact_path))
else:
js_files.extend([ URL(str(bundle.artifact_path)) for bundle in extensions ])
models = [ obj.__class__ for obj in all_objs ] if all_objs else None
ext = bundle_models(models)
if ext is not None:
js_raw.append(ext)
return Bundle(js_files, js_raw, css_files, css_raw, resources.hashes if resources else {})
#-----------------------------------------------------------------------------
# Private API
#-----------------------------------------------------------------------------
def _query_extensions(all_objs: set[HasProps], query: Callable[[type[HasProps]], bool]) -> bool:
names: set[str] = set()
for obj in all_objs:
if hasattr(obj, "__implementation__"):
continue
name = obj.__view_module__.split(".")[0]
if name == "bokeh":
continue
if name in names:
continue
names.add(name)
for model in HasProps.model_class_reverse_map.values():
if model.__module__.startswith(name):
if query(model):
return True
return False
@dataclass(frozen=True)
class URL:
""" Opaque type for representing URLs. """
url: str
def __truediv__(self, path: str) -> URL:
url = self.url if self.url.endswith("/") else f"{self.url}/"
return URL(urljoin(url, path.replace(os.sep, "/")))
def __str__(self) -> str:
return self.url
@dataclass(frozen=True)
class ExtensionEmbed:
artifact_path: Path
server_url: URL
cdn_url: URL | None = None
class Pkg(TypedDict):
name: NotRequired[str]
version: NotRequired[str]
module: NotRequired[str]
main: NotRequired[str]
_default_cdn_host = URL("https://unpkg.com")
extension_dirs: dict[str, Path] = {}
def _bundle_extensions(objs: set[HasProps] | None, resources: Resources) -> list[ExtensionEmbed]:
names: set[str] = set()
bundles: list[ExtensionEmbed] = []
extensions = [".min.js", ".js"] if resources.minified else [".js"]
all_objs = objs if objs is not None else HasProps.model_class_reverse_map.values()
for obj in all_objs:
if hasattr(obj, "__implementation__"):
continue
name = obj.__view_module__.split(".")[0]
if name == "bokeh":
continue
if name in names:
continue
names.add(name)
module = __import__(name)
this_file = Path(module.__file__).absolute()
base_dir = this_file.parent
dist_dir = base_dir / "dist"
ext_path = base_dir / "bokeh.ext.json"
if not ext_path.exists():
continue
server_prefix = URL(resources.root_url) / "static" / "extensions"
package_path = base_dir / "package.json"
pkg: Pkg | None = None
if package_path.exists():
with open(package_path) as io:
try:
pkg = json.load(io)
except json.decoder.JSONDecodeError:
pass
artifact_path: Path
server_url: URL
cdn_url: URL | None = None
if pkg is not None:
pkg_name: str | None = pkg.get("name", None)
if pkg_name is None:
raise ValueError("invalid package.json; missing package name")
pkg_version = pkg.get("version", "latest")
pkg_main = pkg.get("module", pkg.get("main", None))
if pkg_main is not None:
pkg_main = Path(normpath(pkg_main))
cdn_url = _default_cdn_host / f"{pkg_name}@{pkg_version}" / f"{pkg_main}"
else:
pkg_main = dist_dir / f"{name}.js"
artifact_path = base_dir / pkg_main
artifacts_dir = artifact_path.parent
artifact_name = artifact_path.name
server_path = f"{name}/{artifact_name}"
if not settings.dev:
sha = hashlib.sha256()
sha.update(pkg_version.encode())
vstring = sha.hexdigest()
server_path = f"{server_path}?v={vstring}"
else:
for ext in extensions:
artifact_path = dist_dir / f"{name}{ext}"
artifacts_dir = dist_dir
server_path = f"{name}/{name}{ext}"
if artifact_path.exists():
break
else:
raise ValueError(f"can't resolve artifact path for '{name}' extension")
extension_dirs[name] = Path(artifacts_dir)
server_url = server_prefix / server_path
embed = ExtensionEmbed(artifact_path, server_url, cdn_url)
bundles.append(embed)
return bundles
def _all_objs(objs: Sequence[HasProps | Document]) -> set[HasProps]:
all_objs: set[HasProps] = set()
for obj in objs:
if isinstance(obj, Document):
for root in obj.roots:
all_objs |= root.references()
else:
all_objs |= obj.references()
return all_objs
def _any(objs: set[HasProps], query: Callable[[HasProps], bool]) -> bool:
''' Whether any of a collection of objects satisfies a given query predicate
Args:
objs (set[HasProps]) :
query (callable)
Returns:
True, if ``query(obj)`` is True for some object in ``objs``, else False
'''
return any(query(x) for x in objs)
def _use_tables(all_objs: set[HasProps]) -> bool:
''' Whether a collection of Bokeh objects contains a TableWidget
Args:
objs (seq[HasProps or Document]) :
Returns:
bool
'''
from ..models.widgets import TableWidget
return _any(all_objs, lambda obj: isinstance(obj, TableWidget)) or _ext_use_tables(all_objs)
def _use_widgets(all_objs: set[HasProps]) -> bool:
''' Whether a collection of Bokeh objects contains a any Widget
Args:
objs (seq[HasProps or Document]) :
Returns:
bool
'''
from ..models.widgets import Widget
return _any(all_objs, lambda obj: isinstance(obj, Widget)) or _ext_use_widgets(all_objs)
def _model_requires_mathjax(model: HasProps) -> bool:
"""Whether a model requires MathJax to be loaded
Args:
model (HasProps): HasProps to check
Returns:
bool: True if MathJax required, False if not
"""
# TODO query model's properties that include TextLike or better
# yet load mathjax bundle dynamically on bokehjs' side.
from ..models.annotations import TextAnnotation
from ..models.axes import Axis
from ..models.widgets.markups import Div, Paragraph
from ..models.widgets.sliders import AbstractSlider
if isinstance(model, TextAnnotation):
if isinstance(model.text, str) and contains_tex_string(model.text):
return True
if isinstance(model, AbstractSlider):
if isinstance(model.title, str) and contains_tex_string(model.title):
return True
if isinstance(model, Axis):
if isinstance(model.axis_label, str) and contains_tex_string(model.axis_label):
return True
for val in model.major_label_overrides.values():
if isinstance(val, str) and contains_tex_string(val):
return True
if isinstance(model, Div) and not model.disable_math and not model.render_as_text:
if contains_tex_string(model.text):
return True
if isinstance(model, Paragraph) and not model.disable_math:
if contains_tex_string(model.text):
return True
return False
def _use_mathjax(all_objs: set[HasProps]) -> bool:
''' Whether a collection of Bokeh objects contains a model requesting MathJax
Args:
objs (seq[HasProps or Document]) :
Returns:
bool
'''
from ..models.glyphs import MathTextGlyph
from ..models.text import MathText
return _any(all_objs, lambda obj: isinstance(obj, MathTextGlyph | MathText) or _model_requires_mathjax(obj)) or _ext_use_mathjax(all_objs)
def _use_gl(all_objs: set[HasProps]) -> bool:
''' Whether a collection of Bokeh objects contains a plot requesting WebGL
Args:
objs (seq[HasProps or Document]) :
Returns:
bool
'''
from ..models.plots import Plot
return _any(all_objs, lambda obj: isinstance(obj, Plot) and obj.output_backend == "webgl")
def _ext_use_tables(all_objs: set[HasProps]) -> bool:
from ..models.widgets import TableWidget
return _query_extensions(all_objs, lambda cls: issubclass(cls, TableWidget))
def _ext_use_widgets(all_objs: set[HasProps]) -> bool:
from ..models.widgets import Widget
return _query_extensions(all_objs, lambda cls: issubclass(cls, Widget))
def _ext_use_mathjax(all_objs: set[HasProps]) -> bool:
from ..models.text import MathText
return _query_extensions(all_objs, lambda cls: issubclass(cls, MathText))
#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------