diff options
Diffstat (limited to 'venv/lib/python3.8/site-packages/dash/_jupyter.py')
-rw-r--r-- | venv/lib/python3.8/site-packages/dash/_jupyter.py | 487 |
1 files changed, 487 insertions, 0 deletions
diff --git a/venv/lib/python3.8/site-packages/dash/_jupyter.py b/venv/lib/python3.8/site-packages/dash/_jupyter.py new file mode 100644 index 0000000..ac06f84 --- /dev/null +++ b/venv/lib/python3.8/site-packages/dash/_jupyter.py @@ -0,0 +1,487 @@ +# type: ignore +import asyncio +import io +import inspect +import logging +import os +import queue +import uuid +import sys +import threading +import time + +from typing import Optional +from typing_extensions import Literal + +from werkzeug.serving import make_server + + +try: + from IPython import get_ipython + from IPython.display import IFrame, display, Javascript + from IPython.core.display import HTML + from IPython.core.ultratb import FormattedTB + from retrying import retry + from ipykernel.comm import Comm + import nest_asyncio + + import requests + + _dash_comm = Comm(target_name="dash") + _dep_installed = True +except ImportError: + _dep_installed = False + _dash_comm = None + get_ipython = lambda: None + +JupyterDisplayMode = Literal["inline", "external", "jupyterlab", "tab", "_none"] + + +def _get_skip(error: Exception): + from dash._callback import ( # pylint: disable=import-outside-toplevel + _invoke_callback, + ) + + tb = error.__traceback__ + skip = 1 + while tb.tb_next is not None: + skip += 1 + tb = tb.tb_next + if tb.tb_frame.f_code is _invoke_callback.__code__: + return skip + + return skip + + +def _custom_formatargvalues( + args, + varargs, + varkw, + locals, # pylint: disable=W0622 + formatarg=str, + formatvarargs=lambda name: "*" + name, + formatvarkw=lambda name: "**" + name, + formatvalue=lambda value: "=" + repr(value), +): + + """Copied from inspect.formatargvalues, modified to place function + arguments on separate lines""" + + # pylint: disable=W0622 + def convert(name, locals=locals, formatarg=formatarg, formatvalue=formatvalue): + return formatarg(name) + formatvalue(locals[name]) + + specs = [] + + # pylint: disable=C0200 + for i in range(len(args)): + specs.append(convert(args[i])) + if varargs: + specs.append(formatvarargs(varargs) + formatvalue(locals[varargs])) + if varkw: + specs.append(formatvarkw(varkw) + formatvalue(locals[varkw])) + + result = "(" + ", ".join(specs) + ")" + + if len(result) < 40: + return result + # Put each arg on a separate line + return "(\n " + ",\n ".join(specs) + "\n)" + + +_jupyter_config = {} + +_caller = {} + + +def _send_jupyter_config_comm_request(): + # If running in an ipython kernel, + # request that the front end extension send us the notebook server base URL + if get_ipython() is not None: + if _dash_comm.kernel is not None: + _caller["parent"] = _dash_comm.kernel.get_parent() + _dash_comm.send({"type": "base_url_request"}) + + +def _jupyter_comm_response_received(): + return bool(_jupyter_config) + + +def _request_jupyter_config(timeout=2): + # Heavily inspired by implementation of CaptureExecution in the + if _dash_comm.kernel is None: + # Not in jupyter setting + return + + _send_jupyter_config_comm_request() + + # Get shell and kernel + shell = get_ipython() + kernel = shell.kernel + + # Start capturing shell events to replay later + captured_events = [] + + def capture_event(stream, ident, parent): + captured_events.append((stream, ident, parent)) + + kernel.shell_handlers["execute_request"] = capture_event + + # increment execution count to avoid collision error + shell.execution_count += 1 + + # Allow kernel to execute comms until we receive the jupyter configuration comm + # response + t0 = time.time() + while True: + if (time.time() - t0) > timeout: + # give up + raise EnvironmentError( + "Unable to communicate with the jupyter_dash notebook or JupyterLab \n" + "extension required to infer Jupyter configuration." + ) + if _jupyter_comm_response_received(): + break + + if asyncio.iscoroutinefunction(kernel.do_one_iteration): + loop = asyncio.get_event_loop() + nest_asyncio.apply(loop) + loop.run_until_complete(kernel.do_one_iteration()) + else: + kernel.do_one_iteration() + + # Stop capturing events, revert the kernel shell handler to the default + # execute_request behavior + kernel.shell_handlers["execute_request"] = kernel.execute_request + + # Replay captured events + # need to flush before replaying so messages show up in current cell not + # replay cells + sys.stdout.flush() + sys.stderr.flush() + + for stream, ident, parent in captured_events: + # Using kernel.set_parent is the key to getting the output of the replayed + # events to show up in the cells that were captured instead of the current cell + kernel.set_parent(ident, parent) + kernel.execute_request(stream, ident, parent) + + +class JupyterDash: + """ + Interact with dash apps inside jupyter notebooks. + """ + + default_mode: JupyterDisplayMode = "inline" + alive_token = str(uuid.uuid4()) + inline_exceptions: bool = True + + _servers = {} + + def infer_jupyter_proxy_config(self): + """ + Infer the current Jupyter server configuration. This will detect + the proper request_pathname_prefix and server_url values to use when + displaying Dash apps.Dash requests will be routed through the proxy. + + Requirements: + + In the classic notebook, this method requires the `dash` nbextension + which should be installed automatically with the installation of the + jupyter-dash Python package. You can see what notebook extensions are installed + by running the following command: + $ jupyter nbextension list + + In JupyterLab, this method requires the `@plotly/dash-jupyterlab` labextension. This + extension should be installed automatically with the installation of the + jupyter-dash Python package, but JupyterLab must be allowed to rebuild before + the extension is activated (JupyterLab should automatically detect the + extension and produce a popup dialog asking for permission to rebuild). You can + see what JupyterLab extensions are installed by running the following command: + $ jupyter labextension list + """ + if not self.in_ipython or self.in_colab: + # No op when not running in a Jupyter context or when in Colab + return + # Assume classic notebook or JupyterLab + _request_jupyter_config() + + def __init__(self): + self.in_ipython = get_ipython() is not None + self.in_colab = "google.colab" in sys.modules + + if _dep_installed and self.in_ipython and _dash_comm: + + @_dash_comm.on_msg + def _receive_message(msg): + prev_parent = _caller.get("parent") + if prev_parent and prev_parent != _dash_comm.kernel.get_parent(): + _dash_comm.kernel.set_parent( + [prev_parent["header"]["session"]], prev_parent + ) + del _caller["parent"] + + msg_data = msg.get("content").get("data") + msg_type = msg_data.get("type", None) + if msg_type == "base_url_response": + _jupyter_config.update(msg_data) + + # pylint: disable=too-many-locals, too-many-branches, too-many-statements + def run_app( + self, + app, + mode: Optional[JupyterDisplayMode] = None, + width="100%", + height=650, + host="127.0.0.1", + port=8050, + server_url=None, + ): + """ + :type app: dash.Dash + :param mode: How to display the app on the notebook. One Of: + ``"external"``: The URL of the app will be displayed in the notebook + output cell. Clicking this URL will open the app in the default + web browser. + ``"inline"``: The app will be displayed inline in the notebook output cell + in an iframe. + ``"jupyterlab"``: The app will be displayed in a dedicate tab in the + JupyterLab interface. Requires JupyterLab and the `jupyterlab-dash` + extension. + :param width: Width of app when displayed using mode="inline" + :param height: Height of app when displayed using mode="inline" + :param host: Host of the server + :param port: Port used by the server + :param server_url: Use if a custom url is required to display the app. + """ + # Validate / infer display mode + if self.in_colab: + valid_display_values = ["inline", "external"] + else: + valid_display_values = ["jupyterlab", "inline", "external", "tab", "_none"] + + if mode is None: + mode = self.default_mode + elif not isinstance(mode, str): + raise ValueError( + f"The mode argument must be a string\n" + f" Received value of type {type(mode)}: {repr(mode)}" + ) + else: + mode = mode.lower() # type: ignore + if mode not in valid_display_values: + raise ValueError( + f"Invalid display argument {mode}\n" + f" Valid arguments: {valid_display_values}" + ) + + # Terminate any existing server using this port + old_server = self._servers.get((host, port)) + if old_server: + old_server.shutdown() + del self._servers[(host, port)] + + # Configure pathname prefix + if "base_subpath" in _jupyter_config: + requests_pathname_prefix = ( + _jupyter_config["base_subpath"].rstrip("/") + "/proxy/{port}/" + ) + else: + requests_pathname_prefix = app.config.get("requests_pathname_prefix", None) + + if requests_pathname_prefix is not None: + requests_pathname_prefix = requests_pathname_prefix.format(port=port) + else: + requests_pathname_prefix = "/" + + routes_pathname_prefix = app.config.get("routes_pathname_prefix", "/") + + # FIXME Move config initialization to main dash __init__ + # low-level setter to circumvent Dash's config locking + # normally it's unsafe to alter requests_pathname_prefix this late, but + # Jupyter needs some unusual behavior. + dict.__setitem__( + app.config, "requests_pathname_prefix", requests_pathname_prefix + ) + + # # Compute server_url url + if server_url is None: + if "server_url" in _jupyter_config: + server_url = _jupyter_config["server_url"].rstrip("/") + else: + domain_base = os.environ.get("DASH_DOMAIN_BASE", None) + if domain_base: + # Dash Enterprise sets DASH_DOMAIN_BASE environment variable + server_url = "https://" + domain_base + else: + server_url = f"http://{host}:{port}" + else: + server_url = server_url.rstrip("/") + + # server_url = "http://{host}:{port}".format(host=host, port=port) + + dashboard_url = f"{server_url}{requests_pathname_prefix}" + + # prevent partial import of orjson when it's installed and mode=jupyterlab + # TODO: why do we need this? Why only in this mode? Importing here in + # all modes anyway, in case there's a way it can pop up in another mode + try: + # pylint: disable=C0415,W0611 + import orjson # noqa: F401 + except ImportError: + pass + + err_q = queue.Queue() + + server = make_server(host, port, app.server, threaded=True, processes=0) + logging.getLogger("werkzeug").setLevel(logging.ERROR) + + @retry( + stop_max_attempt_number=15, + wait_exponential_multiplier=100, + wait_exponential_max=1000, + ) + def run(): + try: + server.serve_forever() + except SystemExit: + pass + except Exception as error: + err_q.put(error) + raise error + + thread = threading.Thread(target=run) + thread.daemon = True + thread.start() + + self._servers[(host, port)] = server + + # Wait for server to start up + alive_url = f"http://{host}:{port}{routes_pathname_prefix}_alive_{JupyterDash.alive_token}" + + def _get_error(): + try: + err = err_q.get_nowait() + if err: + raise err + except queue.Empty: + pass + + # Wait for app to respond to _alive endpoint + @retry( + stop_max_attempt_number=15, + wait_exponential_multiplier=10, + wait_exponential_max=1000, + ) + def wait_for_app(): + _get_error() + try: + req = requests.get(alive_url) + res = req.content.decode() + if req.status_code != 200: + raise Exception(res) + + if res != "Alive": + url = f"http://{host}:{port}" + raise OSError( + f"Address '{url}' already in use.\n" + " Try passing a different port to run." + ) + except requests.ConnectionError as err: + _get_error() + raise err + + try: + wait_for_app() + + if self.in_colab: + JupyterDash._display_in_colab(dashboard_url, port, mode, width, height) + else: + JupyterDash._display_in_jupyter( + dashboard_url, port, mode, width, height + ) + except Exception as final_error: # pylint: disable=broad-except + msg = str(final_error) + if msg.startswith("<!"): + display(HTML(msg)) + else: + raise final_error + + @staticmethod + def _display_in_colab(dashboard_url, port, mode, width, height): + # noinspection PyUnresolvedReferences + from google.colab import output # pylint: disable=E0401,E0611,C0415 + + if mode == "inline": + output.serve_kernel_port_as_iframe(port, width=width, height=height) + elif mode == "external": + # FIXME there is a 403 on this, maybe it's updated? + # Display a hyperlink that can be clicked to open Dashboard + print("Dash app running on:") + output.serve_kernel_port_as_window(port, anchor_text=dashboard_url) + + @staticmethod + def _display_in_jupyter(dashboard_url, port, mode, width, height): + if mode == "inline": + display(IFrame(dashboard_url, width, height)) + elif mode in ("external", "tab"): + # Display a hyperlink that can be clicked to open Dashboard + print(f"Dash app running on {dashboard_url}") + if mode == "tab": + display(Javascript(f"window.open('{dashboard_url}')")) + elif mode == "jupyterlab": + # Update front-end extension + # FIXME valid only in jupyterlab but accepted in regular notebooks show nothing. + _dash_comm.send( + { + "type": "show", + "port": port, + "url": dashboard_url, + } + ) + + @staticmethod + def serve_alive(): + return "Alive" + + def configure_callback_exception_handling(self, app, dev_tools_prune_errors): + """Install traceback handling for callbacks""" + + @app.server.errorhandler(Exception) + def _wrap_errors(error): + # Compute number of stack frames to skip to get down to callback + skip = _get_skip(error) if dev_tools_prune_errors else 0 + + # Customized formatargvalues function we can place function parameters + # on separate lines + original_formatargvalues = inspect.formatargvalues + inspect.formatargvalues = _custom_formatargvalues + try: + # Use IPython traceback formatting to build the traceback string. + ostream = io.StringIO() + ipytb = FormattedTB( + tb_offset=skip, + mode="Verbose", + color_scheme="NoColor", + include_vars=True, + ostream=ostream, + ) + ipytb() + finally: + # Restore formatargvalues + inspect.formatargvalues = original_formatargvalues + + stacktrace = ostream.getvalue() + + if self.inline_exceptions: + print(stacktrace) + + return stacktrace, 500 + + @property + def active(self): + _inside_dbx = "DATABRICKS_RUNTIME_VERSION" in os.environ + return _dep_installed and not _inside_dbx and (self.in_ipython or self.in_colab) + + +jupyter_dash = JupyterDash() |