diff options
Diffstat (limited to 'venv/lib/python3.8/site-packages/dash/dash.py')
-rw-r--r-- | venv/lib/python3.8/site-packages/dash/dash.py | 2533 |
1 files changed, 2533 insertions, 0 deletions
diff --git a/venv/lib/python3.8/site-packages/dash/dash.py b/venv/lib/python3.8/site-packages/dash/dash.py new file mode 100644 index 0000000..cf6d0f4 --- /dev/null +++ b/venv/lib/python3.8/site-packages/dash/dash.py @@ -0,0 +1,2533 @@ +import functools +import os +import sys +import collections +import importlib +import warnings +from contextvars import copy_context +from importlib.machinery import ModuleSpec +from importlib.util import find_spec +from importlib import metadata +import pkgutil +import threading +import re +import logging +import time +import mimetypes +import hashlib +import base64 +import traceback +from urllib.parse import urlparse +from typing import Any, Callable, Dict, Optional, Union, Sequence, Literal, List + +import asyncio +import flask + +from importlib_metadata import version as _get_distribution_version + +from dash import dcc +from dash import html +from dash import dash_table + +from .fingerprint import build_fingerprint, check_fingerprint +from .resources import Scripts, Css +from .dependencies import ( + Input, + Output, + State, +) +from .development.base_component import ComponentRegistry +from .exceptions import ( + PreventUpdate, + InvalidResourceError, + ProxyError, + DuplicateCallback, +) +from .version import __version__ +from ._configs import get_combined_config, pathname_configs, pages_folder_config +from ._utils import ( + AttributeDict, + format_tag, + generate_hash, + inputs_to_dict, + inputs_to_vals, + interpolate_str, + patch_collections_abc, + split_callback_id, + to_json, + convert_to_AttributeDict, + gen_salt, + hooks_to_js_object, + parse_version, + get_caller_name, +) +from . import _callback +from . import _get_paths +from . import _dash_renderer +from . import _validate +from . import _watch +from . import _get_app + +from ._grouping import map_grouping, grouping_len, update_args_group +from ._obsolete import ObsoleteChecker + +from . import _pages +from ._pages import ( + _parse_query_string, + _page_meta_tags, + _path_to_page, + _import_layouts_from_pages, +) +from ._jupyter import jupyter_dash, JupyterDisplayMode +from .types import RendererHooks + +RouteCallable = Callable[..., Any] + +# If dash_design_kit is installed, check for version +ddk_version = None +if find_spec("dash_design_kit"): + ddk_version = metadata.version("dash_design_kit") + +plotly_version = None +if find_spec("plotly"): + plotly_version = metadata.version("plotly") + +# Add explicit mapping for map files +mimetypes.add_type("application/json", ".map", True) + +_default_index = """<!DOCTYPE html> +<html> + <head> + {%metas%} + <title>{%title%}</title> + {%favicon%} + {%css%} + </head> + <body> + <!--[if IE]><script> + alert("Dash v2.7+ does not support Internet Explorer. Please use a newer browser."); + </script><![endif]--> + {%app_entry%} + <footer> + {%config%} + {%scripts%} + {%renderer%} + </footer> + </body> +</html>""" + +_app_entry = """ +<div id="react-entry-point"> + <div class="_dash-loading"> + Loading... + </div> +</div> +""" + +_re_index_entry = "{%app_entry%}", "{%app_entry%}" +_re_index_config = "{%config%}", "{%config%}" +_re_index_scripts = "{%scripts%}", "{%scripts%}" + +_re_index_entry_id = 'id="react-entry-point"', "#react-entry-point" +_re_index_config_id = 'id="_dash-config"', "#_dash-config" +_re_index_scripts_id = 'src="[^"]*dash[-_]renderer[^"]*"', "dash-renderer" +_re_renderer_scripts_id = 'id="_dash-renderer', "new DashRenderer" + + +_ID_CONTENT = "_pages_content" +_ID_LOCATION = "_pages_location" +_ID_STORE = "_pages_store" +_ID_DUMMY = "_pages_dummy" + +DASH_VERSION_URL = "https://dash-version.plotly.com:8080/current_version" + +# Handles the case in a newly cloned environment where the components are not yet generated. +try: + page_container = html.Div( + [ + dcc.Location(id=_ID_LOCATION, refresh="callback-nav"), + html.Div(id=_ID_CONTENT, disable_n_clicks=True), + dcc.Store(id=_ID_STORE), + html.Div(id=_ID_DUMMY, disable_n_clicks=True), + ] + ) +# pylint: disable-next=bare-except +except: # noqa: E722 + page_container = None + + +def _get_traceback(secret, error: Exception): + try: + # pylint: disable=import-outside-toplevel + from werkzeug.debug import tbtools + except ImportError: + tbtools = None + + def _get_skip(error): + from dash._callback import ( # pylint: disable=import-outside-toplevel + _invoke_callback, + _async_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 in [ + _invoke_callback.__code__, + _async_invoke_callback.__code__, + ]: + return skip + + return skip + + def _do_skip(error): + from dash._callback import ( # pylint: disable=import-outside-toplevel + _invoke_callback, + _async_invoke_callback, + ) + + tb = error.__traceback__ + while tb.tb_next is not None: + if tb.tb_frame.f_code in [ + _invoke_callback.__code__, + _async_invoke_callback.__code__, + ]: + return tb.tb_next + tb = tb.tb_next + return error.__traceback__ + + # werkzeug<2.1.0 + if hasattr(tbtools, "get_current_traceback"): + return tbtools.get_current_traceback( # type: ignore + skip=_get_skip(error) + ).render_full() + + if hasattr(tbtools, "DebugTraceback"): + # pylint: disable=no-member + return tbtools.DebugTraceback( # type: ignore + error, skip=_get_skip(error) + ).render_debugger_html(True, secret, True) + + return "".join(traceback.format_exception(type(error), error, _do_skip(error))) + + +# Singleton signal to not update an output, alternative to PreventUpdate +no_update = _callback.NoUpdate() # pylint: disable=protected-access + + +async def execute_async_function(func, *args, **kwargs): + # Check if the function is a coroutine function + if asyncio.iscoroutinefunction(func): + return await func(*args, **kwargs) + # If the function is not a coroutine, call it directly + return func(*args, **kwargs) + + +# pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-arguments, too-many-locals +class Dash(ObsoleteChecker): + """Dash is a framework for building analytical web applications. + No JavaScript required. + + If a parameter can be set by an environment variable, that is listed as: + env: ``DASH_****`` + Values provided here take precedence over environment variables. + + :param name: The name Flask should use for your app. Even if you provide + your own ``server``, ``name`` will be used to help find assets. + Typically ``__name__`` (the magic global var, not a string) is the + best value to use. Default ``'__main__'``, env: ``DASH_APP_NAME`` + :type name: string + + :param server: Sets the Flask server for your app. There are three options: + ``True`` (default): Dash will create a new server + ``False``: The server will be added later via ``app.init_app(server)`` + where ``server`` is a ``flask.Flask`` instance. + ``flask.Flask``: use this pre-existing Flask server. + :type server: boolean or flask.Flask + + :param assets_folder: a path, relative to the current working directory, + for extra files to be used in the browser. Default ``'assets'``. + All .js and .css files will be loaded immediately unless excluded by + ``assets_ignore``, and other files such as images will be served if + requested. + :type assets_folder: string + + :param pages_folder: a relative or absolute path for pages of a multi-page app. + Default ``'pages'``. + :type pages_folder: string or pathlib.Path + + :param use_pages: When True, the ``pages`` feature for multi-page apps is + enabled. If you set a non-default ``pages_folder`` this will be inferred + to be True. Default `None`. + :type use_pages: boolean + + :param include_pages_meta: Include the page meta tags for twitter cards. + :type include_pages_meta: bool + + :param assets_url_path: The local urls for assets will be: + ``requests_pathname_prefix + assets_url_path + '/' + asset_path`` + where ``asset_path`` is the path to a file inside ``assets_folder``. + Default ``'assets'``. + :type asset_url_path: string + + :param assets_ignore: A regex, as a string to pass to ``re.compile``, for + assets to omit from immediate loading. Ignored files will still be + served if specifically requested. You cannot use this to prevent access + to sensitive files. + :type assets_ignore: string + + :param assets_path_ignore: A list of regex, each regex as a string to pass to ``re.compile``, for + assets path to omit from immediate loading. The files in these ignored paths will still be + served if specifically requested. You cannot use this to prevent access + to sensitive files. + :type assets_path_ignore: list of strings + + :param assets_external_path: an absolute URL from which to load assets. + Use with ``serve_locally=False``. assets_external_path is joined + with assets_url_path to determine the absolute url to the + asset folder. Dash can still find js and css to automatically load + if you also keep local copies in your assets folder that Dash can index, + but external serving can improve performance and reduce load on + the Dash server. + env: ``DASH_ASSETS_EXTERNAL_PATH`` + :type assets_external_path: string + + :param include_assets_files: Default ``True``, set to ``False`` to prevent + immediate loading of any assets. Assets will still be served if + specifically requested. You cannot use this to prevent access + to sensitive files. env: ``DASH_INCLUDE_ASSETS_FILES`` + :type include_assets_files: boolean + + :param url_base_pathname: A local URL prefix to use app-wide. + Default ``'/'``. Both `requests_pathname_prefix` and + `routes_pathname_prefix` default to `url_base_pathname`. + env: ``DASH_URL_BASE_PATHNAME`` + :type url_base_pathname: string + + :param requests_pathname_prefix: A local URL prefix for file requests. + Defaults to `url_base_pathname`, and must end with + `routes_pathname_prefix`. env: ``DASH_REQUESTS_PATHNAME_PREFIX`` + :type requests_pathname_prefix: string + + :param routes_pathname_prefix: A local URL prefix for JSON requests. + Defaults to ``url_base_pathname``, and must start and end + with ``'/'``. env: ``DASH_ROUTES_PATHNAME_PREFIX`` + :type routes_pathname_prefix: string + + :param serve_locally: If ``True`` (default), assets and dependencies + (Dash and Component js and css) will be served from local URLs. + If ``False`` we will use CDN links where available. + :type serve_locally: boolean + + :param compress: Use gzip to compress files and data served by Flask. + To use this option, you need to install dash[compress] + Default ``False`` + :type compress: boolean + + :param meta_tags: html <meta> tags to be added to the index page. + Each dict should have the attributes and values for one tag, eg: + ``{'name': 'description', 'content': 'My App'}`` + :type meta_tags: list of dicts + + :param index_string: Override the standard Dash index page. + Must contain the correct insertion markers to interpolate various + content into it depending on the app config and components used. + See https://dash.plotly.com/external-resources for details. + :type index_string: string + + :param external_scripts: Additional JS files to load with the page. + Each entry can be a string (the URL) or a dict with ``src`` (the URL) + and optionally other ``<script>`` tag attributes such as ``integrity`` + and ``crossorigin``. + :type external_scripts: list of strings or dicts + + :param external_stylesheets: Additional CSS files to load with the page. + Each entry can be a string (the URL) or a dict with ``href`` (the URL) + and optionally other ``<link>`` tag attributes such as ``rel``, + ``integrity`` and ``crossorigin``. + :type external_stylesheets: list of strings or dicts + + :param suppress_callback_exceptions: Default ``False``: check callbacks to + ensure referenced IDs exist and props are valid. Set to ``True`` + if your layout is dynamic, to bypass these checks. + env: ``DASH_SUPPRESS_CALLBACK_EXCEPTIONS`` + :type suppress_callback_exceptions: boolean + + :param prevent_initial_callbacks: Default ``False``: Sets the default value + of ``prevent_initial_call`` for all callbacks added to the app. + Normally all callbacks are fired when the associated outputs are first + added to the page. You can disable this for individual callbacks by + setting ``prevent_initial_call`` in their definitions, or set it + ``True`` here in which case you must explicitly set it ``False`` for + those callbacks you wish to have an initial call. This setting has no + effect on triggering callbacks when their inputs change later on. + + :param show_undo_redo: Default ``False``, set to ``True`` to enable undo + and redo buttons for stepping through the history of the app state. + :type show_undo_redo: boolean + + :param extra_hot_reload_paths: A list of paths to watch for changes, in + addition to assets and known Python and JS code, if hot reloading is + enabled. + :type extra_hot_reload_paths: list of strings + + :param plugins: Extend Dash functionality by passing a list of objects + with a ``plug`` method, taking a single argument: this app, which will + be called after the Flask server is attached. + :type plugins: list of objects + + :param title: Default ``Dash``. Configures the document.title + (the text that appears in a browser tab). + + :param update_title: Default ``Updating...``. Configures the document.title + (the text that appears in a browser tab) text when a callback is being run. + Set to None or '' if you don't want the document.title to change or if you + want to control the document.title through a separate component or + clientside callback. + + :param background_callback_manager: Background callback manager instance + to support the ``@callback(..., background=True)`` decorator. + One of ``DiskcacheManager`` or ``CeleryManager`` currently supported. + + :param add_log_handler: Automatically add a StreamHandler to the app logger + if not added previously. + + :param hooks: Extend Dash renderer functionality by passing a dictionary of + javascript functions. To hook into the layout, use dict keys "layout_pre" and + "layout_post". To hook into the callbacks, use keys "request_pre" and "request_post" + + :param routing_callback_inputs: When using Dash pages (use_pages=True), allows to + add new States to the routing callback, to pass additional data to the layout + functions. The syntax for this parameter is a dict of State objects: + `routing_callback_inputs={"language": Input("language", "value")}` + NOTE: the keys "pathname_" and "search_" are reserved for internal use. + + :param description: Sets a default description for meta tags on Dash pages (use_pages=True). + + :param on_error: Global callback error handler to call when + an exception is raised. Receives the exception object as first argument. + The callback_context can be used to access the original callback inputs, + states and output. + + :param use_async: When True, the app will create async endpoints, as a dev, + they will be responsible for installing the `flask[async]` dependency. + :type use_async: boolean + """ + + _plotlyjs_url: str + STARTUP_ROUTES: list = [] + + server: flask.Flask + + # Layout is a complex type which can be many things + _layout: Any + _extra_components: Any + + def __init__( # pylint: disable=too-many-statements + self, + name: Optional[str] = None, + server: Union[bool, flask.Flask] = True, + assets_folder: str = "assets", + pages_folder: str = "pages", + use_pages: Optional[bool] = None, + assets_url_path: str = "assets", + assets_ignore: str = "", + assets_path_ignore: List[str] = None, + assets_external_path: Optional[str] = None, + eager_loading: bool = False, + include_assets_files: bool = True, + include_pages_meta: bool = True, + url_base_pathname: Optional[str] = None, + requests_pathname_prefix: Optional[str] = None, + routes_pathname_prefix: Optional[str] = None, + serve_locally: bool = True, + compress: Optional[bool] = None, + meta_tags: Optional[Sequence[Dict[str, Any]]] = None, + index_string: str = _default_index, + external_scripts: Optional[Sequence[Union[str, Dict[str, Any]]]] = None, + external_stylesheets: Optional[Sequence[Union[str, Dict[str, Any]]]] = None, + suppress_callback_exceptions: Optional[bool] = None, + prevent_initial_callbacks: bool = False, + show_undo_redo: bool = False, + extra_hot_reload_paths: Optional[Sequence[str]] = None, + plugins: Optional[list] = None, + title: str = "Dash", + update_title: str = "Updating...", + background_callback_manager: Optional[ + Any + ] = None, # Type should be specified if possible + add_log_handler: bool = True, + hooks: Optional[RendererHooks] = None, + routing_callback_inputs: Optional[Dict[str, Union[Input, State]]] = None, + description: Optional[str] = None, + on_error: Optional[Callable[[Exception], Any]] = None, + use_async: Optional[bool] = None, + **obsolete, + ): + + if use_async is None: + try: + import asgiref # pylint: disable=unused-import, import-outside-toplevel # noqa + + use_async = True + except ImportError: + pass + elif use_async: + try: + import asgiref # pylint: disable=unused-import, import-outside-toplevel # noqa + except ImportError as exc: + raise Exception( + "You are trying to use dash[async] without having installed the requirements please install via: `pip install dash[async]`" + ) from exc + + _validate.check_obsolete(obsolete) + + caller_name: str = name if name is not None else get_caller_name() + + # We have 3 cases: server is either True (we create the server), False + # (defer server creation) or a Flask app instance (we use their server) + if isinstance(server, flask.Flask): + self.server = server + if name is None: + caller_name = getattr(server, "name", caller_name) + elif isinstance(server, bool): + self.server = flask.Flask(caller_name) if server else None # type: ignore + else: + raise ValueError("server must be a Flask app or a boolean") + + base_prefix, routes_prefix, requests_prefix = pathname_configs( + url_base_pathname, routes_pathname_prefix, requests_pathname_prefix + ) + + self.config = AttributeDict( + name=caller_name, + assets_folder=os.path.join( + flask.helpers.get_root_path(caller_name), assets_folder + ), # type: ignore + assets_url_path=assets_url_path, + assets_ignore=assets_ignore, + assets_path_ignore=assets_path_ignore, + assets_external_path=get_combined_config( + "assets_external_path", assets_external_path, "" + ), + pages_folder=pages_folder_config(caller_name, pages_folder, use_pages), + eager_loading=eager_loading, + include_assets_files=get_combined_config( + "include_assets_files", include_assets_files, True + ), + url_base_pathname=base_prefix, + routes_pathname_prefix=routes_prefix, + requests_pathname_prefix=requests_prefix, + serve_locally=serve_locally, + compress=get_combined_config("compress", compress, False), + meta_tags=meta_tags or [], + external_scripts=external_scripts or [], + external_stylesheets=external_stylesheets or [], + suppress_callback_exceptions=get_combined_config( + "suppress_callback_exceptions", suppress_callback_exceptions, False + ), + prevent_initial_callbacks=prevent_initial_callbacks, + show_undo_redo=show_undo_redo, + extra_hot_reload_paths=extra_hot_reload_paths or [], + title=title, + update_title=update_title, + include_pages_meta=include_pages_meta, + description=description, + ) + self.config.set_read_only( + [ + "name", + "assets_folder", + "assets_url_path", + "eager_loading", + "serve_locally", + "compress", + "pages_folder", + ], + "Read-only: can only be set in the Dash constructor", + ) + self.config.finalize( + "Invalid config key. Some settings are only available " + "via the Dash constructor" + ) + + _get_paths.CONFIG = self.config + _pages.CONFIG = self.config + + self.pages_folder = str(pages_folder) + self.use_pages = (pages_folder != "pages") if use_pages is None else use_pages + self.routing_callback_inputs = routing_callback_inputs or {} + + # keep title as a class property for backwards compatibility + self.title = title + + # list of dependencies - this one is used by the back end for dispatching + self.callback_map = {} + # same deps as a list to catch duplicate outputs, and to send to the front end + self._callback_list = [] + + # list of inline scripts + self._inline_scripts = [] + + # index_string has special setter so can't go in config + self._index_string = "" + self.index_string = index_string + self._favicon = None + + # default renderer string + self.renderer = f"var renderer = new DashRenderer({hooks_to_js_object(hooks)});" + + # static files from the packages + self.css = Css(serve_locally) + self.scripts = Scripts(serve_locally, eager_loading) + + self.registered_paths = collections.defaultdict(set) + + # urls + self.routes = [] + + self._layout = None + self._layout_is_function = False + self.validation_layout = None + self._on_error = on_error + self._extra_components = [] + self._use_async = use_async + + self._setup_dev_tools() + self._hot_reload = AttributeDict( + hash=None, + hard=False, + lock=threading.RLock(), + watch_thread=None, + changed_assets=[], + ) + + self._assets_files = [] + + self._background_manager = background_callback_manager + + self.logger = logging.getLogger(__name__) + + if not self.logger.handlers and add_log_handler: + self.logger.addHandler(logging.StreamHandler(stream=sys.stdout)) + + if plugins is not None and isinstance( + plugins, patch_collections_abc("Iterable") + ): + for plugin in plugins: + plugin.plug(self) + + self._setup_hooks() + + # tracks internally if a function already handled at least one request. + self._got_first_request = {"pages": False, "setup_server": False} + + if self.server is not None: + self.init_app() + + self.logger.setLevel(logging.INFO) + + if self.__class__.__name__ == "JupyterDash": + warnings.warn( + "JupyterDash is deprecated, use Dash instead.\n" + "See https://dash.plotly.com/dash-in-jupyter for more details." + ) + self.setup_startup_routes() + + def _setup_hooks(self): + # pylint: disable=import-outside-toplevel,protected-access + from ._hooks import HooksManager + + self._hooks = HooksManager + self._hooks.register_setuptools() + + for setup in self._hooks.get_hooks("setup"): + setup(self) + + for hook in self._hooks.get_hooks("callback"): + callback_args, callback_kwargs = hook.data + self.callback(*callback_args, **callback_kwargs)(hook.func) + + for ( + clientside_function, + args, + kwargs, + ) in self._hooks.hooks._clientside_callbacks: + _callback.register_clientside_callback( + self._callback_list, + self.callback_map, + self.config.prevent_initial_callbacks, + self._inline_scripts, + clientside_function, + *args, + **kwargs, + ) + + if self._hooks.get_hooks("error"): + self._on_error = self._hooks.HookErrorHandler(self._on_error) + + def init_app(self, app: Optional[flask.Flask] = None, **kwargs) -> None: + """Initialize the parts of Dash that require a flask app.""" + + config = self.config + + config.update(kwargs) + config.set_read_only( + [ + "url_base_pathname", + "routes_pathname_prefix", + "requests_pathname_prefix", + ], + "Read-only: can only be set in the Dash constructor or during init_app()", + ) + + if app is not None: + self.server = app + + bp_prefix = config.routes_pathname_prefix.replace("/", "_").replace(".", "_") + assets_blueprint_name = f"{bp_prefix}dash_assets" + + self.server.register_blueprint( + flask.Blueprint( + assets_blueprint_name, + config.name, + static_folder=self.config.assets_folder, + static_url_path=config.routes_pathname_prefix + + self.config.assets_url_path.lstrip("/"), + ) + ) + + if config.compress: + try: + # pylint: disable=import-outside-toplevel + from flask_compress import Compress # type: ignore[reportMissingImports] + + # gzip + Compress(self.server) + + _flask_compress_version = parse_version( + _get_distribution_version("flask_compress") + ) + + if not hasattr( + self.server.config, "COMPRESS_ALGORITHM" + ) and _flask_compress_version >= parse_version("1.6.0"): + # flask-compress==1.6.0 changed default to ['br', 'gzip'] + # and non-overridable default compression with Brotli is + # causing performance issues + self.server.config["COMPRESS_ALGORITHM"] = ["gzip"] + except ImportError as error: + raise ImportError( + "To use the compress option, you need to install dash[compress]" + ) from error + + @self.server.errorhandler(PreventUpdate) + def _handle_error(_): + """Handle a halted callback and return an empty 204 response.""" + return "", 204 + + self.server.before_request(self._setup_server) + + # add a handler for components suites errors to return 404 + self.server.errorhandler(InvalidResourceError)(self._invalid_resources_handler) + + self._setup_routes() + + _get_app.APP = self + self.enable_pages() + + self._setup_plotlyjs() + + def _add_url(self, name: str, view_func: RouteCallable, methods=("GET",)) -> None: + full_name = self.config.routes_pathname_prefix + name + + self.server.add_url_rule( + full_name, view_func=view_func, endpoint=full_name, methods=list(methods) + ) + + # record the url in Dash.routes so that it can be accessed later + # e.g. for adding authentication with flask_login + self.routes.append(full_name) + + def _setup_routes(self): + self._add_url( + "_dash-component-suites/<string:package_name>/<path:fingerprinted_path>", + self.serve_component_suites, + ) + self._add_url("_dash-layout", self.serve_layout) + self._add_url("_dash-dependencies", self.dependencies) + if self._use_async: + self._add_url("_dash-update-component", self.async_dispatch, ["POST"]) + else: + self._add_url("_dash-update-component", self.dispatch, ["POST"]) + self._add_url("_reload-hash", self.serve_reload_hash) + self._add_url("_favicon.ico", self._serve_default_favicon) + self._add_url("", self.index) + + if jupyter_dash.active: + self._add_url( + "_alive_" + jupyter_dash.alive_token, jupyter_dash.serve_alive + ) + + for hook in self._hooks.get_hooks("routes"): + self._add_url(hook.data["name"], hook.func, hook.data["methods"]) + + # catch-all for front-end routes, used by dcc.Location + self._add_url("<path:path>", self.index) + + def _setup_plotlyjs(self): + # pylint: disable=import-outside-toplevel + from plotly.offline import get_plotlyjs_version + + url = f"https://cdn.plot.ly/plotly-{get_plotlyjs_version()}.min.js" + + # pylint: disable=protected-access + dcc._js_dist.extend( + [ + { + "relative_package_path": "package_data/plotly.min.js", + "external_url": url, + "namespace": "plotly", + "async": "eager", + } + ] + ) + self._plotlyjs_url = url + + @property + def layout(self) -> Any: + return self._layout + + @layout.setter + def layout(self, value: Any): + _validate.validate_layout_type(value) + self._layout_is_function = callable(value) + self._layout = value + + # for using flask.has_request_context() to deliver a full layout for + # validation inside a layout function - track if a user might be doing this. + if ( + self._layout_is_function + and not self.validation_layout + and not self.config.suppress_callback_exceptions + ): + layout_value = self._layout_value() + _validate.validate_layout(value, layout_value) + self.validation_layout = layout_value + + def _layout_value(self): + if self._layout_is_function: + layout = self._layout() # type: ignore[reportOptionalCall] + else: + layout = self._layout + + # Add any extra components + if self._extra_components: + layout = html.Div(children=[layout] + self._extra_components) # type: ignore[reportArgumentType] + + return layout + + @property + def index_string(self) -> str: + return self._index_string + + @index_string.setter + def index_string(self, value: str) -> None: + checks = (_re_index_entry, _re_index_config, _re_index_scripts) + _validate.validate_index("index string", checks, value) + self._index_string = value + + def serve_layout(self): + layout = self._layout_value() + + for hook in self._hooks.get_hooks("layout"): + layout = hook(layout) + + # TODO - Set browser cache limit - pass hash into frontend + return flask.Response( + to_json(layout), + mimetype="application/json", + ) + + def _config(self): + # pieces of config needed by the front end + config = { + "url_base_pathname": self.config.url_base_pathname, + "requests_pathname_prefix": self.config.requests_pathname_prefix, + "ui": self._dev_tools.ui, + "props_check": self._dev_tools.props_check, + "disable_version_check": self._dev_tools.disable_version_check, + "show_undo_redo": self.config.show_undo_redo, + "suppress_callback_exceptions": self.config.suppress_callback_exceptions, + "update_title": self.config.update_title, + "children_props": ComponentRegistry.children_props, + "serve_locally": self.config.serve_locally, + "dash_version": __version__, + "python_version": sys.version, + "dash_version_url": DASH_VERSION_URL, + "ddk_version": ddk_version, + "plotly_version": plotly_version, + } + if not self.config.serve_locally: + config["plotlyjs_url"] = self._plotlyjs_url + if self._dev_tools.hot_reload: + config["hot_reload"] = { + # convert from seconds to msec as used by js `setInterval` + "interval": int(self._dev_tools.hot_reload_interval * 1000), + "max_retry": self._dev_tools.hot_reload_max_retry, + } + if self.validation_layout and not self.config.suppress_callback_exceptions: + validation_layout = self.validation_layout + + # Add extra components + if self._extra_components: + validation_layout = html.Div( + children=[validation_layout] + self._extra_components + ) + + config["validation_layout"] = validation_layout + + return config + + def serve_reload_hash(self): + _reload = self._hot_reload + with _reload.lock: + hard = _reload.hard + changed = _reload.changed_assets + _hash = _reload.hash + _reload.hard = False + _reload.changed_assets = [] + + return flask.jsonify( + { + "reloadHash": _hash, + "hard": hard, + "packages": list(self.registered_paths.keys()), + "files": list(changed), + } + ) + + def get_dist(self, libraries: Sequence[str]) -> list: + dists = [] + for dist_type in ("_js_dist", "_css_dist"): + resources = ComponentRegistry.get_resources(dist_type, libraries) + srcs = self._collect_and_register_resources(resources, False) + for src in srcs: + dists.append(dict(type=dist_type, url=src)) + return dists + + def _collect_and_register_resources(self, resources, include_async=True): + # now needs the app context. + # template in the necessary component suite JS bundles + # add the version number of the package as a query parameter + # for cache busting + def _relative_url_path(relative_package_path="", namespace=""): + if any( + relative_package_path.startswith(x + "/") + for x in ["dcc", "html", "dash_table"] + ): + relative_package_path = relative_package_path.replace("dash.", "") + version = importlib.import_module( + f"{namespace}.{os.path.split(relative_package_path)[0]}" + ).__version__ + else: + version = importlib.import_module(namespace).__version__ + + module_path = os.path.join( # type: ignore[reportCallIssue] + os.path.dirname(sys.modules[namespace].__file__), # type: ignore[reportCallIssue] + relative_package_path, + ) + + modified = int(os.stat(module_path).st_mtime) + + fingerprint = build_fingerprint(relative_package_path, version, modified) + return f"{self.config.requests_pathname_prefix}_dash-component-suites/{namespace}/{fingerprint}" + + srcs = [] + for resource in resources: + is_dynamic_resource = resource.get("dynamic", False) + is_async = resource.get("async") is not None + excluded = not include_async and is_async + + if "relative_package_path" in resource: + paths = resource["relative_package_path"] + paths = [paths] if isinstance(paths, str) else paths + + for rel_path in paths: + if any(x in rel_path for x in ["dcc", "html", "dash_table"]): + rel_path = rel_path.replace("dash.", "") + + self.registered_paths[resource["namespace"]].add(rel_path) + + if not is_dynamic_resource and not excluded: + srcs.append( + _relative_url_path( + relative_package_path=rel_path, + namespace=resource["namespace"], + ) + ) + elif "external_url" in resource: + if not is_dynamic_resource and not excluded: + if isinstance(resource["external_url"], str): + srcs.append(resource["external_url"]) + else: + srcs += resource["external_url"] + elif "absolute_path" in resource: + raise Exception("Serving files from absolute_path isn't supported yet") + elif "asset_path" in resource: + static_url = self.get_asset_url(resource["asset_path"]) + # Import .mjs files with type=module script tag + if static_url.endswith(".mjs"): + srcs.append( + { + "src": static_url + + f"?m={resource['ts']}", # Add a cache-busting query param + "type": "module", + } + ) + else: + srcs.append( + static_url + f"?m={resource['ts']}" + ) # Add a cache-busting query param + + return srcs + + # pylint: disable=protected-access + def _generate_css_dist_html(self): + external_links = self.config.external_stylesheets + links = self._collect_and_register_resources( + self.css.get_all_css() + + self.css._resources._filter_resources(self._hooks.hooks._css_dist) + ) + + return "\n".join( + [ + format_tag("link", link, opened=True) + if isinstance(link, dict) + else f'<link rel="stylesheet" href="{link}">' + for link in (external_links + links) + ] + ) + + def _generate_scripts_html(self) -> str: + # Dash renderer has dependencies like React which need to be rendered + # before every other script. However, the dash renderer bundle + # itself needs to be rendered after all of the component's + # scripts have rendered. + # The rest of the scripts can just be loaded after React but before + # dash renderer. + # pylint: disable=protected-access + + mode = "dev" if self._dev_tools["props_check"] is True else "prod" + + deps = [ + { + key: value[mode] if isinstance(value, dict) else value + for key, value in js_dist_dependency.items() + } + for js_dist_dependency in _dash_renderer._js_dist_dependencies + ] + dev = self._dev_tools.serve_dev_bundles + srcs = ( + self._collect_and_register_resources( + self.scripts._resources._filter_resources(deps, dev_bundles=dev) # type: ignore[reportArgumentType] + ) + + self.config.external_scripts + + self._collect_and_register_resources( + self.scripts.get_all_scripts(dev_bundles=dev) + + self.scripts._resources._filter_resources( + _dash_renderer._js_dist, dev_bundles=dev + ) + + self.scripts._resources._filter_resources( + dcc._js_dist, dev_bundles=dev + ) + + self.scripts._resources._filter_resources( + html._js_dist, dev_bundles=dev + ) + + self.scripts._resources._filter_resources( + dash_table._js_dist, dev_bundles=dev + ) + + self.scripts._resources._filter_resources( + self._hooks.hooks._js_dist, dev_bundles=dev + ) + ) + ) + + self._inline_scripts.extend(_callback.GLOBAL_INLINE_SCRIPTS) + _callback.GLOBAL_INLINE_SCRIPTS.clear() + + return "\n".join( + [ + format_tag("script", src) + if isinstance(src, dict) + else f'<script src="{src}"></script>' + for src in srcs + ] + + [f"<script>{src}</script>" for src in self._inline_scripts] + ) + + def _generate_config_html(self) -> str: + return f'<script id="_dash-config" type="application/json">{to_json(self._config())}</script>' + + def _generate_renderer(self) -> str: + return f'<script id="_dash-renderer" type="application/javascript">{self.renderer}</script>' + + def _generate_meta(self): + meta_tags = [] + has_ie_compat = any( + x.get("http-equiv", "") == "X-UA-Compatible" for x in self.config.meta_tags + ) + has_charset = any("charset" in x for x in self.config.meta_tags) + has_viewport = any(x.get("name") == "viewport" for x in self.config.meta_tags) + + if not has_ie_compat: + meta_tags.append({"http-equiv": "X-UA-Compatible", "content": "IE=edge"}) + if not has_charset: + meta_tags.append({"charset": "UTF-8"}) + if not has_viewport: + meta_tags.append( + {"name": "viewport", "content": "width=device-width, initial-scale=1"} + ) + + return meta_tags + self.config.meta_tags + + # Serve the JS bundles for each package + def serve_component_suites(self, package_name, fingerprinted_path): + path_in_pkg, has_fingerprint = check_fingerprint(fingerprinted_path) + + _validate.validate_js_path(self.registered_paths, package_name, path_in_pkg) + + extension = "." + path_in_pkg.split(".")[-1] + mimetype = mimetypes.types_map.get(extension, "application/octet-stream") + + package = sys.modules[package_name] + self.logger.debug( + "serving -- package: %s[%s] resource: %s => location: %s", + package_name, + package.__version__, + path_in_pkg, + package.__path__, + ) + + response = flask.Response( + pkgutil.get_data(package_name, path_in_pkg), mimetype=mimetype + ) + + if has_fingerprint: + # Fingerprinted resources are good forever (1 year) + # No need for ETag as the fingerprint changes with each build + response.cache_control.max_age = 31536000 # 1 year + else: + # Non-fingerprinted resources are given an ETag that + # will be used / check on future requests + response.add_etag() + tag = response.get_etag()[0] + + request_etag = flask.request.headers.get("If-None-Match") + + if f'"{tag}"' == request_etag: + response = flask.Response(None, status=304) + + return response + + def index(self, *args, **kwargs): # pylint: disable=unused-argument + scripts = self._generate_scripts_html() + css = self._generate_css_dist_html() + config = self._generate_config_html() + metas = self._generate_meta() + renderer = self._generate_renderer() + + # use self.title instead of app.config.title for backwards compatibility + title = self.title + + if self.use_pages and self.config.include_pages_meta: + metas = _page_meta_tags(self) + metas + + if self._favicon: + favicon_mod_time = os.path.getmtime( + os.path.join(self.config.assets_folder, self._favicon) + ) + favicon_url = f"{self.get_asset_url(self._favicon)}?m={favicon_mod_time}" + else: + prefix = self.config.requests_pathname_prefix + favicon_url = f"{prefix}_favicon.ico?v={__version__}" + + favicon = format_tag( + "link", + {"rel": "icon", "type": "image/x-icon", "href": favicon_url}, + opened=True, + ) + + tags = "\n ".join( + format_tag("meta", x, opened=True, sanitize=True) for x in metas + ) + + index = self.interpolate_index( + metas=tags, + title=title, + css=css, + config=config, + scripts=scripts, + app_entry=_app_entry, + favicon=favicon, + renderer=renderer, + ) + + for hook in self._hooks.get_hooks("index"): + index = hook(index) + + checks = ( + _re_index_entry_id, + _re_index_config_id, + _re_index_scripts_id, + _re_renderer_scripts_id, + ) + _validate.validate_index("index", checks, index) + return index + + def interpolate_index( + self, + metas="", + title="", + css="", + config="", + scripts="", + app_entry="", + favicon="", + renderer="", + ): + """Called to create the initial HTML string that is loaded on page. + Override this method to provide you own custom HTML. + + :Example: + + class MyDash(dash.Dash): + def interpolate_index(self, **kwargs): + return '''<!DOCTYPE html> + <html> + <head> + <title>My App</title> + </head> + <body> + <div id="custom-header">My custom header</div> + {app_entry} + {config} + {scripts} + {renderer} + <div id="custom-footer">My custom footer</div> + </body> + </html>'''.format(app_entry=kwargs.get('app_entry'), + config=kwargs.get('config'), + scripts=kwargs.get('scripts'), + renderer=kwargs.get('renderer')) + + :param metas: Collected & formatted meta tags. + :param title: The title of the app. + :param css: Collected & formatted css dependencies as <link> tags. + :param config: Configs needed by dash-renderer. + :param scripts: Collected & formatted scripts tags. + :param renderer: A script tag that instantiates the DashRenderer. + :param app_entry: Where the app will render. + :param favicon: A favicon <link> tag if found in assets folder. + :return: The interpolated HTML string for the index. + """ + return interpolate_str( + self.index_string, + metas=metas, + title=title, + css=css, + config=config, + scripts=scripts, + favicon=favicon, + renderer=renderer, + app_entry=app_entry, + ) + + def dependencies(self): + return flask.Response( + to_json(self._callback_list), + content_type="application/json", + ) + + def clientside_callback(self, clientside_function, *args, **kwargs): + """Create a callback that updates the output by calling a clientside + (JavaScript) function instead of a Python function. + + Unlike `@app.callback`, `clientside_callback` is not a decorator: + it takes either a + `dash.dependencies.ClientsideFunction(namespace, function_name)` + argument that describes which JavaScript function to call + (Dash will look for the JavaScript function at + `window.dash_clientside[namespace][function_name]`), or it may take + a string argument that contains the clientside function source. + + For example, when using a `dash.dependencies.ClientsideFunction`: + ``` + app.clientside_callback( + ClientsideFunction('my_clientside_library', 'my_function'), + Output('my-div' 'children'), + [Input('my-input', 'value'), + Input('another-input', 'value')] + ) + ``` + + With this signature, Dash's front-end will call + `window.dash_clientside.my_clientside_library.my_function` with the + current values of the `value` properties of the components `my-input` + and `another-input` whenever those values change. + + Include a JavaScript file by including it your `assets/` folder. The + file can be named anything but you'll need to assign the function's + namespace to the `window.dash_clientside` namespace. For example, + this file might look: + ``` + window.dash_clientside = window.dash_clientside || {}; + window.dash_clientside.my_clientside_library = { + my_function: function(input_value_1, input_value_2) { + return ( + parseFloat(input_value_1, 10) + + parseFloat(input_value_2, 10) + ); + } + } + ``` + + Alternatively, you can pass the JavaScript source directly to + `clientside_callback`. In this case, the same example would look like: + ``` + app.clientside_callback( + ''' + function(input_value_1, input_value_2) { + return ( + parseFloat(input_value_1, 10) + + parseFloat(input_value_2, 10) + ); + } + ''', + Output('my-div' 'children'), + [Input('my-input', 'value'), + Input('another-input', 'value')] + ) + ``` + + The last, optional argument `prevent_initial_call` causes the callback + not to fire when its outputs are first added to the page. Defaults to + `False` unless `prevent_initial_callbacks=True` at the app level. + """ + return _callback.register_clientside_callback( + self._callback_list, + self.callback_map, + self.config.prevent_initial_callbacks, + self._inline_scripts, + clientside_function, + *args, + **kwargs, + ) + + def callback(self, *_args, **_kwargs) -> Callable[..., Any]: + """ + Normally used as a decorator, `@app.callback` provides a server-side + callback relating the values of one or more `Output` items to one or + more `Input` items which will trigger the callback when they change, + and optionally `State` items which provide additional information but + do not trigger the callback directly. + + The last, optional argument `prevent_initial_call` causes the callback + not to fire when its outputs are first added to the page. Defaults to + `False` unless `prevent_initial_callbacks=True` at the app level. + + + """ + return _callback.callback( + *_args, + config_prevent_initial_callbacks=self.config.prevent_initial_callbacks, + callback_list=self._callback_list, + callback_map=self.callback_map, + **_kwargs, + ) + + # pylint: disable=R0915 + def _initialize_context(self, body): + """Initialize the global context for the request.""" + g = AttributeDict({}) + g.inputs_list = body.get("inputs", []) + g.states_list = body.get("state", []) + g.outputs_list = body.get("outputs", []) + g.input_values = inputs_to_dict(g.inputs_list) + g.state_values = inputs_to_dict(g.states_list) + g.triggered_inputs = [ + {"prop_id": x, "value": g.input_values.get(x)} + for x in body.get("changedPropIds", []) + ] + g.dash_response = flask.Response(mimetype="application/json") + g.cookies = dict(**flask.request.cookies) + g.headers = dict(**flask.request.headers) + g.path = flask.request.full_path + g.remote = flask.request.remote_addr + g.origin = flask.request.origin + g.updated_props = {} + return g + + def _prepare_callback(self, g, body): + """Prepare callback-related data.""" + output = body["output"] + try: + cb = self.callback_map[output] + func = cb["callback"] + g.background_callback_manager = ( + cb.get("manager") or self._background_manager + ) + g.ignore_register_page = cb.get("background", False) + + # Add args_grouping + inputs_state_indices = cb["inputs_state_indices"] + inputs_state = convert_to_AttributeDict(g.inputs_list + g.states_list) + + if cb.get("no_output"): + g.outputs_list = [] + elif not g.outputs_list: + # Legacy support for older renderers + split_callback_id(output) + + # Update args_grouping attributes + for s in inputs_state: + # check for pattern matching: list of inputs or state + if isinstance(s, list): + for pattern_match_g in s: + update_args_group( + pattern_match_g, body.get("changedPropIds", []) + ) + update_args_group(s, body.get("changedPropIds", [])) + + g.args_grouping, g.using_args_grouping = self._prepare_grouping( + inputs_state, inputs_state_indices + ) + g.outputs_grouping, g.using_outputs_grouping = self._prepare_grouping( + g.outputs_list, cb.get("outputs_indices", []) + ) + except KeyError as e: + raise KeyError(f"Callback function not found for output '{output}'.") from e + return func + + def _prepare_grouping(self, data_list, indices): + """Prepare grouping logic for inputs or outputs.""" + if not isinstance(data_list, list): + flat_data = [data_list] + else: + flat_data = data_list + + if len(flat_data) > 0: + grouping = map_grouping(lambda ind: flat_data[ind], indices) + using_grouping = not isinstance(indices, int) and indices != list( + range(grouping_len(indices)) + ) + else: + grouping, using_grouping = [], False + + return grouping, using_grouping + + def _execute_callback(self, func, args, outputs_list, g): + """Execute the callback with the prepared arguments.""" + g.cookies = dict(**flask.request.cookies) + g.headers = dict(**flask.request.headers) + g.path = flask.request.full_path + g.remote = flask.request.remote_addr + g.origin = flask.request.origin + g.custom_data = AttributeDict({}) + + for hook in self._hooks.get_hooks("custom_data"): + g.custom_data[hook.data["namespace"]] = hook(g) + + # noinspection PyArgumentList + partial_func = functools.partial( + func, + *args, + outputs_list=outputs_list, + background_callback_manager=g.background_callback_manager, + callback_context=g, + app=self, + app_on_error=self._on_error, + app_use_async=self._use_async, + ) + return partial_func + + async def async_dispatch(self): + body = flask.request.get_json() + g = self._initialize_context(body) + func = self._prepare_callback(g, body) + args = inputs_to_vals(g.inputs_list + g.states_list) + + ctx = copy_context() + partial_func = self._execute_callback(func, args, g.outputs_list, g) + if asyncio.iscoroutine(func): + response_data = await ctx.run(partial_func) + else: + response_data = ctx.run(partial_func) + + if asyncio.iscoroutine(response_data): + response_data = await response_data + + g.dash_response.set_data(response_data) + return g.dash_response + + def dispatch(self): + body = flask.request.get_json() + g = self._initialize_context(body) + func = self._prepare_callback(g, body) + args = inputs_to_vals(g.inputs_list + g.states_list) + + ctx = copy_context() + partial_func = self._execute_callback(func, args, g.outputs_list, g) + response_data = ctx.run(partial_func) + + if asyncio.iscoroutine(response_data): + raise Exception( + "You are trying to use a coroutine without dash[async]. " + "Please install the dependencies via `pip install dash[async]` and ensure " + "that `use_async=False` is not being passed to the app." + ) + + g.dash_response.set_data(response_data) + return g.dash_response + + def _setup_server(self): + if self._got_first_request["setup_server"]: + return + self._got_first_request["setup_server"] = True + + # Apply _force_eager_loading overrides from modules + eager_loading = self.config.eager_loading + for module_name in ComponentRegistry.registry: + module = sys.modules[module_name] + eager = getattr(module, "_force_eager_loading", False) + eager_loading = eager_loading or eager + + # Update eager_loading settings + self.scripts.config.eager_loading = eager_loading + + if self.config.include_assets_files: + self._walk_assets_directory() + + if not self.layout and self.use_pages: + self.layout = page_container + + _validate.validate_layout(self.layout, self._layout_value()) + + self._generate_scripts_html() + self._generate_css_dist_html() + + # Copy over global callback data structures assigned with `dash.callback` + for k in list(_callback.GLOBAL_CALLBACK_MAP): + if k in self.callback_map: + raise DuplicateCallback( + f"The callback `{k}` provided with `dash.callback` was already " + "assigned with `app.callback`." + ) + + self.callback_map[k] = _callback.GLOBAL_CALLBACK_MAP.pop(k) + + self._callback_list.extend(_callback.GLOBAL_CALLBACK_LIST) + _callback.GLOBAL_CALLBACK_LIST.clear() + + _validate.validate_background_callbacks(self.callback_map) + + cancels = {} + + for callback in self.callback_map.values(): + background = callback.get("background") + if not background: + continue + if "cancel_inputs" in background: + cancel = background.pop("cancel_inputs") + for c in cancel: + cancels[c] = background.get("manager") + + if cancels: + for cancel_input, manager in cancels.items(): + # pylint: disable=cell-var-from-loop + @self.callback( + Output(cancel_input.component_id, "id"), + cancel_input, + prevent_initial_call=True, + manager=manager, + ) + def cancel_call(*_): + job_ids = flask.request.args.getlist("cancelJob") + executor = _callback.context_value.get().background_callback_manager + if job_ids: + for job_id in job_ids: + executor.terminate_job(job_id) + return no_update + + def _add_assets_resource(self, url_path, file_path): + res = {"asset_path": url_path, "filepath": file_path} + if self.config.assets_external_path: + res["external_url"] = self.get_asset_url(url_path.lstrip("/")) + self._assets_files.append(file_path) + return res + + def _walk_assets_directory(self): + walk_dir = self.config.assets_folder + slash_splitter = re.compile(r"[\\/]+") + ignore_str = self.config.assets_ignore + ignore_path_list = self.config.assets_path_ignore + ignore_filter = re.compile(ignore_str) if ignore_str else None + ignore_path_filters = [ + re.compile(ignore_path) + for ignore_path in (ignore_path_list or []) + if ignore_path + ] + + for current, _, files in sorted(os.walk(walk_dir)): + if current == walk_dir: + base = "" + s = "" + else: + s = current.replace(walk_dir, "").lstrip("\\").lstrip("/") + splitted = slash_splitter.split(s) + if len(splitted) > 1: + base = "/".join(slash_splitter.split(s)) + else: + base = splitted[0] + + # Check if any level of current path matches ignore path + if s and any( + ignore_path_filter.search(x) + for ignore_path_filter in ignore_path_filters + for x in s.split(os.path.sep) + ): + pass + else: + if ignore_filter: + files_gen = (x for x in files if not ignore_filter.search(x)) + else: + files_gen = files + + for f in sorted(files_gen): + path = "/".join([base, f]) if base else f + + full = os.path.join(current, f) + + if f.endswith("js"): + self.scripts.append_script( + self._add_assets_resource(path, full) + ) + elif f.endswith("css"): + self.css.append_css(self._add_assets_resource(path, full)) # type: ignore[reportArgumentType] + elif f == "favicon.ico": + self._favicon = path + + @staticmethod + def _invalid_resources_handler(err): + return err.args[0], 404 + + @staticmethod + def _serve_default_favicon(): + return flask.Response( + pkgutil.get_data("dash", "favicon.ico"), content_type="image/x-icon" + ) + + def csp_hashes(self, hash_algorithm="sha256") -> Sequence[str]: + """Calculates CSP hashes (sha + base64) of all inline scripts, such that + one of the biggest benefits of CSP (disallowing general inline scripts) + can be utilized together with Dash clientside callbacks (inline scripts). + + Calculate these hashes after all inline callbacks are defined, + and add them to your CSP headers before starting the server, for example + with the flask-talisman package from PyPI: + + flask_talisman.Talisman(app.server, content_security_policy={ + "default-src": "'self'", + "script-src": ["'self'"] + app.csp_hashes() + }) + + :param hash_algorithm: One of the recognized CSP hash algorithms ('sha256', 'sha384', 'sha512'). + :return: List of CSP hash strings of all inline scripts. + """ + + HASH_ALGORITHMS = ["sha256", "sha384", "sha512"] + if hash_algorithm not in HASH_ALGORITHMS: + raise ValueError( + "Possible CSP hash algorithms: " + ", ".join(HASH_ALGORITHMS) + ) + + method = getattr(hashlib, hash_algorithm) + + def _hash(script): + return base64.b64encode(method(script.encode("utf-8")).digest()).decode( + "utf-8" + ) + + self._inline_scripts.extend(_callback.GLOBAL_INLINE_SCRIPTS) + _callback.GLOBAL_INLINE_SCRIPTS.clear() + + return [ + f"'{hash_algorithm}-{_hash(script)}'" + for script in (self._inline_scripts + [self.renderer]) + ] + + def get_asset_url(self, path: str) -> str: + """ + Return the URL for the provided `path` in the assets directory. + + If `assets_external_path` is set, `get_asset_url` returns + `assets_external_path` + `assets_url_path` + `path`, where + `path` is the path passed to `get_asset_url`. + + Otherwise, `get_asset_url` returns + `requests_pathname_prefix` + `assets_url_path` + `path`, where + `path` is the path passed to `get_asset_url`. + + Use `get_asset_url` in an app to access assets at the correct location + in different environments. In a deployed app on Dash Enterprise, + `requests_pathname_prefix` is the app name. For an app called "my-app", + `app.get_asset_url("image.png")` would return: + + ``` + /my-app/assets/image.png + ``` + + While the same app running locally, without + `requests_pathname_prefix` set, would return: + + ``` + /assets/image.png + ``` + """ + return _get_paths.app_get_asset_url(self.config, path) + + def get_relative_path(self, path): + """ + Return a path with `requests_pathname_prefix` prefixed before it. + Use this function when specifying local URL paths that will work + in environments regardless of what `requests_pathname_prefix` is. + In some deployment environments, like Dash Enterprise, + `requests_pathname_prefix` is set to the application name, + e.g. `my-dash-app`. + When working locally, `requests_pathname_prefix` might be unset and + so a relative URL like `/page-2` can just be `/page-2`. + However, when the app is deployed to a URL like `/my-dash-app`, then + `app.get_relative_path('/page-2')` will return `/my-dash-app/page-2`. + This can be used as an alternative to `get_asset_url` as well with + `app.get_relative_path('/assets/logo.png')` + + Use this function with `app.strip_relative_path` in callbacks that + deal with `dcc.Location` `pathname` routing. + That is, your usage may look like: + ``` + app.layout = html.Div([ + dcc.Location(id='url'), + html.Div(id='content') + ]) + @app.callback(Output('content', 'children'), [Input('url', 'pathname')]) + def display_content(path): + page_name = app.strip_relative_path(path) + if not page_name: # None or '' + return html.Div([ + dcc.Link(href=app.get_relative_path('/page-1')), + dcc.Link(href=app.get_relative_path('/page-2')), + ]) + elif page_name == 'page-1': + return chapters.page_1 + if page_name == "page-2": + return chapters.page_2 + ``` + """ + return _get_paths.app_get_relative_path( + self.config.requests_pathname_prefix, path + ) + + def strip_relative_path(self, path: str) -> Union[str, None]: + """ + Return a path with `requests_pathname_prefix` and leading and trailing + slashes stripped from it. Also, if None is passed in, None is returned. + Use this function with `get_relative_path` in callbacks that deal + with `dcc.Location` `pathname` routing. + That is, your usage may look like: + ``` + app.layout = html.Div([ + dcc.Location(id='url'), + html.Div(id='content') + ]) + @app.callback(Output('content', 'children'), [Input('url', 'pathname')]) + def display_content(path): + page_name = app.strip_relative_path(path) + if not page_name: # None or '' + return html.Div([ + dcc.Link(href=app.get_relative_path('/page-1')), + dcc.Link(href=app.get_relative_path('/page-2')), + ]) + elif page_name == 'page-1': + return chapters.page_1 + if page_name == "page-2": + return chapters.page_2 + ``` + Note that `chapters.page_1` will be served if the user visits `/page-1` + _or_ `/page-1/` since `strip_relative_path` removes the trailing slash. + + Also note that `strip_relative_path` is compatible with + `get_relative_path` in environments where `requests_pathname_prefix` set. + In some deployment environments, like Dash Enterprise, + `requests_pathname_prefix` is set to the application name, e.g. `my-dash-app`. + When working locally, `requests_pathname_prefix` might be unset and + so a relative URL like `/page-2` can just be `/page-2`. + However, when the app is deployed to a URL like `/my-dash-app`, then + `app.get_relative_path('/page-2')` will return `/my-dash-app/page-2` + + The `pathname` property of `dcc.Location` will return '`/my-dash-app/page-2`' + to the callback. + In this case, `app.strip_relative_path('/my-dash-app/page-2')` + will return `'page-2'` + + For nested URLs, slashes are still included: + `app.strip_relative_path('/page-1/sub-page-1/')` will return + `page-1/sub-page-1` + ``` + """ + return _get_paths.app_strip_relative_path( + self.config.requests_pathname_prefix, path + ) + + @staticmethod + def add_startup_route( + name: str, view_func: RouteCallable, methods: Sequence[Literal["POST", "GET"]] + ) -> None: + """ + Add a route to the app to be initialized at the end of Dash initialization. + Use this if the package requires a route to be added to the app, and you will not need to worry about at what point to add it. + + :param name: The name of the route. eg "my-new-url/path". + :param view_func: The function to call when the route is requested. The function should return a JSON serializable object. + :param methods: The HTTP methods that the route should respond to. eg ["GET", "POST"] or either one. + """ + if not isinstance(name, str) or name.startswith("/"): + raise ValueError("name must be a string and should not start with '/'") + + if not callable(view_func): + raise ValueError("view_func must be callable") + + valid_methods = {"POST", "GET"} + if not set(methods).issubset(valid_methods): + raise ValueError(f"methods should only contain {valid_methods}") + + if any(route[0] == name for route in Dash.STARTUP_ROUTES): + raise ValueError(f"Route name '{name}' is already in use.") + + Dash.STARTUP_ROUTES.append((name, view_func, methods)) + + def setup_startup_routes(self) -> None: + """ + Initialize the startup routes stored in STARTUP_ROUTES. + """ + for _name, _view_func, _methods in self.STARTUP_ROUTES: + self._add_url(f"_dash_startup_route/{_name}", _view_func, _methods) + self.STARTUP_ROUTES = [] + + def _setup_dev_tools(self, **kwargs): + debug = kwargs.get("debug", False) + dev_tools = self._dev_tools = AttributeDict() + + for attr in ( + "ui", + "props_check", + "serve_dev_bundles", + "hot_reload", + "silence_routes_logging", + "prune_errors", + ): + dev_tools[attr] = get_combined_config( + attr, kwargs.get(attr, None), default=debug + ) + + for attr, _type, default in ( + ("hot_reload_interval", float, 3), + ("hot_reload_watch_interval", float, 0.5), + ("hot_reload_max_retry", int, 8), + ): + dev_tools[attr] = _type( + get_combined_config(attr, kwargs.get(attr, None), default=default) + ) + + dev_tools["disable_version_check"] = get_combined_config( + "disable_version_check", + kwargs.get("disable_version_check", None), + default=False, + ) + + return dev_tools + + def enable_dev_tools( + self, + debug: Optional[bool] = None, + dev_tools_ui: Optional[bool] = None, + dev_tools_props_check: Optional[bool] = None, + dev_tools_serve_dev_bundles: Optional[bool] = None, + dev_tools_hot_reload: Optional[bool] = None, + dev_tools_hot_reload_interval: Optional[int] = None, + dev_tools_hot_reload_watch_interval: Optional[int] = None, + dev_tools_hot_reload_max_retry: Optional[int] = None, + dev_tools_silence_routes_logging: Optional[bool] = None, + dev_tools_disable_version_check: Optional[bool] = None, + dev_tools_prune_errors: Optional[bool] = None, + ) -> bool: + """Activate the dev tools, called by `run`. If your application + is served by wsgi and you want to activate the dev tools, you can call + this method out of `__main__`. + + All parameters can be set by environment variables as listed. + Values provided here take precedence over environment variables. + + Available dev_tools environment variables: + + - DASH_DEBUG + - DASH_UI + - DASH_PROPS_CHECK + - DASH_SERVE_DEV_BUNDLES + - DASH_HOT_RELOAD + - DASH_HOT_RELOAD_INTERVAL + - DASH_HOT_RELOAD_WATCH_INTERVAL + - DASH_HOT_RELOAD_MAX_RETRY + - DASH_SILENCE_ROUTES_LOGGING + - DASH_DISABLE_VERSION_CHECK + - DASH_PRUNE_ERRORS + + :param debug: Enable/disable all the dev tools unless overridden by the + arguments or environment variables. Default is ``True`` when + ``enable_dev_tools`` is called directly, and ``False`` when called + via ``run``. env: ``DASH_DEBUG`` + :type debug: bool + + :param dev_tools_ui: Show the dev tools UI. env: ``DASH_UI`` + :type dev_tools_ui: bool + + :param dev_tools_props_check: Validate the types and values of Dash + component props. env: ``DASH_PROPS_CHECK`` + :type dev_tools_props_check: bool + + :param dev_tools_serve_dev_bundles: Serve the dev bundles. Production + bundles do not necessarily include all the dev tools code. + env: ``DASH_SERVE_DEV_BUNDLES`` + :type dev_tools_serve_dev_bundles: bool + + :param dev_tools_hot_reload: Activate hot reloading when app, assets, + and component files change. env: ``DASH_HOT_RELOAD`` + :type dev_tools_hot_reload: bool + + :param dev_tools_hot_reload_interval: Interval in seconds for the + client to request the reload hash. Default 3. + env: ``DASH_HOT_RELOAD_INTERVAL`` + :type dev_tools_hot_reload_interval: float + + :param dev_tools_hot_reload_watch_interval: Interval in seconds for the + server to check asset and component folders for changes. + Default 0.5. env: ``DASH_HOT_RELOAD_WATCH_INTERVAL`` + :type dev_tools_hot_reload_watch_interval: float + + :param dev_tools_hot_reload_max_retry: Maximum number of failed reload + hash requests before failing and displaying a pop up. Default 8. + env: ``DASH_HOT_RELOAD_MAX_RETRY`` + :type dev_tools_hot_reload_max_retry: int + + :param dev_tools_silence_routes_logging: Silence the `werkzeug` logger, + will remove all routes logging. Enabled with debugging by default + because hot reload hash checks generate a lot of requests. + env: ``DASH_SILENCE_ROUTES_LOGGING`` + :type dev_tools_silence_routes_logging: bool + + :param dev_tools_disable_version_check: Silence the upgrade + notification to prevent making requests to the Dash server. + env: ``DASH_DISABLE_VERSION_CHECK`` + :type dev_tools_disable_version_check: bool + + :param dev_tools_prune_errors: Reduce tracebacks to just user code, + stripping out Flask and Dash pieces. Only available with debugging. + `True` by default, set to `False` to see the complete traceback. + env: ``DASH_PRUNE_ERRORS`` + :type dev_tools_prune_errors: bool + + :return: debug + """ + if debug is None: + debug = get_combined_config("debug", None, True) + + dev_tools = self._setup_dev_tools( + debug=debug, + ui=dev_tools_ui, + props_check=dev_tools_props_check, + serve_dev_bundles=dev_tools_serve_dev_bundles, + hot_reload=dev_tools_hot_reload, + hot_reload_interval=dev_tools_hot_reload_interval, + hot_reload_watch_interval=dev_tools_hot_reload_watch_interval, + hot_reload_max_retry=dev_tools_hot_reload_max_retry, + silence_routes_logging=dev_tools_silence_routes_logging, + disable_version_check=dev_tools_disable_version_check, + prune_errors=dev_tools_prune_errors, + ) + + if dev_tools.silence_routes_logging: + logging.getLogger("werkzeug").setLevel(logging.ERROR) + + if dev_tools.hot_reload: + _reload = self._hot_reload + _reload.hash = generate_hash() + + # find_loader should return None on __main__ but doesn't + # on some Python versions https://bugs.python.org/issue14710 + packages = [ + pkgutil.find_loader(x) + for x in list(ComponentRegistry.registry) + if x != "__main__" + ] + + # # additional condition to account for AssertionRewritingHook object + # # loader when running pytest + + if "_pytest" in sys.modules: + from _pytest.assertion.rewrite import ( # pylint: disable=import-outside-toplevel + AssertionRewritingHook, # type: ignore[reportPrivateImportUsage] + ) + + for index, package in enumerate(packages): + if isinstance(package, AssertionRewritingHook): + dash_spec = importlib.util.find_spec("dash") # type: ignore[reportAttributeAccess] + dash_test_path = dash_spec.submodule_search_locations[0] + setattr(dash_spec, "path", dash_test_path) + packages[index] = dash_spec + + component_packages_dist = [ + dash_test_path # type: ignore[reportPossiblyUnboundVariable] + if isinstance(package, ModuleSpec) + else os.path.dirname(package.path) # type: ignore[reportAttributeAccessIssue] + if hasattr(package, "path") + else os.path.dirname( + package._path[0] # type: ignore[reportAttributeAccessIssue]; pylint: disable=protected-access + ) + if hasattr(package, "_path") + else package.filename # type: ignore[reportAttributeAccessIssue] + for package in packages + ] + + for i, package in enumerate(packages): + if hasattr(package, "path") and "dash/dash" in os.path.dirname( + package.path # type: ignore[reportAttributeAccessIssue] + ): + component_packages_dist[i : i + 1] = [ + os.path.join(os.path.dirname(package.path), x) # type: ignore[reportAttributeAccessIssue] + for x in ["dcc", "html", "dash_table"] + ] + + _reload.watch_thread = threading.Thread( + target=lambda: _watch.watch( + [self.config.assets_folder] + component_packages_dist, + self._on_assets_change, + sleep_time=dev_tools.hot_reload_watch_interval, + ) + ) + _reload.watch_thread.daemon = True + _reload.watch_thread.start() + + if debug: + if jupyter_dash.active: + jupyter_dash.configure_callback_exception_handling( + self, dev_tools.prune_errors + ) + elif dev_tools.prune_errors: + secret = gen_salt(20) + + @self.server.errorhandler(Exception) + def _wrap_errors(error): + # find the callback invocation, if the error is from a callback + # and skip the traceback up to that point + # if the error didn't come from inside a callback, we won't + # skip anything. + tb = _get_traceback(secret, error) + return tb, 500 + + if debug and dev_tools.ui: + + def _before_request(): + flask.g.timing_information = { # pylint: disable=assigning-non-slot + "__dash_server": {"dur": time.time(), "desc": None} + } + + def _after_request(response): + timing_information = flask.g.get("timing_information", None) + if timing_information is None: + return response + + dash_total = timing_information.get("__dash_server", None) + if dash_total is not None: + dash_total["dur"] = round((time.time() - dash_total["dur"]) * 1000) + + for name, info in timing_information.items(): + value = name + if info.get("desc") is not None: + value += f';desc="{info["desc"]}"' + + if info.get("dur") is not None: + value += f";dur={info['dur']}" + + response.headers.add("Server-Timing", value) + + return response + + self.server.before_request(_before_request) + + self.server.after_request(_after_request) + + if ( + debug + and dev_tools.serve_dev_bundles + and not self.scripts.config.serve_locally + ): + # Dev bundles only works locally. + self.scripts.config.serve_locally = True + print( + "WARNING: dev bundles requested with serve_locally=False.\n" + "This is not supported, switching to serve_locally=True" + ) + + return debug + + # noinspection PyProtectedMember + def _on_assets_change(self, filename, modified, deleted): + _reload = self._hot_reload + with _reload.lock: + _reload.hard = True + _reload.hash = generate_hash() + + if self.config.assets_folder in filename: + asset_path = ( + os.path.relpath( + filename, + os.path.commonprefix([self.config.assets_folder, filename]), + ) + .replace("\\", "/") + .lstrip("/") + ) + + _reload.changed_assets.append( + { + "url": self.get_asset_url(asset_path), + "modified": int(modified), + "is_css": filename.endswith("css"), + } + ) + + if filename not in self._assets_files and not deleted: + res = self._add_assets_resource(asset_path, filename) + if filename.endswith("js"): + self.scripts.append_script(res) + elif filename.endswith("css"): + self.css.append_css(res) # type: ignore[reportArgumentType] + + if deleted: + if filename in self._assets_files: + self._assets_files.remove(filename) + + def delete_resource(resources): + to_delete = None + for r in resources: + if r.get("asset_path") == asset_path: + to_delete = r + break + if to_delete: + resources.remove(to_delete) + + if filename.endswith("js"): + # pylint: disable=protected-access + delete_resource(self.scripts._resources._resources) + elif filename.endswith("css"): + # pylint: disable=protected-access + delete_resource(self.css._resources._resources) + + # pylint: disable=too-many-branches + def run( + self, + host: Optional[str] = None, + port: Optional[Union[str, int]] = None, + proxy: Optional[str] = None, + debug: Optional[bool] = None, + jupyter_mode: Optional[JupyterDisplayMode] = None, + jupyter_width: str = "100%", + jupyter_height: int = 650, + jupyter_server_url: Optional[str] = None, + dev_tools_ui: Optional[bool] = None, + dev_tools_props_check: Optional[bool] = None, + dev_tools_serve_dev_bundles: Optional[bool] = None, + dev_tools_hot_reload: Optional[bool] = None, + dev_tools_hot_reload_interval: Optional[int] = None, + dev_tools_hot_reload_watch_interval: Optional[int] = None, + dev_tools_hot_reload_max_retry: Optional[int] = None, + dev_tools_silence_routes_logging: Optional[bool] = None, + dev_tools_disable_version_check: Optional[bool] = None, + dev_tools_prune_errors: Optional[bool] = None, + **flask_run_options, + ): + """Start the flask server in local mode, you should not run this on a + production server, use gunicorn/waitress instead. + + If a parameter can be set by an environment variable, that is listed + too. Values provided here take precedence over environment variables. + + :param host: Host IP used to serve the application, default to "127.0.0.1" + env: ``HOST`` + :type host: string + + :param port: Port used to serve the application, default to "8050" + env: ``PORT`` + :type port: int + + :param proxy: If this application will be served to a different URL + via a proxy configured outside of Python, you can list it here + as a string of the form ``"{input}::{output}"``, for example: + ``"http://0.0.0.0:8050::https://my.domain.com"`` + so that the startup message will display an accurate URL. + env: ``DASH_PROXY`` + :type proxy: string + + :param debug: Set Flask debug mode and enable dev tools. + env: ``DASH_DEBUG`` + :type debug: bool + + :param debug: Enable/disable all the dev tools unless overridden by the + arguments or environment variables. Default is ``True`` when + ``enable_dev_tools`` is called directly, and ``False`` when called + via ``run``. env: ``DASH_DEBUG`` + :type debug: bool + + :param dev_tools_ui: Show the dev tools UI. env: ``DASH_UI`` + :type dev_tools_ui: bool + + :param dev_tools_props_check: Validate the types and values of Dash + component props. env: ``DASH_PROPS_CHECK`` + :type dev_tools_props_check: bool + + :param dev_tools_serve_dev_bundles: Serve the dev bundles. Production + bundles do not necessarily include all the dev tools code. + env: ``DASH_SERVE_DEV_BUNDLES`` + :type dev_tools_serve_dev_bundles: bool + + :param dev_tools_hot_reload: Activate hot reloading when app, assets, + and component files change. env: ``DASH_HOT_RELOAD`` + :type dev_tools_hot_reload: bool + + :param dev_tools_hot_reload_interval: Interval in seconds for the + client to request the reload hash. Default 3. + env: ``DASH_HOT_RELOAD_INTERVAL`` + :type dev_tools_hot_reload_interval: float + + :param dev_tools_hot_reload_watch_interval: Interval in seconds for the + server to check asset and component folders for changes. + Default 0.5. env: ``DASH_HOT_RELOAD_WATCH_INTERVAL`` + :type dev_tools_hot_reload_watch_interval: float + + :param dev_tools_hot_reload_max_retry: Maximum number of failed reload + hash requests before failing and displaying a pop up. Default 8. + env: ``DASH_HOT_RELOAD_MAX_RETRY`` + :type dev_tools_hot_reload_max_retry: int + + :param dev_tools_silence_routes_logging: Silence the `werkzeug` logger, + will remove all routes logging. Enabled with debugging by default + because hot reload hash checks generate a lot of requests. + env: ``DASH_SILENCE_ROUTES_LOGGING`` + :type dev_tools_silence_routes_logging: bool + + :param dev_tools_disable_version_check: Silence the upgrade + notification to prevent making requests to the Dash server. + env: ``DASH_DISABLE_VERSION_CHECK`` + :type dev_tools_disable_version_check: bool + + :param dev_tools_prune_errors: Reduce tracebacks to just user code, + stripping out Flask and Dash pieces. Only available with debugging. + `True` by default, set to `False` to see the complete traceback. + env: ``DASH_PRUNE_ERRORS`` + :type dev_tools_prune_errors: bool + + :param jupyter_mode: How to display the application when running + inside a jupyter notebook. + + :param jupyter_width: Determine the width of the output cell + when displaying inline in jupyter notebooks. + :type jupyter_width: str + + :param jupyter_height: Height of app when displayed using + jupyter_mode="inline" + :type jupyter_height: int + + :param jupyter_server_url: Custom server url to display + the app in jupyter notebook. + + :param flask_run_options: Given to `Flask.run` + + :return: + """ + if debug is None: + debug = get_combined_config("debug", None, False) + + debug = self.enable_dev_tools( + debug, + dev_tools_ui, + dev_tools_props_check, + dev_tools_serve_dev_bundles, + dev_tools_hot_reload, + dev_tools_hot_reload_interval, + dev_tools_hot_reload_watch_interval, + dev_tools_hot_reload_max_retry, + dev_tools_silence_routes_logging, + dev_tools_disable_version_check, + dev_tools_prune_errors, + ) + + # Evaluate the env variables at runtime + + if "CONDA_PREFIX" in os.environ: + # Some conda systems has issue with setting the host environment + # to an invalid hostname. + # Related issue: https://github.com/plotly/dash/issues/3069 + host = host or "127.0.0.1" + else: + host = host or os.getenv("HOST", "127.0.0.1") + port = port or os.getenv("PORT", "8050") + proxy = proxy or os.getenv("DASH_PROXY") + + # Verify port value + try: + port = int(port) + assert port in range(1, 65536) + except Exception as e: + e.args = (f"Expecting an integer from 1 to 65535, found port={repr(port)}",) + raise + + # so we only see the "Running on" message once with hot reloading + # https://stackoverflow.com/a/57231282/9188800 + if os.getenv("WERKZEUG_RUN_MAIN") != "true": + ssl_context = flask_run_options.get("ssl_context") + protocol = "https" if ssl_context else "http" + path = self.config.requests_pathname_prefix + + if proxy: + served_url, proxied_url = map(urlparse, proxy.split("::")) + + def verify_url_part(served_part, url_part, part_name): + if served_part != url_part: + raise ProxyError( + f""" + {part_name}: {url_part} is incompatible with the proxy: + {proxy} + To see your app at {proxied_url.geturl()}, + you must use {part_name}: {served_part} + """ + ) + + verify_url_part(served_url.scheme, protocol, "protocol") + verify_url_part(served_url.hostname, host, "host") + verify_url_part(served_url.port, port, "port") + + display_url = ( + proxied_url.scheme, + proxied_url.hostname, + f":{proxied_url.port}" if proxied_url.port else "", + path, + ) + else: + display_url = (protocol, host, f":{port}", path) + + if not jupyter_dash or not jupyter_dash.in_ipython: + self.logger.info("Dash is running on %s://%s%s%s\n", *display_url) + + if self.config.extra_hot_reload_paths: + extra_files = flask_run_options["extra_files"] = [] + for path in self.config.extra_hot_reload_paths: + if os.path.isdir(path): + for dirpath, _, filenames in os.walk(path): + for fn in filenames: + extra_files.append(os.path.join(dirpath, fn)) + elif os.path.isfile(path): + extra_files.append(path) + + if jupyter_dash.active: + jupyter_dash.run_app( + self, + mode=jupyter_mode, + width=jupyter_width, + height=jupyter_height, + host=host, + port=port, + server_url=jupyter_server_url, + ) + else: + self.server.run(host=host, port=port, debug=debug, **flask_run_options) + + def enable_pages(self) -> None: + if not self.use_pages: + return + if self.pages_folder: + _import_layouts_from_pages(self.config.pages_folder) + + @self.server.before_request + def router(): + if self._got_first_request["pages"]: + return + self._got_first_request["pages"] = True + + inputs = { + "pathname_": Input(_ID_LOCATION, "pathname"), + "search_": Input(_ID_LOCATION, "search"), + } + inputs.update(self.routing_callback_inputs) # type: ignore[reportCallIssue] + + if self._use_async: + + @self.callback( + Output(_ID_CONTENT, "children"), + Output(_ID_STORE, "data"), + inputs=inputs, + prevent_initial_call=True, + ) + async def update(pathname_, search_, **states): + """ + Updates dash.page_container layout on page navigation. + Updates the stored page title which will trigger the clientside callback to update the app title + """ + + query_parameters = _parse_query_string(search_) + page, path_variables = _path_to_page( + self.strip_relative_path(pathname_) + ) + + # get layout + if page == {}: + for module, page in _pages.PAGE_REGISTRY.items(): + if module.split(".")[-1] == "not_found_404": + layout = page["layout"] + title = page["title"] + break + else: + layout = html.H1("404 - Page not found") + title = self.title + else: + layout = page.get("layout", "") + title = page["title"] + + if callable(layout): + layout = await execute_async_function( + layout, + **{**(path_variables or {}), **query_parameters, **states}, + ) + if callable(title): + title = await execute_async_function( + title, **(path_variables or {}) + ) + + return layout, {"title": title} + + _validate.check_for_duplicate_pathnames(_pages.PAGE_REGISTRY) + _validate.validate_registry(_pages.PAGE_REGISTRY) + + # Set validation_layout + if not self.config.suppress_callback_exceptions: + self.validation_layout = html.Div( + [ + asyncio.run(execute_async_function(page["layout"])) + if callable(page["layout"]) + else page["layout"] + for page in _pages.PAGE_REGISTRY.values() + ] + + [ + # pylint: disable=not-callable + self.layout() + if callable(self.layout) + else self.layout + ] + ) + if _ID_CONTENT not in self.validation_layout: + raise Exception("`dash.page_container` not found in the layout") + else: + + @self.callback( + Output(_ID_CONTENT, "children"), + Output(_ID_STORE, "data"), + inputs=inputs, + prevent_initial_call=True, + ) + def update(pathname_, search_, **states): + """ + Updates dash.page_container layout on page navigation. + Updates the stored page title which will trigger the clientside callback to update the app title + """ + + query_parameters = _parse_query_string(search_) + page, path_variables = _path_to_page( + self.strip_relative_path(pathname_) + ) + + # get layout + if page == {}: + for module, page in _pages.PAGE_REGISTRY.items(): + if module.split(".")[-1] == "not_found_404": + layout = page["layout"] + title = page["title"] + break + else: + layout = html.H1("404 - Page not found") + title = self.title + else: + layout = page.get("layout", "") + title = page["title"] + + if callable(layout): + layout = layout( + **{**(path_variables or {}), **query_parameters, **states} + ) + if callable(title): + title = title(**(path_variables or {})) + + return layout, {"title": title} + + _validate.check_for_duplicate_pathnames(_pages.PAGE_REGISTRY) + _validate.validate_registry(_pages.PAGE_REGISTRY) + + # Set validation_layout + if not self.config.suppress_callback_exceptions: + layout = self.layout + if not isinstance(layout, list): + layout = [ + # pylint: disable=not-callable + self.layout() + if callable(self.layout) + else self.layout + ] + self.validation_layout = html.Div( + [ + page["layout"]() + if callable(page["layout"]) + else page["layout"] + for page in _pages.PAGE_REGISTRY.values() + ] + + layout + ) + if _ID_CONTENT not in self.validation_layout: + raise Exception("`dash.page_container` not found in the layout") + + # Update the page title on page navigation + self.clientside_callback( + """ + function(data) {{ + document.title = data.title + }} + """, + Output(_ID_DUMMY, "children"), + Input(_ID_STORE, "data"), + ) + + def __call__(self, environ, start_response): + """ + This method makes instances of Dash WSGI-compliant callables. + It delegates the actual WSGI handling to the internal Flask app's + __call__ method. + """ + return self.server(environ, start_response) |