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''' Provide utility functions for implementing the ``bokeh`` command. 8 9''' 10#----------------------------------------------------------------------------- 11# Boilerplate 12#----------------------------------------------------------------------------- 13import logging # isort:skip 14log = logging.getLogger(__name__) 15 16#----------------------------------------------------------------------------- 17# Imports 18#----------------------------------------------------------------------------- 19 20# Standard library imports 21import contextlib 22import errno 23import os 24import sys 25import warnings 26from typing import Dict, Iterator, List, Optional, Sequence 27 28# Bokeh imports 29from bokeh.application import Application 30from bokeh.application.handlers import ( 31 DirectoryHandler, 32 Handler, 33 NotebookHandler, 34 ScriptHandler, 35) 36from bokeh.document import Document 37from bokeh.models import Plot 38 39#----------------------------------------------------------------------------- 40# Globals and constants 41#----------------------------------------------------------------------------- 42 43__all__ = ( 44 'build_single_handler_application', 45 'build_single_handler_applications', 46 'die', 47 'report_server_init_errors', 48 'set_single_plot_width_height', 49) 50 51#----------------------------------------------------------------------------- 52# General API 53#----------------------------------------------------------------------------- 54 55def die(message: str, status: Optional[int] = 1) -> None: 56 ''' Print an error message and exit. 57 58 This function will call ``sys.exit`` with the given ``status`` and the 59 process will terminate. 60 61 Args: 62 message (str) : error message to print 63 64 status (int) : the exit status to pass to ``sys.exit`` 65 66 ''' 67 print(message, file=sys.stderr) 68 sys.exit(status) 69 70DIRSTYLE_MAIN_WARNING = """ 71It looks like you might be running the main.py of a directory app directly. 72If this is the case, to enable the features of directory style apps, you must 73call "bokeh serve" on the directory instead. For example: 74 75 bokeh serve my_app_dir/ 76 77If this is not the case, renaming main.py will suppress this warning. 78""" 79 80def build_single_handler_application(path: str, argv: Optional[Sequence[str]] = None) -> Application: 81 ''' Return a Bokeh application built using a single handler for a script, 82 notebook, or directory. 83 84 In general a Bokeh :class:`~bokeh.application.application.Application` may 85 have any number of handlers to initialize :class:`~bokeh.document.Document` 86 objects for new client sessions. However, in many cases only a single 87 handler is needed. This function examines the ``path`` provided, and 88 returns an ``Application`` initialized with one of the following handlers: 89 90 * :class:`~bokeh.application.handlers.script.ScriptHandler` when ``path`` 91 is to a ``.py`` script. 92 93 * :class:`~bokeh.application.handlers.notebook.NotebookHandler` when 94 ``path`` is to an ``.ipynb`` Jupyter notebook. 95 96 * :class:`~bokeh.application.handlers.directory.DirectoryHandler` when 97 ``path`` is to a directory containing a ``main.py`` script. 98 99 Args: 100 path (str) : path to a file or directory for creating a Bokeh 101 application. 102 103 argv (seq[str], optional) : command line arguments to pass to the 104 application handler 105 106 Returns: 107 :class:`~bokeh.application.application.Application` 108 109 Raises: 110 RuntimeError 111 112 Notes: 113 If ``path`` ends with a file ``main.py`` then a warning will be printed 114 regarding running directory-style apps by passing the directory instead. 115 116 ''' 117 argv = argv or [] 118 path = os.path.abspath(path) 119 handler: Handler 120 121 # There are certainly race conditions here if the file/directory is deleted 122 # in between the isdir/isfile tests and subsequent code. But it would be a 123 # failure if they were not there to begin with, too (just a different error) 124 if os.path.isdir(path): 125 handler = DirectoryHandler(filename=path, argv=argv) 126 elif os.path.isfile(path): 127 if path.endswith(".ipynb"): 128 handler = NotebookHandler(filename=path, argv=argv) 129 elif path.endswith(".py"): 130 if path.endswith("main.py"): 131 warnings.warn(DIRSTYLE_MAIN_WARNING) 132 handler = ScriptHandler(filename=path, argv=argv) 133 else: 134 raise ValueError("Expected a '.py' script or '.ipynb' notebook, got: '%s'" % path) 135 else: 136 raise ValueError("Path for Bokeh server application does not exist: %s" % path) 137 138 if handler.failed: 139 raise RuntimeError("Error loading %s:\n\n%s\n%s " % (path, handler.error, handler.error_detail)) 140 141 application = Application(handler) 142 143 return application 144 145def build_single_handler_applications(paths: List[str], argvs: Optional[Dict[str, List[str]]] = None) -> Dict[str, Application]: 146 ''' Return a dictionary mapping routes to Bokeh applications built using 147 single handlers, for specified files or directories. 148 149 This function iterates over ``paths`` and ``argvs`` and calls 150 :func:`~bokeh.command.util.build_single_handler_application` on each 151 to generate the mapping. 152 153 Args: 154 paths (seq[str]) : paths to files or directories for creating Bokeh 155 applications. 156 157 argvs (dict[str, list[str]], optional) : mapping of paths to command 158 line arguments to pass to the handler for each path 159 160 Returns: 161 dict[str, Application] 162 163 Raises: 164 RuntimeError 165 166 ''' 167 applications: Dict[str, Application] = {} 168 argvs = argvs or {} 169 170 for path in paths: 171 application = build_single_handler_application(path, argvs.get(path, [])) 172 173 route = application.handlers[0].url_path() 174 175 if not route: 176 if '/' in applications: 177 raise RuntimeError("Don't know the URL path to use for %s" % (path)) 178 route = '/' 179 applications[route] = application 180 181 return applications 182 183 184@contextlib.contextmanager 185def report_server_init_errors(address: Optional[str] = None, port: Optional[int] = None, **kwargs: str) -> Iterator[None]: 186 ''' A context manager to help print more informative error messages when a 187 ``Server`` cannot be started due to a network problem. 188 189 Args: 190 address (str) : network address that the server will be listening on 191 192 port (int) : network address that the server will be listening on 193 194 Example: 195 196 .. code-block:: python 197 198 with report_server_init_errors(**server_kwargs): 199 server = Server(applications, **server_kwargs) 200 201 If there are any errors (e.g. port or address in already in use) then a 202 critical error will be logged and the process will terminate with a 203 call to ``sys.exit(1)`` 204 205 ''' 206 try: 207 yield 208 except OSError as e: 209 if e.errno == errno.EADDRINUSE: 210 log.critical("Cannot start Bokeh server, port %s is already in use", port) 211 elif e.errno == errno.EADDRNOTAVAIL: 212 log.critical("Cannot start Bokeh server, address '%s' not available", address) 213 else: 214 codename = errno.errorcode[e.errno] 215 log.critical("Cannot start Bokeh server [%s]: %r", codename, e) 216 sys.exit(1) 217 218def set_single_plot_width_height(doc: Document, width: Optional[int], height: Optional[int]) -> None: 219 if width is not None or height is not None: 220 layout = doc.roots 221 if len(layout) != 1 or not isinstance(layout[0], Plot): 222 warnings.warn("Width/height arguments will be ignored for this muliple layout. (Size valus only apply when exporting single plots.)") 223 else: 224 plot = layout[0] 225 # TODO - below fails mypy check 226 # unsure how to handle with typing. width is int base type and class property getter is typing.Int 227 # plot.plot_width = width if width is not None else plot.plot_width # doesnt solve problem 228 plot.plot_height = height or plot.plot_height 229 plot.plot_width = width or plot.plot_width 230 231#----------------------------------------------------------------------------- 232# Dev API 233#----------------------------------------------------------------------------- 234 235#----------------------------------------------------------------------------- 236# Private API 237#----------------------------------------------------------------------------- 238 239#----------------------------------------------------------------------------- 240# Code 241#----------------------------------------------------------------------------- 242