Source code for bokeh.io.util

#-----------------------------------------------------------------------------
# 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

# Standard library imports
import logging

log = logging.getLogger(__name__)

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Standard library imports
import os
import sys
from contextlib import contextmanager
from os.path import (
    basename,
    dirname,
    join,
    splitext,
)
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Iterator

# Bokeh imports
from ..embed import file_html
from ..resources import INLINE

if TYPE_CHECKING:
    from tempfile import _TemporaryFileWrapper

    from ..document import Document
    from ..embed.util import ThemeSource
    from ..model import Model
    from ..models.plots import Plot
    from ..models.ui import UIElement
    from ..resources import Resources

#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------

__all__ = (
    'default_filename',
    'detect_current_filename',
    'temp_filename',
    'tmp_html',
    'get_layout_html',
)

#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------

[docs] def default_filename(ext: str) -> str: ''' Generate a default filename with a given extension, attempting to use the filename of the currently running process, if possible. If the filename of the current process is not available (or would not be writable), then a temporary file with the given extension is returned. Args: ext (str) : the desired extension for the filename Returns: str Raises: RuntimeError If the extensions requested is ".py" ''' if ext == "py": raise RuntimeError("asked for a default filename with 'py' extension") filename = detect_current_filename() if filename is None: return temp_filename(ext) basedir = dirname(filename) or os.getcwd() if _no_access(basedir) or _shares_exec_prefix(basedir): return temp_filename(ext) name, _ = splitext(basename(filename)) return join(basedir, name + "." + ext)
[docs] def detect_current_filename() -> str | None: ''' Attempt to return the filename of the currently running Python process Returns None if the filename cannot be detected. ''' import inspect filename = None frame = inspect.currentframe() if frame is not None: try: while frame.f_back and frame.f_globals.get('name') != '__main__': frame = frame.f_back filename = frame.f_globals.get('__file__') finally: del frame return filename
[docs] def temp_filename(ext: str) -> str: ''' Generate a temporary, writable filename with the given extension ''' # todo: not safe - the file is deleted before being written to so another # process can generate the same filename with NamedTemporaryFile(suffix="." + ext) as f: return f.name
[docs] @contextmanager def tmp_html() -> Iterator[_TemporaryFileWrapper[bytes]]: '''Create a named temporary HTML file that is cleaned up on exit. According to https://docs.python.org/3/library/tempfile.html#tempfile.NamedTemporaryFile in order for named temp files to be safely re-openable on Windows, we need to set delete=False, so this context manager explicitly manages the unlink. ''' tmp = NamedTemporaryFile(mode="wb", dir=Path.home(), prefix="bokeh", suffix=".html", delete=False) try: yield tmp finally: os.unlink(tmp.name)
[docs] def get_layout_html(obj: UIElement | Document, *, resources: Resources = INLINE, width: int | None = None, height: int | None = None, theme: ThemeSource | None = None) -> str: ''' ''' template = r"""\ {% block preamble %} <style> html, body { box-sizing: border-box; width: 100%; height: 100%; margin: 0; border: 0; padding: 0; overflow: hidden; } </style> {% endblock %} """ def html() -> str: return file_html( obj, resources=resources, title="", template=template, theme=theme, suppress_callback_warning=True, _always_new=True, ) if width is not None or height is not None: # Defer this import, it is expensive from ..models.plots import Plot if not isinstance(obj, Plot): from ..util.warnings import warn warn("Export method called with width or height argument on a non-Plot model. The size values will be ignored.") else: with _resized(obj, width, height): return html() return html()
#----------------------------------------------------------------------------- # Private API #----------------------------------------------------------------------------- def _no_access(basedir: str) -> bool: ''' Return True if the given base dir is not accessible or writeable ''' return not os.access(basedir, os.W_OK | os.X_OK) def _shares_exec_prefix(basedir: str) -> bool: ''' Whether a give base directory is on the system exex prefix ''' # XXX: exec_prefix has type str so why the check? prefix: str | None = sys.exec_prefix return prefix is not None and basedir.startswith(prefix) @contextmanager def _resized(obj: Plot, width: int | None, height: int | None) -> Iterator[None]: old_width = obj.width old_height = obj.height if width is not None: obj.width = width if height is not None: obj.height = height try: yield finally: obj.width = old_width obj.height = old_height #----------------------------------------------------------------------------- # Shared JavaScript snippets for Selenium and Playwright backends #----------------------------------------------------------------------------- # Check whether Bokeh has loaded and created at least one document. _BOKEH_LOADED_CHECK = """\ return typeof Bokeh !== "undefined" && Bokeh.documents != null && Bokeh.documents.length != 0\ """ # Set a flag once the first Bokeh document becomes idle. # Both backends poll/wait on ``window._bokeh_render_complete`` afterwards. _WAIT_SCRIPT = """\ window._bokeh_render_complete = false; function done() { window._bokeh_render_complete = true; } const doc = Bokeh.documents[0]; if (doc.is_idle) done(); else doc.idle.connect(done); """ # Read the bounding box of the first root view and the device pixel ratio. _ROOT_VIEW_BBOX_SCRIPT = """\ const root_view = Bokeh.index.roots[0]; const {x, y, width, height} = root_view.el.getBoundingClientRect(); return [x, y, Math.round(width), Math.round(height), window.devicePixelRatio];\ """ # TODO: consider UIElement like Pane _SVGS_SCRIPT = """ const {LayoutDOMView} = Bokeh.require("models/layouts/layout_dom") const {PlotView} = Bokeh.require("models/plots/plot") function* collect_svgs(views) { for (const view of views) { if (view instanceof LayoutDOMView) { yield* collect_svgs(view.child_views.values()) } if (view instanceof PlotView && view.model.output_backend == "svg") { const {ctx} = view.export("svg") yield ctx.get_serialized_svg(true) } } } return [...collect_svgs(Bokeh.index)] """ def _SVG_SCRIPT(obj: Model | Document) -> str: from ..document import Document if isinstance(obj, Document): ids = [root.id for root in obj.roots] else: ids = [obj.id] return f"""\ const ids = new Set({ids}) function* export_svgs(views) {{ for (const view of views) {{ // TODO: use to_blob() API in future if (ids.has(view.model.id)) {{ const {{ctx}} = view.export("svg") yield ctx.get_serialized_svg(true) }} }} }} return [...export_svgs(Bokeh.index)] """ #----------------------------------------------------------------------------- # Code #-----------------------------------------------------------------------------