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