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