File: setup.py

import io
import os
from setuptools import setup, find_packages

main_ns = {}
exec(open("dash/version.py", encoding="utf-8").read(), main_ns)  # pylint: disable=exec-used, consider-using-with


def read_req_file(req_type):
    with open(os.path.join("requirements", f"{req_type}.txt"), encoding="utf-8") as fp:
        requires = (line.strip() for line in fp)
        return [req for req in requires if req and not req.startswith("#")]


setup(
    name="dash",
    version=main_ns["__version__"],
    author="Chris Parmer",
    author_email="chris@plotly.com",
    packages=find_packages(exclude=["tests*"]),
    include_package_data=True,
    license="MIT",
    description=(
        "A Python framework for building reactive web-apps. "
        "Developed by Plotly."
    ),
    long_description=io.open("README.md", encoding="utf-8").read(),  # pylint: disable=consider-using-with
    long_description_content_type="text/markdown",
    install_requires=read_req_file("install"),
    python_requires=">=3.8",
    extras_require={
        "ci": read_req_file("ci"),
        "dev": read_req_file("dev"),
        "testing": read_req_file("testing"),
        "celery": read_req_file("celery"),
        "diskcache": read_req_file("diskcache"),
        "compress": read_req_file("compress")
    },
    entry_points={
        "console_scripts": [
            "dash-generate-components = "
            "dash.development.component_generator:cli",
            "renderer = dash.development.build_process:renderer",
            "dash-update-components = dash.development.update_components:cli"
        ],
        "pytest11": ["dash = dash.testing.plugin"],
    },
    url="https://plotly.com/dash",
    project_urls={
        "Documentation": "https://dash.plotly.com",
        "Source": "https://github.com/plotly/dash",
        "Issue Tracker": "https://github.com/plotly/dash/issues",
    },
    classifiers=[
        "Development Status :: 5 - Production/Stable",
        "Environment :: Web Environment",
        "Framework :: Dash",
        "Framework :: Flask",
        "Intended Audience :: Developers",
        "Intended Audience :: Education",
        "Intended Audience :: Financial and Insurance Industry",
        "Intended Audience :: Healthcare Industry",
        "Intended Audience :: Manufacturing",
        "Intended Audience :: Science/Research",
        "License :: OSI Approved :: MIT License",
        "Programming Language :: Python",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.8",
        "Programming Language :: Python :: 3.9",
        "Programming Language :: Python :: 3.10",
        "Programming Language :: Python :: 3.11",
        "Programming Language :: Python :: 3.12",
        "Topic :: Database :: Front-Ends",
        "Topic :: Office/Business :: Financial :: Spreadsheet",
        "Topic :: Scientific/Engineering :: Visualization",
        "Topic :: Software Development :: Libraries :: Application Frameworks",
        "Topic :: Software Development :: Widget Sets",
    ],
    data_files=[
        # like `jupyter nbextension install --sys-prefix`
        ("share/jupyter/nbextensions/dash", [
            "dash/nbextension/main.js",
        ]),
        # like `jupyter nbextension enable --sys-prefix`
        ("etc/jupyter/nbconfig/notebook.d", [
            "dash/nbextension/dash.json"
        ]),
        # Place jupyterlab extension in extension directory
        ("share/jupyter/lab/extensions", [
            "dash/labextension/dist/dash-jupyterlab.tgz"
        ]),
    ],
)



File: components/dash-core-components/__init__.py




File: components/dash-core-components/setup.py

from setuptools import setup
import json

with open("package.json") as f:
    package = json.load(f)

package_name = str(package["name"].replace(" ", "_").replace("-", "_"))

setup(
    name="dash_core_components",
    version=package["version"],
    author=package["author"],
    author_email="chris@plotly.com",
    packages=[package_name],
    include_package_data=True,
    license=package["license"],
    description=package.get("description", package_name),
    install_requires=[],
)



File: components/dash-core-components/dash_core_components_base/__init__.py

import json
import os as _os
import sys as _sys

import dash as _dash

from ._imports_ import *  # noqa: F401, F403, E402
from ._imports_ import __all__ as _components
from .express import (  # noqa: F401, E402
    send_bytes,
    send_data_frame,
    send_file,
    send_string,
)

__all__ = _components + [
    "send_bytes",
    "send_data_frame",
    "send_file",
    "send_string",
]

_basepath = _os.path.dirname(__file__)
_filepath = _os.path.abspath(_os.path.join(_basepath, "package-info.json"))
with open(_filepath) as f:
    package = json.load(f)

package_name = package["name"].replace(" ", "_").replace("-", "_")
__version__ = package["version"]

# Module imports trigger a dash.development import, need to check this first
if not hasattr(_dash, "__plotly_dash") and not hasattr(_dash, "development"):
    print(
        "Dash was not successfully imported. Make sure you don't have a file "
        "named \n'dash.py' in your current directory.",
        file=_sys.stderr,
    )
    _sys.exit(1)


_current_path = _os.path.dirname(_os.path.abspath(__file__))


_this_module = "dash_core_components"

async_resources = [
    "datepicker",
    "dropdown",
    "graph",
    "highlight",
    "markdown",
    "mathjax",
    "slider",
    "upload",
]

_js_dist = []

_js_dist.extend(
    [
        {
            "relative_package_path": "dcc/async-{}.js".format(async_resource),
            "external_url": (
                "https://unpkg.com/dash-core-components@{}"
                "/dash_core_components/async-{}.js"
            ).format(__version__, async_resource),
            "namespace": "dash",
            "async": True,
        }
        for async_resource in async_resources
    ]
)

_js_dist.extend(
    [
        {
            "relative_package_path": "dcc/async-{}.js.map".format(async_resource),
            "external_url": (
                "https://unpkg.com/dash-core-components@{}"
                "/dash_core_components/async-{}.js.map"
            ).format(__version__, async_resource),
            "namespace": "dash",
            "dynamic": True,
        }
        for async_resource in async_resources
    ]
)

_js_dist.extend(
    [
        {
            "relative_package_path": "dcc/{}.js".format(_this_module),
            "external_url": (
                "https://unpkg.com/dash-core-components@{}"
                "/dash_core_components/dash_core_components.js"
            ).format(__version__),
            "namespace": "dash",
        },
        {
            "relative_package_path": "dcc/{}.js.map".format(_this_module),
            "external_url": (
                "https://unpkg.com/dash-core-components@{}"
                "/dash_core_components/dash_core_components.js.map"
            ).format(__version__),
            "namespace": "dash",
            "dynamic": True,
        },
        {
            "relative_package_path": "dcc/{}-shared.js".format(_this_module),
            "external_url": (
                "https://unpkg.com/dash-core-components@{}"
                "/dash_core_components/dash_core_components-shared.js"
            ).format(__version__),
            "namespace": "dash",
        },
        {
            "relative_package_path": "dcc/{}-shared.js.map".format(_this_module),
            "external_url": (
                "https://unpkg.com/dash-core-components@{}"
                "/dash_core_components/dash_core_components-shared.js.map"
            ).format(__version__),
            "namespace": "dash",
            "dynamic": True,
        },
    ]
)

for _component in __all__:
    setattr(locals()[_component], "_js_dist", _js_dist)



File: components/dash-core-components/dash_core_components_base/express.py

import io
import ntpath
import base64

# region Utils for Download component


def send_file(path, filename=None, type=None):
    """
    Convert a file into the format expected by the Download component.
    :param path: path to the file to be sent
    :param filename: name of the file, if not provided the original filename is used
    :param type: type of the file (optional, passed to Blob in the javascript layer)
    :return: dict of file content (base64 encoded) and meta data used by the Download component
    """
    # If filename is not set, read it from the path.
    if filename is None:
        filename = ntpath.basename(path)
    # Read the file contents and send it.
    with open(path, "rb") as f:
        return send_bytes(f.read(), filename, type)


def send_bytes(src, filename, type=None, **kwargs):
    """
    Convert data written to BytesIO into the format expected by the Download component.
    :param src: array of bytes or a writer that can write to BytesIO
    :param filename: the name of the file
    :param type: type of the file (optional, passed to Blob in the javascript layer)
    :return: dict of data frame content (base64 encoded) and meta data used by the Download component
    """
    content = src if isinstance(src, bytes) else _io_to_str(io.BytesIO(), src, **kwargs)
    return dict(
        content=base64.b64encode(content).decode(),
        filename=filename,
        type=type,
        base64=True,
    )


def send_string(src, filename, type=None, **kwargs):
    """
    Convert data written to StringIO into the format expected by the Download component.
    :param src: a string or a writer that can write to StringIO
    :param filename: the name of the file
    :param type: type of the file (optional, passed to Blob in the javascript layer)
    :return: dict of data frame content (NOT base64 encoded) and meta data used by the Download component
    """
    content = src if isinstance(src, str) else _io_to_str(io.StringIO(), src, **kwargs)
    return dict(content=content, filename=filename, type=type, base64=False)


def _io_to_str(data_io, writer, **kwargs):
    # Some pandas writers try to close the IO, we do not want that.
    data_io_close = data_io.close
    data_io.close = lambda: None
    # Write data content.
    writer(data_io, **kwargs)
    data_value = data_io.getvalue()
    data_io_close()
    return data_value


def send_data_frame(writer, filename, type=None, **kwargs):
    """
    Convert data frame into the format expected by the Download component.
    :param writer: a data frame writer
    :param filename: the name of the file
    :param type: type of the file (optional, passed to Blob in the javascript layer)
    :return: dict of data frame content (base64 encoded) and meta data used by the Download component

    Examples
    --------

    >>> df = pd.DataFrame({'a': [1, 2, 3, 4], 'b': [2, 1, 5, 6], 'c': ['x', 'x', 'y', 'y']})
    ...
    >>> send_data_frame(df.to_csv, "mydf.csv")  # download as csv
    >>> send_data_frame(df.to_json, "mydf.json")  # download as json
    >>> send_data_frame(df.to_excel, "mydf.xls", index=False) # download as excel
    >>> send_data_frame(df.to_pickle, "mydf.pkl") # download as pickle

    """
    name = writer.__name__
    # Check if the provided writer is known.
    if name not in _data_frame_senders.keys():
        raise ValueError(
            "The provided writer ({}) is not supported, "
            "try calling send_string or send_bytes directly.".format(name)
        )
    # Send data frame using the appropriate send function.
    return _data_frame_senders[name](writer, filename, type, **kwargs)


_data_frame_senders = {
    "to_csv": send_string,
    "to_json": send_string,
    "to_html": send_string,
    "to_excel": send_bytes,
    "to_feather": send_bytes,
    "to_parquet": send_bytes,
    "to_msgpack": send_bytes,
    "to_stata": send_bytes,
    "to_pickle": send_bytes,
}

# endregion



File: components/dash-html-components/setup.py

import io
import json
from setuptools import setup

with open("package.json") as f:
    package = json.load(f)

package_name = str(package["name"].replace(" ", "_").replace("-", "_"))

setup(
    name="dash_html_components",
    version=package["version"],
    author=package["author"],
    author_email="chris@plotly.com",
    packages=[package_name],
    url="https://github.com/plotly/dash-html-components",
    include_package_data=True,
    license=package["license"],
    description=package["description"] if "description" in package else package_name,
    long_description=io.open("README.md", encoding="utf-8").read(),
    long_description_content_type="text/markdown",
    install_requires=[],
)



File: components/dash-html-components/dash_html_components_base/__init__.py

"""Vanilla HTML components for Dash"""

from ._imports_ import *  # noqa: E402, F401, F403
from ._imports_ import __all__  # noqa: E402

import json
import os as _os
import sys as _sys
import dash as _dash

_basepath = _os.path.dirname(__file__)
_filepath = _os.path.abspath(_os.path.join(_basepath, "package-info.json"))
with open(_filepath) as f:
    package = json.load(f)

package_name = package["name"].replace(" ", "_").replace("-", "_")
__version__ = package["version"]


# Module imports trigger a dash.development import, need to check this first
if not hasattr(_dash, "__plotly_dash") and not hasattr(_dash, "development"):
    print(
        "Dash was not successfully imported. Make sure you don't have a file "
        "named \n'dash.py' in your current directory.",
        file=_sys.stderr,
    )
    _sys.exit(1)

_current_path = _os.path.dirname(_os.path.abspath(__file__))


_this_module = "dash_html_components"

_js_dist = [
    {
        "relative_package_path": "html/{}.min.js".format(_this_module),
        "external_url": (
            "https://unpkg.com/dash-html-components@{}"
            "/dash_html_components/dash_html_components.min.js"
        ).format(__version__),
        "namespace": "dash",
    },
    {
        "relative_package_path": "html/{}.min.js.map".format(_this_module),
        "external_url": (
            "https://unpkg.com/dash-html-components@{}"
            "/dash_html_components/dash_html_components.min.js.map"
        ).format(__version__),
        "namespace": "dash",
        "dynamic": True,
    },
]

_css_dist = []


for _component in __all__:
    setattr(locals()[_component], "_js_dist", _js_dist)
    setattr(locals()[_component], "_css_dist", _css_dist)



File: components/dash-table/__init__.py




File: components/dash-table/setup.py

from setuptools import setup
import json

with open("package.json") as f:
    package = json.load(f)

package_name = str(package["name"].replace(" ", "_").replace("-", "_"))

setup(
    name=package_name,
    version=package["version"],
    author=package["author"],
    packages=[package_name],
    include_package_data=True,
    license=package["license"],
    description=package["description"] if "description" in package else package_name,
    install_requires=[],
)



File: components/dash-table/dash_table_base/FormatTemplate.py

from .Format import Format, Group, Scheme, Sign, Symbol


def money(decimals, sign=Sign.default):
    return Format(
        group=Group.yes,
        precision=decimals,
        scheme=Scheme.fixed,
        sign=sign,
        symbol=Symbol.yes,
    )


def percentage(decimals, rounded=False):
    if not isinstance(rounded, bool):
        raise TypeError("expected rounded to be a boolean")

    rounded = Scheme.percentage_rounded if rounded else Scheme.percentage
    return Format(scheme=rounded, precision=decimals)



File: components/dash-table/dash_table_base/__init__.py

import os as _os
import sys as _sys
import json

import dash as _dash

if not hasattr(_dash, "__plotly_dash") and not hasattr(_dash, "development"):
    print(
        "Dash was not successfully imported. "
        "Make sure you don't have a file "
        'named \n"dash.py" in your current directory.',
        file=_sys.stderr,
    )
    _sys.exit(1)

from ._imports_ import *  # noqa: E402, F401, F403
from ._imports_ import __all__ as _components
from . import Format  # noqa: F401, E402
from . import FormatTemplate  # noqa: F401, E402

__all__ = _components + ["Format", "FormatTemplate"]

_basepath = _os.path.dirname(__file__)
_filepath = _os.path.abspath(_os.path.join(_basepath, "package-info.json"))
with open(_filepath) as f:
    package = json.load(f)

package_name = package["name"].replace(" ", "_").replace("-", "_")
__version__ = package["version"]

_current_path = _os.path.dirname(_os.path.abspath(__file__))

_this_module = _sys.modules[__name__]


async_resources = ["export", "table", "highlight"]

_js_dist = []

_js_dist.extend(
    [
        {
            "relative_package_path": "dash_table/async-{}.js".format(async_resource),
            "external_url": (
                "https://unpkg.com/dash-table@{}" "/dash_table/async-{}.js"
            ).format(__version__, async_resource),
            "namespace": "dash",
            "async": True,
        }
        for async_resource in async_resources
    ]
)

_js_dist.extend(
    [
        {
            "relative_package_path": "dash_table/async-{}.js.map".format(
                async_resource
            ),
            "external_url": (
                "https://unpkg.com/dash-table@{}" "/dash_table/async-{}.js.map"
            ).format(__version__, async_resource),
            "namespace": "dash",
            "dynamic": True,
        }
        for async_resource in async_resources
    ]
)

_js_dist.extend(
    [
        {
            "relative_package_path": "dash_table/bundle.js",
            "external_url": (
                "https://unpkg.com/dash-table@{}/dash_table/bundle.js"
            ).format(__version__),
            "namespace": "dash",
        },
        {
            "relative_package_path": "dash_table/bundle.js.map",
            "external_url": (
                "https://unpkg.com/dash-table@{}/dash_table/bundle.js.map"
            ).format(__version__),
            "namespace": "dash",
            "dynamic": True,
        },
    ]
)

_css_dist = []


for _component in __all__:
    setattr(locals()[_component], "_js_dist", _js_dist)
    setattr(locals()[_component], "_css_dist", _css_dist)



File: components/dash-table/dash_table_base/Format.py

import collections


def get_named_tuple(name, dict):
    return collections.namedtuple(name, dict.keys())(*dict.values())


Align = get_named_tuple(
    "align",
    {"default": "", "left": "<", "right": ">", "center": "^", "right_sign": "="},
)

Group = get_named_tuple("group", {"no": "", "yes": ","})

Padding = get_named_tuple("padding", {"no": "", "yes": "0"})

Prefix = get_named_tuple(
    "prefix",
    {
        "yocto": 10**-24,
        "zepto": 10**-21,
        "atto": 10**-18,
        "femto": 10**-15,
        "pico": 10**-12,
        "nano": 10**-9,
        "micro": 10**-6,
        "milli": 10**-3,
        "none": None,
        "kilo": 10**3,
        "mega": 10**6,
        "giga": 10**9,
        "tera": 10**12,
        "peta": 10**15,
        "exa": 10**18,
        "zetta": 10**21,
        "yotta": 10**24,
    },
)

Scheme = get_named_tuple(
    "scheme",
    {
        "default": "",
        "decimal": "r",
        "decimal_integer": "d",
        "decimal_or_exponent": "g",
        "decimal_si_prefix": "s",
        "exponent": "e",
        "fixed": "f",
        "percentage": "%",
        "percentage_rounded": "p",
        "binary": "b",
        "octal": "o",
        "lower_case_hex": "x",
        "upper_case_hex": "X",
        "unicode": "c",
    },
)

Sign = get_named_tuple(
    "sign",
    {"default": "", "negative": "-", "positive": "+", "parantheses": "(", "space": " "},
)

Symbol = get_named_tuple(
    "symbol", {"no": "", "yes": "$", "binary": "#b", "octal": "#o", "hex": "#x"}
)

Trim = get_named_tuple("trim", {"no": "", "yes": "~"})


class Format:
    def __init__(self, **kwargs):
        self._locale = {}
        self._nully = ""
        self._prefix = Prefix.none
        self._specifier = {
            "align": Align.default,
            "fill": "",
            "group": Group.no,
            "width": "",
            "padding": Padding.no,
            "precision": "",
            "sign": Sign.default,
            "symbol": Symbol.no,
            "trim": Trim.no,
            "type": Scheme.default,
        }

        valid_methods = [
            m for m in dir(self.__class__) if m[0] != "_" and m != "to_plotly_json"
        ]

        for kw, val in kwargs.items():
            if kw not in valid_methods:
                raise TypeError(
                    "{0} is not a format method. Expected one of".format(kw),
                    str(list(valid_methods)),
                )

            getattr(self, kw)(val)

    def _validate_char(self, value):
        self._validate_string(value)

        if len(value) != 1:
            raise ValueError("expected value to a string of length one")

    def _validate_non_negative_integer_or_none(self, value):
        if value is None:
            return

        if not isinstance(value, int):
            raise TypeError("expected value to be an integer")

        if value < 0:
            raise ValueError("expected value to be non-negative", str(value))

    def _validate_named(self, value, named_values):
        if value not in named_values:
            raise TypeError("expected value to be one of", str(list(named_values)))

    def _validate_string(self, value):
        if not isinstance(value, (str, "".__class__)):
            raise TypeError("expected value to be a string")

    # Specifier
    def align(self, value):
        self._validate_named(value, Align)

        self._specifier["align"] = value
        return self

    def fill(self, value):
        self._validate_char(value)

        self._specifier["fill"] = value
        return self

    def group(self, value):
        if isinstance(value, bool):
            value = Group.yes if value else Group.no

        self._validate_named(value, Group)

        self._specifier["group"] = value
        return self

    def padding(self, value):
        if isinstance(value, bool):
            value = Padding.yes if value else Padding.no

        self._validate_named(value, Padding)

        self._specifier["padding"] = value
        return self

    def padding_width(self, value):
        self._validate_non_negative_integer_or_none(value)

        self._specifier["width"] = value if value is not None else ""
        return self

    def precision(self, value):
        self._validate_non_negative_integer_or_none(value)

        self._specifier["precision"] = ".{0}".format(value) if value is not None else ""
        return self

    def scheme(self, value):
        self._validate_named(value, Scheme)

        self._specifier["type"] = value
        return self

    def sign(self, value):
        self._validate_named(value, Sign)

        self._specifier["sign"] = value
        return self

    def symbol(self, value):
        self._validate_named(value, Symbol)

        self._specifier["symbol"] = value
        return self

    def trim(self, value):
        if isinstance(value, bool):
            value = Trim.yes if value else Trim.no

        self._validate_named(value, Trim)

        self._specifier["trim"] = value
        return self

    # Locale
    def symbol_prefix(self, value):
        self._validate_string(value)

        if "symbol" not in self._locale:
            self._locale["symbol"] = [value, ""]
        else:
            self._locale["symbol"][0] = value

        return self

    def symbol_suffix(self, value):
        self._validate_string(value)

        if "symbol" not in self._locale:
            self._locale["symbol"] = ["", value]
        else:
            self._locale["symbol"][1] = value

        return self

    def decimal_delimiter(self, value):
        self._validate_char(value)

        self._locale["decimal"] = value
        return self

    def group_delimiter(self, value):
        self._validate_char(value)

        self._locale["group"] = value
        return self

    def groups(self, groups):
        groups = (
            groups
            if isinstance(groups, list)
            else [groups]
            if isinstance(groups, int)
            else None
        )

        if not isinstance(groups, list):
            raise TypeError("expected groups to be an integer or a list of integers")
        if len(groups) == 0:
            raise ValueError(
                "expected groups to be an integer or a list of " "one or more integers"
            )

        for group in groups:
            if not isinstance(group, int):
                raise TypeError("expected entry to be an integer")

            if group <= 0:
                raise ValueError("expected entry to be a non-negative integer")

        self._locale["grouping"] = groups
        return self

    # Nully
    def nully(self, value):
        self._nully = value
        return self

    # Prefix
    def si_prefix(self, value):
        self._validate_named(value, Prefix)

        self._prefix = value
        return self

    def to_plotly_json(self):
        f = {}
        f["locale"] = self._locale.copy()
        f["nully"] = self._nully
        f["prefix"] = self._prefix
        aligned = self._specifier["align"] != Align.default
        f["specifier"] = "{}{}{}{}{}{}{}{}{}{}".format(
            self._specifier["fill"] if aligned else "",
            self._specifier["align"],
            self._specifier["sign"],
            self._specifier["symbol"],
            self._specifier["padding"],
            self._specifier["width"],
            self._specifier["group"],
            self._specifier["precision"],
            self._specifier["trim"],
            self._specifier["type"],
        )

        return f



File: dash/_dash_renderer.py

import os

__version__ = "1.21.0"

_available_react_versions = {"16.14.0", "18.2.0"}
_available_reactdom_versions = {"16.14.0", "18.2.0"}
_js_dist_dependencies = []  # to be set by _set_react_version


def _set_react_version(v_react, v_reactdom=None):
    if not v_reactdom:
        v_reactdom = v_react

    react_err = f"looking for one of {_available_react_versions}, found {v_react}"
    reactdom_err = (
        f"looking for one of {_available_reactdom_versions}, found {v_reactdom}"
    )
    assert v_react in _available_react_versions, react_err
    assert v_reactdom in _available_reactdom_versions, reactdom_err

    _js_dist_dependencies[:] = [
        {
            "external_url": {
                "prod": [
                    "https://unpkg.com/@babel/polyfill@7.12.1/dist/polyfill.min.js",
                    f"https://unpkg.com/react@{v_react}/umd/react.production.min.js",
                    f"https://unpkg.com/react-dom@{v_reactdom}/umd/react-dom.production.min.js",
                    "https://unpkg.com/prop-types@15.8.1/prop-types.min.js",
                ],
                "dev": [
                    "https://unpkg.com/@babel/polyfill@7.12.1/dist/polyfill.min.js",
                    f"https://unpkg.com/react@{v_react}/umd/react.development.js",
                    f"https://unpkg.com/react-dom@{v_reactdom}/umd/react-dom.development.js",
                    "https://unpkg.com/prop-types@15.8.1/prop-types.js",
                ],
            },
            "relative_package_path": {
                "prod": [
                    "deps/polyfill@7.12.1.min.js",
                    f"deps/react@{v_react}.min.js",
                    f"deps/react-dom@{v_reactdom}.min.js",
                    "deps/prop-types@15.8.1.min.js",
                ],
                "dev": [
                    "deps/polyfill@7.12.1.min.js",
                    f"deps/react@{v_react}.js",
                    f"deps/react-dom@{v_reactdom}.js",
                    "deps/prop-types@15.8.1.js",
                ],
            },
            "namespace": "dash",
        }
    ]


_env_react_version = os.getenv("REACT_VERSION")
if _env_react_version:
    _set_react_version(_env_react_version)
    print(f"EXPERIMENTAL: Using react version from env: {_env_react_version}")
else:
    _set_react_version("16.14.0", "16.14.0")

_js_dist = [
    {
        "relative_package_path": "dash-renderer/build/dash_renderer.min.js",
        "dev_package_path": "dash-renderer/build/dash_renderer.dev.js",
        "external_url": "https://unpkg.com/dash-renderer@1.21.0"
        "/build/dash_renderer.min.js",
        "namespace": "dash",
    },
    {
        "relative_package_path": "dash-renderer/build/dash_renderer.min.js.map",
        "dev_package_path": "dash-renderer/build/dash_renderer.dev.js.map",
        "namespace": "dash",
        "dynamic": True,
    },
]



File: dash/version.py

__version__ = "2.18.0"



File: dash/_configs.py

import os
import flask

# noinspection PyCompatibility
from . import exceptions
from ._utils import AttributeDict


def load_dash_env_vars():
    return AttributeDict(
        {
            var: os.getenv(var, os.getenv(var.lower()))
            for var in (
                "DASH_APP_NAME",
                "DASH_URL_BASE_PATHNAME",
                "DASH_ROUTES_PATHNAME_PREFIX",
                "DASH_REQUESTS_PATHNAME_PREFIX",
                "DASH_SUPPRESS_CALLBACK_EXCEPTIONS",
                "DASH_ASSETS_EXTERNAL_PATH",
                "DASH_INCLUDE_ASSETS_FILES",
                "DASH_COMPONENTS_CACHE_MAX_AGE",
                "DASH_INCLUDE_ASSETS_FILES",
                "DASH_SERVE_DEV_BUNDLES",
                "DASH_DEBUG",
                "DASH_UI",
                "DASH_PROPS_CHECK",
                "DASH_HOT_RELOAD",
                "DASH_HOT_RELOAD_INTERVAL",
                "DASH_HOT_RELOAD_WATCH_INTERVAL",
                "DASH_HOT_RELOAD_MAX_RETRY",
                "DASH_SILENCE_ROUTES_LOGGING",
                "DASH_PRUNE_ERRORS",
                "DASH_COMPRESS",
                "HOST",
                "PORT",
            )
        }
    )


DASH_ENV_VARS = load_dash_env_vars()  # used in tests


def get_combined_config(name, val, default=None):
    """Consolidate the config with priority from high to low provided init
    value > OS environ > default."""

    if val is not None:
        return val

    env = load_dash_env_vars().get(f"DASH_{name.upper()}")
    if env is None:
        return default

    return env.lower() == "true" if env.lower() in {"true", "false"} else env


def pathname_configs(
    url_base_pathname=None, routes_pathname_prefix=None, requests_pathname_prefix=None
):
    _pathname_config_error_message = """
    {} This is ambiguous.
    To fix this, set `routes_pathname_prefix` instead of `url_base_pathname`.

    Note that `requests_pathname_prefix` is the prefix for the AJAX calls that
    originate from the client (the web browser) and `routes_pathname_prefix` is
    the prefix for the API routes on the backend (this flask server).
    `url_base_pathname` will set `requests_pathname_prefix` and
    `routes_pathname_prefix` to the same value.
    If you need these to be different values then you should set
    `requests_pathname_prefix` and `routes_pathname_prefix`,
    not `url_base_pathname`.
    """
    url_base_pathname = get_combined_config("url_base_pathname", url_base_pathname)

    routes_pathname_prefix = get_combined_config(
        "routes_pathname_prefix", routes_pathname_prefix
    )

    requests_pathname_prefix = get_combined_config(
        "requests_pathname_prefix", requests_pathname_prefix
    )

    if url_base_pathname is not None and requests_pathname_prefix is not None:
        raise exceptions.InvalidConfig(
            _pathname_config_error_message.format(
                "You supplied `url_base_pathname` and `requests_pathname_prefix`."
            )
        )

    if url_base_pathname is not None and routes_pathname_prefix is not None:
        raise exceptions.InvalidConfig(
            _pathname_config_error_message.format(
                "You supplied `url_base_pathname` and `routes_pathname_prefix`."
            )
        )

    if url_base_pathname is not None and routes_pathname_prefix is None:
        routes_pathname_prefix = url_base_pathname
    elif routes_pathname_prefix is None:
        routes_pathname_prefix = "/"

    if not routes_pathname_prefix.startswith("/"):
        raise exceptions.InvalidConfig(
            "`routes_pathname_prefix` needs to start with `/`"
        )
    if not routes_pathname_prefix.endswith("/"):
        raise exceptions.InvalidConfig("`routes_pathname_prefix` needs to end with `/`")

    app_name = load_dash_env_vars().DASH_APP_NAME

    if not requests_pathname_prefix and app_name:
        requests_pathname_prefix = "/" + app_name + routes_pathname_prefix
    elif requests_pathname_prefix is None:
        requests_pathname_prefix = routes_pathname_prefix

    if not requests_pathname_prefix.startswith("/"):
        raise exceptions.InvalidConfig(
            "`requests_pathname_prefix` needs to start with `/`"
        )

    return url_base_pathname, routes_pathname_prefix, requests_pathname_prefix


def pages_folder_config(name, pages_folder, use_pages):
    if not pages_folder:
        return None
    is_custom_folder = str(pages_folder) != "pages"
    pages_folder_path = os.path.join(flask.helpers.get_root_path(name), pages_folder)
    if (use_pages or is_custom_folder) and not os.path.isdir(pages_folder_path):
        error_msg = f"""
        A folder called `{pages_folder}` does not exist. If a folder for pages is not
        required in your application, set `pages_folder=""`. For example:
        `app = Dash(__name__,  pages_folder="")`
        """
        raise exceptions.InvalidConfig(error_msg)
    return pages_folder_path



File: dash/_callback_context.py

import functools
import warnings
import json
import contextvars
import typing

import flask

from . import exceptions
from ._utils import AttributeDict, stringify_id


context_value = contextvars.ContextVar("callback_context")
context_value.set({})


def has_context(func):
    @functools.wraps(func)
    def assert_context(*args, **kwargs):
        if not context_value.get():
            raise exceptions.MissingCallbackContextException(
                f"dash.callback_context.{getattr(func, '__name__')} is only available from a callback!"
            )
        return func(*args, **kwargs)

    return assert_context


def _get_context_value():
    return context_value.get()


class FalsyList(list):
    def __bool__(self):
        # for Python 3
        return False

    def __nonzero__(self):
        # for Python 2
        return False


falsy_triggered = FalsyList([{"prop_id": ".", "value": None}])


# pylint: disable=no-init
class CallbackContext:
    @property
    @has_context
    def inputs(self):
        return getattr(_get_context_value(), "input_values", {})

    @property
    @has_context
    def states(self):
        return getattr(_get_context_value(), "state_values", {})

    @property
    @has_context
    def triggered(self):
        """
        Returns a list of all the Input props that changed and caused the callback to execute. It is empty when the
        callback is called on initial load, unless an Input prop got its value from another initial callback.
        Callbacks triggered by user actions typically have one item in triggered, unless the same action changes
        two props at once or the callback has several Input props that are all modified by another callback based on
        a single user action.

        Example:  To get the id of the component that triggered the callback:
        `component_id = ctx.triggered[0]['prop_id'].split('.')[0]`

        Example:  To detect initial call, empty triggered is not really empty, it's falsy so that you can use:
        `if ctx.triggered:`
        """
        # For backward compatibility: previously `triggered` always had a
        # value - to avoid breaking existing apps, add a dummy item but
        # make the list still look falsy. So `if ctx.triggered` will make it
        # look empty, but you can still do `triggered[0]["prop_id"].split(".")`
        return getattr(_get_context_value(), "triggered_inputs", []) or falsy_triggered

    @property
    @has_context
    def triggered_prop_ids(self):
        """
        Returns a dictionary of all the Input props that changed and caused the callback to execute. It is empty when
        the callback is called on initial load, unless an Input prop got its value from another initial callback.
        Callbacks triggered by user actions typically have one item in triggered, unless the same action changes
        two props at once or the callback has several Input props that are all modified by another callback based
        on a single user action.

        triggered_prop_ids (dict):
        - keys (str) : the triggered "prop_id" composed of "component_id.component_property"
        - values (str or dict): the id of the component that triggered the callback. Will be the dict id for pattern matching callbacks

        Example - regular callback
        {"btn-1.n_clicks": "btn-1"}

        Example - pattern matching callbacks:
        {'{"index":0,"type":"filter-dropdown"}.value': {"index":0,"type":"filter-dropdown"}}

        Example usage:
        `if "btn-1.n_clicks" in ctx.triggered_prop_ids:
            do_something()`
        """
        triggered = getattr(_get_context_value(), "triggered_inputs", [])
        ids = AttributeDict({})
        for item in triggered:
            component_id, _, _ = item["prop_id"].rpartition(".")
            ids[item["prop_id"]] = component_id
            if component_id.startswith("{"):
                ids[item["prop_id"]] = AttributeDict(json.loads(component_id))
        return ids

    @property
    @has_context
    def triggered_id(self):
        """
        Returns the component id (str or dict) of the Input component that triggered the callback.

        Note - use `triggered_prop_ids` if you need both the component id and the prop that triggered the callback or if
        multiple Inputs triggered the callback.

        Example usage:
        `if "btn-1" == ctx.triggered_id:
            do_something()`

        """
        component_id = None
        if self.triggered:
            prop_id = self.triggered_prop_ids.first()
            component_id = self.triggered_prop_ids[prop_id]
        return component_id

    @property
    @has_context
    def args_grouping(self):
        """
        args_grouping is a dict of the inputs used with flexible callback signatures. The keys are the variable names
        and the values are dictionaries containing:
        - “id”: (string or dict) the component id. If it’s a pattern matching id, it will be a dict.
        - “id_str”: (str) for pattern matching ids, it’s the stringified dict id with no white spaces.
        - “property”: (str) The component property used in the callback.
        - “value”: the value of the component property at the time the callback was fired.
        - “triggered”: (bool)Whether this input triggered the callback.

        Example usage:
        @app.callback(
            Output("container", "children"),
            inputs=dict(btn1=Input("btn-1", "n_clicks"), btn2=Input("btn-2", "n_clicks")),
        )
        def display(btn1, btn2):
            c = ctx.args_grouping
            if c.btn1.triggered:
                return f"Button 1 clicked {btn1} times"
            elif c.btn2.triggered:
                return f"Button 2 clicked {btn2} times"
            else:
               return "No clicks yet"

        """
        return getattr(_get_context_value(), "args_grouping", [])

    @property
    @has_context
    def outputs_grouping(self):
        return getattr(_get_context_value(), "outputs_grouping", [])

    @property
    @has_context
    def outputs_list(self):
        if self.using_outputs_grouping:
            warnings.warn(
                "outputs_list is deprecated, use outputs_grouping instead",
                DeprecationWarning,
            )

        return getattr(_get_context_value(), "outputs_list", [])

    @property
    @has_context
    def inputs_list(self):
        if self.using_args_grouping:
            warnings.warn(
                "inputs_list is deprecated, use args_grouping instead",
                DeprecationWarning,
            )

        return getattr(_get_context_value(), "inputs_list", [])

    @property
    @has_context
    def states_list(self):
        if self.using_args_grouping:
            warnings.warn(
                "states_list is deprecated, use args_grouping instead",
                DeprecationWarning,
            )
        return getattr(_get_context_value(), "states_list", [])

    @property
    @has_context
    def response(self):
        return getattr(_get_context_value(), "dash_response")

    @staticmethod
    @has_context
    def record_timing(name, duration=None, description=None):
        """Records timing information for a server resource.

        :param name: The name of the resource.
        :type name: string

        :param duration: The time in seconds to report. Internally, this
            is rounded to the nearest millisecond.
        :type duration: float or None

        :param description: A description of the resource.
        :type description: string or None
        """
        timing_information = getattr(flask.g, "timing_information", {})

        if name in timing_information:
            raise KeyError(f'Duplicate resource name "{name}" found.')

        timing_information[name] = {"dur": round(duration * 1000), "desc": description}

        setattr(flask.g, "timing_information", timing_information)

    @property
    @has_context
    def using_args_grouping(self):
        """
        Return True if this callback is using dictionary or nested groupings for
        Input/State dependencies, or if Input and State dependencies are interleaved
        """
        return getattr(_get_context_value(), "using_args_grouping", [])

    @property
    @has_context
    def using_outputs_grouping(self):
        """
        Return True if this callback is using dictionary or nested groupings for
        Output dependencies.
        """
        return getattr(_get_context_value(), "using_outputs_grouping", [])

    @property
    @has_context
    def timing_information(self):
        return getattr(flask.g, "timing_information", {})

    @has_context
    def set_props(self, component_id: typing.Union[str, dict], props: dict):
        ctx_value = _get_context_value()
        _id = stringify_id(component_id)
        existing = ctx_value.updated_props.get(_id)
        if existing is not None:
            ctx_value.updated_props[_id] = {**existing, **props}
        else:
            ctx_value.updated_props[_id] = props


callback_context = CallbackContext()


def set_props(component_id: typing.Union[str, dict], props: dict):
    """
    Set the props for a component not included in the callback outputs.
    """
    callback_context.set_props(component_id, props)



File: dash/_callback.py

import collections
import hashlib
from functools import wraps
from typing import Callable, Optional, Any

import flask

from .dependencies import (
    handle_callback_args,
    handle_grouped_callback_args,
    Output,
)
from .development.base_component import ComponentRegistry
from .exceptions import (
    InvalidCallbackReturnValue,
    PreventUpdate,
    WildcardInLongCallback,
    MissingLongCallbackManagerError,
    LongCallbackError,
    ImportedInsideCallbackError,
)

from ._grouping import (
    flatten_grouping,
    make_grouping_by_index,
    grouping_len,
)
from ._utils import (
    create_callback_id,
    stringify_id,
    to_json,
    coerce_to_list,
    AttributeDict,
    clean_property_name,
)

from . import _validate
from .long_callback.managers import BaseLongCallbackManager
from ._callback_context import context_value


def _invoke_callback(func, *args, **kwargs):  # used to mark the frame for the debugger
    return func(*args, **kwargs)  # %% callback invoked %%


class NoUpdate:
    def to_plotly_json(self):  # pylint: disable=no-self-use
        return {"_dash_no_update": "_dash_no_update"}

    @staticmethod
    def is_no_update(obj):
        return isinstance(obj, NoUpdate) or (
            isinstance(obj, dict) and obj == {"_dash_no_update": "_dash_no_update"}
        )


GLOBAL_CALLBACK_LIST = []
GLOBAL_CALLBACK_MAP = {}
GLOBAL_INLINE_SCRIPTS = []


# pylint: disable=too-many-locals
def callback(
    *_args,
    background=False,
    interval=1000,
    progress=None,
    progress_default=None,
    running=None,
    cancel=None,
    manager=None,
    cache_args_to_ignore=None,
    on_error: Optional[Callable[[Exception], Any]] = None,
    **_kwargs,
):
    """
    Normally used as a decorator, `@dash.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.

    `@dash.callback` is an alternative to `@app.callback` (where `app = dash.Dash()`)
    introduced in Dash 2.0.
    It allows you to register callbacks without defining or importing the `app`
    object. The call signature is identical and it can be used instead of `app.callback`
    in all cases.

    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` and unlike `app.callback` is not configurable at the app level.

    :Keyword Arguments:
        :param background:
            Mark the callback as a long callback to execute in a manager for
            callbacks that take a long time without locking up the Dash app
            or timing out.
        :param manager:
            A long callback manager instance. Currently, an instance of one of
            `DiskcacheManager` or `CeleryManager`.
            Defaults to the `background_callback_manager` instance provided to the
            `dash.Dash constructor`.
            - A diskcache manager (`DiskcacheManager`) that runs callback
              logic in a separate process and stores the results to disk using the
              diskcache library. This is the easiest backend to use for local
              development.
            - A Celery manager (`CeleryManager`) that runs callback logic
              in a celery worker and returns results to the Dash app through a Celery
              broker like RabbitMQ or Redis.
        :param running:
            A list of 3-element tuples. The first element of each tuple should be
            an `Output` dependency object referencing a property of a component in
            the app layout. The second element is the value that the property
            should be set to while the callback is running, and the third element
            is the value the property should be set to when the callback completes.
        :param cancel:
            A list of `Input` dependency objects that reference a property of a
            component in the app's layout.  When the value of this property changes
            while a callback is running, the callback is canceled.
            Note that the value of the property is not significant, any change in
            value will result in the cancellation of the running job (if any).
        :param progress:
            An `Output` dependency grouping that references properties of
            components in the app's layout. When provided, the decorated function
            will be called with an extra argument as the first argument to the
            function.  This argument, is a function handle that the decorated
            function should call in order to provide updates to the app on its
            current progress. This function accepts a single argument, which
            correspond to the grouping of properties specified in the provided
            `Output` dependency grouping
        :param progress_default:
            A grouping of values that should be assigned to the components
            specified by the `progress` argument when the callback is not in
            progress. If `progress_default` is not provided, all the dependency
            properties specified in `progress` will be set to `None` when the
            callback is not running.
        :param cache_args_to_ignore:
            Arguments to ignore when caching is enabled. If callback is configured
            with keyword arguments (Input/State provided in a dict),
            this should be a list of argument names as strings. Otherwise,
            this should be a list of argument indices as integers.
        :param interval:
            Time to wait between the long callback update requests.
        :param on_error:
            Function to call when the callback raises an exception. Receives the
            exception object as first argument. The callback_context can be used
            to access the original callback inputs, states and output.
    """

    long_spec = None

    config_prevent_initial_callbacks = _kwargs.pop(
        "config_prevent_initial_callbacks", False
    )
    callback_map = _kwargs.pop("callback_map", GLOBAL_CALLBACK_MAP)
    callback_list = _kwargs.pop("callback_list", GLOBAL_CALLBACK_LIST)

    if background:
        long_spec = {
            "interval": interval,
        }

        if manager:
            long_spec["manager"] = manager

        if progress:
            long_spec["progress"] = coerce_to_list(progress)
            validate_long_inputs(long_spec["progress"])

        if progress_default:
            long_spec["progressDefault"] = coerce_to_list(progress_default)

            if not len(long_spec["progress"]) == len(long_spec["progressDefault"]):
                raise Exception(
                    "Progress and progress default needs to be of same length"
                )

        if cancel:
            cancel_inputs = coerce_to_list(cancel)
            validate_long_inputs(cancel_inputs)

            long_spec["cancel"] = [c.to_dict() for c in cancel_inputs]
            long_spec["cancel_inputs"] = cancel_inputs

        if cache_args_to_ignore:
            long_spec["cache_args_to_ignore"] = cache_args_to_ignore

    return register_callback(
        callback_list,
        callback_map,
        config_prevent_initial_callbacks,
        *_args,
        **_kwargs,
        long=long_spec,
        manager=manager,
        running=running,
        on_error=on_error,
    )


def validate_long_inputs(deps):
    for dep in deps:
        if dep.has_wildcard():
            raise WildcardInLongCallback(
                f"""
                long callbacks does not support dependencies with
                pattern-matching ids
                    Received: {repr(dep)}\n"""
            )


def clientside_callback(clientside_function, *args, **kwargs):
    return register_clientside_callback(
        GLOBAL_CALLBACK_LIST,
        GLOBAL_CALLBACK_MAP,
        False,
        GLOBAL_INLINE_SCRIPTS,
        clientside_function,
        *args,
        **kwargs,
    )


# pylint: disable=too-many-arguments
def insert_callback(
    callback_list,
    callback_map,
    config_prevent_initial_callbacks,
    output,
    outputs_indices,
    inputs,
    state,
    inputs_state_indices,
    prevent_initial_call,
    long=None,
    manager=None,
    running=None,
    dynamic_creator: Optional[bool] = False,
    no_output=False,
):
    if prevent_initial_call is None:
        prevent_initial_call = config_prevent_initial_callbacks

    _validate.validate_duplicate_output(
        output, prevent_initial_call, config_prevent_initial_callbacks
    )

    callback_id = create_callback_id(output, inputs, no_output)
    callback_spec = {
        "output": callback_id,
        "inputs": [c.to_dict() for c in inputs],
        "state": [c.to_dict() for c in state],
        "clientside_function": None,
        # prevent_initial_call can be a string "initial_duplicates"
        # which should not prevent the initial call.
        "prevent_initial_call": prevent_initial_call is True,
        "long": long
        and {
            "interval": long["interval"],
        },
        "dynamic_creator": dynamic_creator,
        "no_output": no_output,
    }
    if running:
        callback_spec["running"] = running

    callback_map[callback_id] = {
        "inputs": callback_spec["inputs"],
        "state": callback_spec["state"],
        "outputs_indices": outputs_indices,
        "inputs_state_indices": inputs_state_indices,
        "long": long,
        "output": output,
        "raw_inputs": inputs,
        "manager": manager,
        "allow_dynamic_callbacks": dynamic_creator,
        "no_output": no_output,
    }
    callback_list.append(callback_spec)

    return callback_id


def _set_side_update(ctx, response) -> bool:
    side_update = dict(ctx.updated_props)
    if len(side_update) > 0:
        response["sideUpdate"] = side_update
        return True
    return False


# pylint: disable=too-many-branches,too-many-statements
def register_callback(
    callback_list, callback_map, config_prevent_initial_callbacks, *_args, **_kwargs
):
    (
        output,
        flat_inputs,
        flat_state,
        inputs_state_indices,
        prevent_initial_call,
    ) = handle_grouped_callback_args(_args, _kwargs)
    if isinstance(output, Output):
        # Insert callback with scalar (non-multi) Output
        insert_output = output
        multi = False
        has_output = True
    else:
        # Insert callback as multi Output
        insert_output = flatten_grouping(output)
        multi = True
        has_output = len(output) > 0

    long = _kwargs.get("long")
    manager = _kwargs.get("manager")
    running = _kwargs.get("running")
    on_error = _kwargs.get("on_error")
    if running is not None:
        if not isinstance(running[0], (list, tuple)):
            running = [running]
        running = {
            "running": {str(r[0]): r[1] for r in running},
            "runningOff": {str(r[0]): r[2] for r in running},
        }
    allow_dynamic_callbacks = _kwargs.get("_allow_dynamic_callbacks")

    output_indices = make_grouping_by_index(output, list(range(grouping_len(output))))
    callback_id = insert_callback(
        callback_list,
        callback_map,
        config_prevent_initial_callbacks,
        insert_output,
        output_indices,
        flat_inputs,
        flat_state,
        inputs_state_indices,
        prevent_initial_call,
        long=long,
        manager=manager,
        dynamic_creator=allow_dynamic_callbacks,
        running=running,
        no_output=not has_output,
    )

    # pylint: disable=too-many-locals
    def wrap_func(func):

        if long is not None:
            long_key = BaseLongCallbackManager.register_func(
                func,
                long.get("progress") is not None,
                callback_id,
            )

        @wraps(func)
        def add_context(*args, **kwargs):
            output_spec = kwargs.pop("outputs_list")
            app_callback_manager = kwargs.pop("long_callback_manager", None)

            callback_ctx = kwargs.pop(
                "callback_context", AttributeDict({"updated_props": {}})
            )
            app = kwargs.pop("app", None)
            callback_manager = long and long.get("manager", app_callback_manager)
            error_handler = on_error or kwargs.pop("app_on_error", None)
            original_packages = set(ComponentRegistry.registry)

            if has_output:
                _validate.validate_output_spec(insert_output, output_spec, Output)

            context_value.set(callback_ctx)

            func_args, func_kwargs = _validate.validate_and_group_input_args(
                args, inputs_state_indices
            )

            response: dict = {"multi": True}
            has_update = False

            if long is not None:
                if not callback_manager:
                    raise MissingLongCallbackManagerError(
                        "Running `long` callbacks requires a manager to be installed.\n"
                        "Available managers:\n"
                        "- Diskcache (`pip install dash[diskcache]`) to run callbacks in a separate Process"
                        " and store results on the local filesystem.\n"
                        "- Celery (`pip install dash[celery]`) to run callbacks in a celery worker"
                        " and store results on redis.\n"
                    )

                progress_outputs = long.get("progress")
                cache_key = flask.request.args.get("cacheKey")
                job_id = flask.request.args.get("job")
                old_job = flask.request.args.getlist("oldJob")

                current_key = callback_manager.build_cache_key(
                    func,
                    # Inputs provided as dict is kwargs.
                    func_args if func_args else func_kwargs,
                    long.get("cache_args_to_ignore", []),
                )

                if old_job:
                    for job in old_job:
                        callback_manager.terminate_job(job)

                if not cache_key:
                    cache_key = current_key

                    job_fn = callback_manager.func_registry.get(long_key)

                    job = callback_manager.call_job_fn(
                        cache_key,
                        job_fn,
                        func_args if func_args else func_kwargs,
                        AttributeDict(
                            args_grouping=callback_ctx.args_grouping,
                            using_args_grouping=callback_ctx.using_args_grouping,
                            outputs_grouping=callback_ctx.outputs_grouping,
                            using_outputs_grouping=callback_ctx.using_outputs_grouping,
                            inputs_list=callback_ctx.inputs_list,
                            states_list=callback_ctx.states_list,
                            outputs_list=callback_ctx.outputs_list,
                            input_values=callback_ctx.input_values,
                            state_values=callback_ctx.state_values,
                            triggered_inputs=callback_ctx.triggered_inputs,
                            ignore_register_page=True,
                        ),
                    )

                    data = {
                        "cacheKey": cache_key,
                        "job": job,
                    }

                    cancel = long.get("cancel")
                    if cancel:
                        data["cancel"] = cancel

                    progress_default = long.get("progressDefault")
                    if progress_default:
                        data["progressDefault"] = {
                            str(o): x
                            for o, x in zip(progress_outputs, progress_default)
                        }
                    return to_json(data)
                if progress_outputs:
                    # Get the progress before the result as it would be erased after the results.
                    progress = callback_manager.get_progress(cache_key)
                    if progress:
                        response["progress"] = {
                            str(x): progress[i] for i, x in enumerate(progress_outputs)
                        }

                output_value = callback_manager.get_result(cache_key, job_id)
                # Must get job_running after get_result since get_results terminates it.
                job_running = callback_manager.job_running(job_id)
                if not job_running and output_value is callback_manager.UNDEFINED:
                    # Job canceled -> no output to close the loop.
                    output_value = NoUpdate()

                elif (
                    isinstance(output_value, dict)
                    and "long_callback_error" in output_value
                ):
                    error = output_value.get("long_callback_error", {})
                    exc = LongCallbackError(
                        f"An error occurred inside a long callback: {error['msg']}\n{error['tb']}"
                    )
                    if error_handler:
                        output_value = error_handler(exc)

                        if output_value is None:
                            output_value = NoUpdate()
                        # set_props from the error handler uses the original ctx
                        # instead of manager.get_updated_props since it runs in the
                        # request process.
                        has_update = (
                            _set_side_update(callback_ctx, response)
                            or output_value is not None
                        )
                    else:
                        raise exc

                if job_running and output_value is not callback_manager.UNDEFINED:
                    # cached results.
                    callback_manager.terminate_job(job_id)

                if multi and isinstance(output_value, (list, tuple)):
                    output_value = [
                        NoUpdate() if NoUpdate.is_no_update(r) else r
                        for r in output_value
                    ]
                updated_props = callback_manager.get_updated_props(cache_key)
                if len(updated_props) > 0:
                    response["sideUpdate"] = updated_props
                    has_update = True

                if output_value is callback_manager.UNDEFINED:
                    return to_json(response)
            else:
                try:
                    output_value = _invoke_callback(func, *func_args, **func_kwargs)
                except PreventUpdate as err:
                    raise err
                except Exception as err:  # pylint: disable=broad-exception-caught
                    if error_handler:
                        output_value = error_handler(err)

                        # If the error returns nothing, automatically puts NoUpdate for response.
                        if output_value is None:
                            output_value = NoUpdate()
                    else:
                        raise err

            component_ids = collections.defaultdict(dict)

            if has_output:
                if not multi:
                    output_value, output_spec = [output_value], [output_spec]
                    flat_output_values = output_value
                else:
                    if isinstance(output_value, (list, tuple)):
                        # For multi-output, allow top-level collection to be
                        # list or tuple
                        output_value = list(output_value)

                    if NoUpdate.is_no_update(output_value):
                        flat_output_values = [output_value]
                    else:
                        # Flatten grouping and validate grouping structure
                        flat_output_values = flatten_grouping(output_value, output)

                if not NoUpdate.is_no_update(output_value):
                    _validate.validate_multi_return(
                        output_spec, flat_output_values, callback_id
                    )

                for val, spec in zip(flat_output_values, output_spec):
                    if NoUpdate.is_no_update(val):
                        continue
                    for vali, speci in (
                        zip(val, spec) if isinstance(spec, list) else [[val, spec]]
                    ):
                        if not NoUpdate.is_no_update(vali):
                            has_update = True
                            id_str = stringify_id(speci["id"])
                            prop = clean_property_name(speci["property"])
                            component_ids[id_str][prop] = vali
            else:
                if output_value is not None:
                    raise InvalidCallbackReturnValue(
                        f"No-output callback received return value: {output_value}"
                    )
                output_value = []
                flat_output_values = []

            if not long:
                has_update = _set_side_update(callback_ctx, response) or has_update

            if not has_update:
                raise PreventUpdate

            response["response"] = component_ids

            if len(ComponentRegistry.registry) != len(original_packages):
                diff_packages = list(
                    set(ComponentRegistry.registry).difference(original_packages)
                )
                if not allow_dynamic_callbacks:
                    raise ImportedInsideCallbackError(
                        f"Component librar{'y' if len(diff_packages) == 1 else 'ies'} was imported during callback.\n"
                        "You can set `_allow_dynamic_callbacks` to allow for development purpose only."
                    )
                dist = app.get_dist(diff_packages)
                response["dist"] = dist

            try:
                jsonResponse = to_json(response)
            except TypeError:
                _validate.fail_callback_output(output_value, output)

            return jsonResponse

        callback_map[callback_id]["callback"] = add_context

        return func

    return wrap_func


_inline_clientside_template = """
(function() {{
    var clientside = window.dash_clientside = window.dash_clientside || {{}};
    var ns = clientside["{namespace}"] = clientside["{namespace}"] || {{}};
    ns["{function_name}"] = {clientside_function};
}})();
"""


def register_clientside_callback(
    callback_list,
    callback_map,
    config_prevent_initial_callbacks,
    inline_scripts,
    clientside_function,
    *args,
    **kwargs,
):
    output, inputs, state, prevent_initial_call = handle_callback_args(args, kwargs)
    no_output = isinstance(output, (list,)) and len(output) == 0
    insert_callback(
        callback_list,
        callback_map,
        config_prevent_initial_callbacks,
        output,
        None,
        inputs,
        state,
        None,
        prevent_initial_call,
        no_output=no_output,
    )

    # If JS source is explicitly given, create a namespace and function
    # name, then inject the code.
    if isinstance(clientside_function, str):
        namespace = "_dashprivate_clientside_funcs"
        # Create a hash from the function, it will be the same always
        function_name = hashlib.sha256(clientside_function.encode("utf-8")).hexdigest()

        inline_scripts.append(
            _inline_clientside_template.format(
                namespace=namespace,
                function_name=function_name,
                clientside_function=clientside_function,
            )
        )

    # Callback is stored in an external asset.
    else:
        namespace = clientside_function.namespace
        function_name = clientside_function.function_name

    callback_list[-1]["clientside_function"] = {
        "namespace": namespace,
        "function_name": function_name,
    }



File: dash/fingerprint.py

import re

cache_regex = re.compile(r"^v[\w-]+m[0-9a-fA-F]+$")
version_clean = re.compile(r"[^\w-]")


def build_fingerprint(path, version, hash_value):
    path_parts = path.split("/")
    filename, extension = path_parts[-1].split(".", 1)
    file_path = "/".join(path_parts[:-1] + [filename])
    v_str = re.sub(version_clean, "_", str(version))

    return f"{file_path}.v{v_str}m{hash_value}.{extension}"


def check_fingerprint(path):
    path_parts = path.split("/")
    name_parts = path_parts[-1].split(".")

    # Check if the resource has a fingerprint
    if len(name_parts) > 2 and cache_regex.match(name_parts[1]):
        original_name = ".".join([name_parts[0]] + name_parts[2:])
        return "/".join(path_parts[:-1] + [original_name]), True

    return path, False



File: dash/_get_paths.py

from ._utils import AttributeDict
from . import exceptions

CONFIG = AttributeDict()


def get_asset_url(path):
    return app_get_asset_url(CONFIG, path)


def app_get_asset_url(config, path):
    if config.assets_external_path:
        prefix = config.assets_external_path
    else:
        prefix = config.requests_pathname_prefix
    return "/".join(
        [
            # Only take the first part of the pathname
            prefix.rstrip("/"),
            config.assets_url_path.lstrip("/"),
            path,
        ]
    )


def get_relative_path(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
    `dash.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
    `dash.get_relative_path('/assets/logo.png')`

    Use this function with `dash.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')
    ])
    @dash.callback(Output('content', 'children'), [Input('url', 'pathname')])
    def display_content(path):
        page_name = dash.strip_relative_path(path)
        if not page_name:  # None or ''
            return html.Div([
                dcc.Link(href=dash.get_relative_path('/page-1')),
                dcc.Link(href=dash.get_relative_path('/page-2')),
            ])
        elif page_name == 'page-1':
            return chapters.page_1
        if page_name == "page-2":
            return chapters.page_2
    ```
    """
    return app_get_relative_path(CONFIG.requests_pathname_prefix, path)


def app_get_relative_path(requests_pathname, path):
    if requests_pathname == "/" and path == "":
        return "/"
    if requests_pathname != "/" and path == "":
        return requests_pathname
    if not path.startswith("/"):
        raise exceptions.UnsupportedRelativePath(
            f"""
            Paths that aren't prefixed with a leading / are not supported.
            You supplied: {path}
            """
        )
    return "/".join([requests_pathname.rstrip("/"), path.lstrip("/")])


def strip_relative_path(path):
    """
    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')
    ])
    @dash.callback(Output('content', 'children'), [Input('url', 'pathname')])
    def display_content(path):
        page_name = dash.strip_relative_path(path)
        if not page_name:  # None or ''
            return html.Div([
                dcc.Link(href=dash.get_relative_path('/page-1')),
                dcc.Link(href=dash.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
    `dash.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, `dash.strip_relative_path('/my-dash-app/page-2')`
    will return `'page-2'`

    For nested URLs, slashes are still included:
    `dash.strip_relative_path('/page-1/sub-page-1/')` will return
    `page-1/sub-page-1`
    ```
    """
    return app_strip_relative_path(CONFIG.requests_pathname_prefix, path)


def app_strip_relative_path(requests_pathname, path):
    if path is None:
        return None
    if (
        requests_pathname != "/" and not path.startswith(requests_pathname.rstrip("/"))
    ) or (requests_pathname == "/" and not path.startswith("/")):
        raise exceptions.UnsupportedRelativePath(
            f"""
            Paths that aren't prefixed with requests_pathname_prefix are not supported.
            You supplied: {path} and requests_pathname_prefix was {requests_pathname}
            """
        )
    if requests_pathname != "/" and path.startswith(requests_pathname.rstrip("/")):
        path = path.replace(
            # handle the case where the path might be `/my-dash-app`
            # but the requests_pathname_prefix is `/my-dash-app/`
            requests_pathname.rstrip("/"),
            "",
            1,
        )
    return path.strip("/")



File: dash/__init__.py

# pylint: disable=C0413
# __plotly_dash is for the "make sure you don't have a dash.py" check
# must come before any other imports.
__plotly_dash = True
from .dependencies import (  # noqa: F401,E402
    Input,  # noqa: F401,E402
    Output,  # noqa: F401,E402,
    State,  # noqa: F401,E402
    ClientsideFunction,  # noqa: F401,E402
    MATCH,  # noqa: F401,E402
    ALL,  # noqa: F401,E402
    ALLSMALLER,  # noqa: F401,E402
)  # noqa: F401,E402
from . import development  # noqa: F401,E402
from . import exceptions  # noqa: F401,E402
from . import resources  # noqa: F401,E402
from . import dcc  # noqa: F401,E402
from . import html  # noqa: F401,E402
from . import dash_table  # noqa: F401,E402
from .version import __version__  # noqa: F401,E402
from ._callback_context import callback_context, set_props  # noqa: F401,E402
from ._callback import callback, clientside_callback  # noqa: F401,E402
from ._get_app import get_app  # noqa: F401,E402
from ._get_paths import (  # noqa: F401,E402
    get_asset_url,
    get_relative_path,
    strip_relative_path,
)
from .long_callback import (  # noqa: F401,E402
    CeleryManager,
    DiskcacheManager,
)


from ._pages import register_page, PAGE_REGISTRY as page_registry  # noqa: F401,E402
from .dash import (  # noqa: F401,E402
    Dash,
    no_update,
    page_container,
)
from ._patch import Patch  # noqa: F401,E402
from ._jupyter import jupyter_dash  # noqa: F401,E402

ctx = callback_context


def _jupyter_nbextension_paths():
    return [
        {
            "section": "notebook",
            "src": "nbextension",
            "dest": "dash",
            "require": "dash/main",
        }
    ]



File: dash/_patch.py

def _operation(name, location, **kwargs):
    return {"operation": name, "location": location, "params": dict(**kwargs)}


_noop = object()


def validate_slice(obj):
    if isinstance(obj, slice):
        raise TypeError("a slice is not a valid index for patch")


class Patch:
    """
    Patch a callback output value

    Act like a proxy of the output prop value on the frontend.

    Supported prop types: Dictionaries and lists.
    """

    def __init__(self, location=None, parent=None):
        if location is not None:
            self._location = location
        else:
            # pylint: disable=consider-using-ternary
            self._location = (parent and parent._location) or []
        if parent is not None:
            self._operations = parent._operations
        else:
            self._operations = []

    def __getstate__(self):
        return vars(self)

    def __setstate__(self, state):
        vars(self).update(state)

    def __getitem__(self, item) -> "Patch":
        validate_slice(item)
        return Patch(location=self._location + [item], parent=self)

    def __getattr__(self, item) -> "Patch":
        if item == "tolist":
            # to_json fix
            raise AttributeError
        if item == "_location":
            return self._location  # type: ignore
        if item == "_operations":
            return self._operations  # type: ignore
        return self.__getitem__(item)

    def __setattr__(self, key, value):
        if key in ("_location", "_operations"):
            self.__dict__[key] = value
        else:
            self.__setitem__(key, value)

    def __delattr__(self, item):
        self.__delitem__(item)

    def __setitem__(self, key, value):
        validate_slice(key)
        if value is _noop:
            # The += set themselves.
            return
        self._operations.append(
            _operation(
                "Assign",
                self._location + [key],
                value=value,
            )
        )

    def __delitem__(self, key):
        validate_slice(key)
        self._operations.append(_operation("Delete", self._location + [key]))

    def __iadd__(self, other):
        if isinstance(other, (list, tuple)):
            self.extend(other)
        else:
            self._operations.append(_operation("Add", self._location, value=other))
        if not self._location:
            return self
        return _noop

    def __isub__(self, other):
        self._operations.append(_operation("Sub", self._location, value=other))
        if not self._location:
            return self
        return _noop

    def __imul__(self, other):
        self._operations.append(_operation("Mul", self._location, value=other))
        if not self._location:
            return self
        return _noop

    def __itruediv__(self, other):
        self._operations.append(_operation("Div", self._location, value=other))
        if not self._location:
            return self
        return _noop

    def __ior__(self, other):
        self.update(E=other)
        if not self._location:
            return self
        return _noop

    def __iter__(self):
        raise TypeError("Patch objects are write-only, you cannot iterate them.")

    def __repr__(self):
        return f"<write-only dash.Patch object at {self._location}>"

    def append(self, item):
        """Add the item to the end of a list"""
        self._operations.append(_operation("Append", self._location, value=item))

    def prepend(self, item):
        """Add the item to the start of a list"""
        self._operations.append(_operation("Prepend", self._location, value=item))

    def insert(self, index, item):
        """Add the item at the index of a list"""
        self._operations.append(
            _operation("Insert", self._location, value=item, index=index)
        )

    def clear(self):
        """Remove all items in a list"""
        self._operations.append(_operation("Clear", self._location))

    def reverse(self):
        """Reversal of the order of items in a list"""
        self._operations.append(_operation("Reverse", self._location))

    def extend(self, item):
        """Add all the items to the end of a list"""
        if not isinstance(item, (list, tuple)):
            raise TypeError(f"{item} should be a list or tuple")
        self._operations.append(_operation("Extend", self._location, value=item))

    def remove(self, item):
        """filter the item out of a list on the frontend"""
        self._operations.append(_operation("Remove", self._location, value=item))

    def update(self, E=None, **F):
        """Merge a dict or keyword arguments with another dictionary"""
        value = E or {}
        value.update(F)
        self._operations.append(_operation("Merge", self._location, value=value))

    # pylint: disable=no-self-use
    def sort(self):
        raise KeyError(
            "sort is reserved for future use, use brackets to access this key on your object"
        )

    def to_plotly_json(self):
        return {
            "__dash_patch_update": "__dash_patch_update",
            "operations": self._operations,
        }



File: dash/types.py

from typing_extensions import TypedDict, NotRequired


class RendererHooks(TypedDict):
    layout_pre: NotRequired[str]
    layout_post: NotRequired[str]
    request_pre: NotRequired[str]
    request_post: NotRequired[str]
    callback_resolved: NotRequired[str]
    request_refresh_jwt: NotRequired[str]



File: dash/_grouping.py

"""
This module contains a collection of utility function for dealing with property
groupings.

Terminology:

For the purpose of grouping and ungrouping, tuples/lists and dictionaries are considered
"composite values" and all other values are considered "scalar values".

A "grouping value" is either composite or scalar.

A "schema" is a grouping value that can be used to encode an expected grouping
structure

"""
from dash.exceptions import InvalidCallbackReturnValue
from ._utils import AttributeDict, stringify_id


def flatten_grouping(grouping, schema=None):
    """
    Convert a grouping value to a list of scalar values

    :param grouping: grouping value to flatten
    :param schema: If provided, a grouping value representing the expected structure of
        the input grouping value. If not provided, the grouping value is its own schema.
        A schema is required in order to be able treat tuples and dicts in the input
        grouping as scalar values.

    :return: list of the scalar values in the input grouping
    """
    if schema is None:
        schema = grouping
    else:
        validate_grouping(grouping, schema)

    if isinstance(schema, (tuple, list)):
        return [
            g
            for group_el, schema_el in zip(grouping, schema)
            for g in flatten_grouping(group_el, schema_el)
        ]

    if isinstance(schema, dict):
        return [g for k in schema for g in flatten_grouping(grouping[k], schema[k])]

    return [grouping]


def grouping_len(grouping):
    """
    Get the length of a grouping. The length equal to the number of scalar values
    contained in the grouping, which is equivalent to the length of the list that would
    result from calling flatten_grouping on the grouping value.

    :param grouping: The grouping value to calculate the length of
    :return: non-negative integer
    """
    if isinstance(grouping, (tuple, list)):
        return sum([grouping_len(group_el) for group_el in grouping])

    if isinstance(grouping, dict):
        return sum([grouping_len(group_el) for group_el in grouping.values()])

    return 1


def make_grouping_by_index(schema, flat_values):
    """
    Make a grouping like the provided grouping schema, with scalar values drawn from a
    flat list by index.

    Note: Scalar values in schema are not used

    :param schema: Grouping value encoding the structure of the grouping to return
    :param flat_values: List of values with length matching the grouping_len of schema.
        Elements of flat_values will become the scalar values in the resulting grouping
    """

    def _perform_make_grouping_like(value, next_values):
        if isinstance(value, (tuple, list)):
            return list(
                _perform_make_grouping_like(el, next_values)
                for i, el in enumerate(value)
            )

        if isinstance(value, dict):
            return {
                k: _perform_make_grouping_like(v, next_values)
                for i, (k, v) in enumerate(value.items())
            }

        return next_values.pop(0)

    if not isinstance(flat_values, list):
        raise ValueError(
            "The flat_values argument must be a list. "
            f"Received value of type {type(flat_values)}"
        )

    expected_length = len(flatten_grouping(schema))
    if len(flat_values) != expected_length:
        raise ValueError(
            f"The specified grouping pattern requires {expected_length} "
            f"elements but received {len(flat_values)}\n"
            f"    Grouping pattern: {repr(schema)}\n"
            f"    Values: {flat_values}"
        )

    return _perform_make_grouping_like(schema, list(flat_values))


def map_grouping(fn, grouping):
    """
    Map a function over all of the scalar values of a grouping, maintaining the
    grouping structure

    :param fn: Single-argument function that accepts and returns scalar grouping values
    :param grouping: The grouping to map the function over
    :return: A new grouping with the same structure as input grouping with scalar
        values updated by the input function.
    """
    if isinstance(grouping, (tuple, list)):
        return [map_grouping(fn, g) for g in grouping]

    if isinstance(grouping, dict):
        return AttributeDict({k: map_grouping(fn, g) for k, g in grouping.items()})

    return fn(grouping)


def make_grouping_by_key(schema, source, default=None):
    """
    Create a grouping from a schema by using the schema's scalar values to look up
    items in the provided source object.

    :param schema: A grouping of potential keys in source
    :param source: Dict-like object to use to look up scalar grouping value using
        scalar grouping values as keys
    :param default: Default scalar value to use if grouping scalar key is not present
        in source
    :return: grouping
    """
    return map_grouping(lambda s: source.get(s, default), schema)


class SchemaTypeValidationError(InvalidCallbackReturnValue):
    def __init__(self, value, full_schema, path, expected_type):
        super().__init__(
            msg=f"""
                Schema: {full_schema}
                Path: {repr(path)}
                Expected type: {expected_type}
                Received value of type {type(value)}:
                    {repr(value)}
                """
        )

    @classmethod
    def check(cls, value, full_schema, path, expected_type):
        if not isinstance(value, expected_type):
            raise SchemaTypeValidationError(value, full_schema, path, expected_type)


class SchemaLengthValidationError(InvalidCallbackReturnValue):
    def __init__(self, value, full_schema, path, expected_len):
        super().__init__(
            msg=f"""
                Schema: {full_schema}
                Path: {repr(path)}
                Expected length: {expected_len}
                Received value of length {len(value)}:
                    {repr(value)}
                """
        )

    @classmethod
    def check(cls, value, full_schema, path, expected_len):
        if len(value) != expected_len:
            raise SchemaLengthValidationError(value, full_schema, path, expected_len)


class SchemaKeysValidationError(InvalidCallbackReturnValue):
    def __init__(self, value, full_schema, path, expected_keys):
        super().__init__(
            msg=f"""
                Schema: {full_schema}
                Path: {repr(path)}
                Expected keys: {expected_keys}
                Received value with keys {set(value.keys())}:
                    {repr(value)}
                """
        )

    @classmethod
    def check(cls, value, full_schema, path, expected_keys):
        if set(value.keys()) != set(expected_keys):
            raise SchemaKeysValidationError(value, full_schema, path, expected_keys)


def validate_grouping(grouping, schema, full_schema=None, path=()):
    """
    Validate that the provided grouping conforms to the provided schema.
    If not, raise a SchemaValidationError
    """
    if full_schema is None:
        full_schema = schema

    if isinstance(schema, (tuple, list)):
        SchemaTypeValidationError.check(grouping, full_schema, path, (tuple, list))
        SchemaLengthValidationError.check(grouping, full_schema, path, len(schema))

        for i, (g, s) in enumerate(zip(grouping, schema)):
            validate_grouping(g, s, full_schema=full_schema, path=path + (i,))
    elif isinstance(schema, dict):
        SchemaTypeValidationError.check(grouping, full_schema, path, dict)
        SchemaKeysValidationError.check(grouping, full_schema, path, set(schema))

        for k in schema:
            validate_grouping(
                grouping[k], schema[k], full_schema=full_schema, path=path + (k,)
            )
    else:
        pass


def update_args_group(g, triggered):
    if isinstance(g, dict):
        str_id = stringify_id(g["id"])
        prop_id = f"{str_id}.{g['property']}"

        new_values = {
            "value": g.get("value"),
            "str_id": str_id,
            "triggered": prop_id in triggered,
            "id": AttributeDict(g["id"]) if isinstance(g["id"], dict) else g["id"],
        }
        g.update(new_values)



File: dash/dash.py

import functools
import os
import sys
import collections
import importlib
import warnings
from contextvars import copy_context
from importlib.machinery import ModuleSpec
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, List

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

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

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

        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 _do_skip(error):
        from dash._callback import (  # pylint: disable=import-outside-toplevel
            _invoke_callback,
        )

        tb = error.__traceback__
        while tb.tb_next is not None:
            if tb.tb_frame.f_code is _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


# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-arguments, too-many-locals
class Dash:
    """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_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 long_callback_manager: Deprecated, use ``background_callback_manager``
        instead.

    :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.
    """

    _plotlyjs_url: str
    STARTUP_ROUTES: list = []

    server: flask.Flask

    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_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[List[Dict[str, Any]]] = None,
        index_string: str = _default_index,
        external_scripts: Optional[List[Union[str, Dict[str, Any]]]] = None,
        external_stylesheets: Optional[List[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[List[str]] = None,
        plugins: Optional[list] = None,
        title: str = "Dash",
        update_title: str = "Updating...",
        long_callback_manager: Optional[
            Any
        ] = None,  # Type should be specified if possible
        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,
        **obsolete,
    ):
        _validate.check_obsolete(obsolete)

        caller_name = None if name 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:
                name = getattr(server, "name", caller_name)
        elif isinstance(server, bool):
            name = name if name else caller_name
            self.server = flask.Flask(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=name,
            assets_folder=os.path.join(
                flask.helpers.get_root_path(name), assets_folder
            ),  # type: ignore
            assets_url_path=assets_url_path,
            assets_ignore=assets_ignore,
            assets_external_path=get_combined_config(
                "assets_external_path", assets_external_path, ""
            ),
            pages_folder=pages_folder_config(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._setup_dev_tools()
        self._hot_reload = AttributeDict(
            hash=None,
            hard=False,
            lock=threading.RLock(),
            watch_thread=None,
            changed_assets=[],
        )

        self._assets_files = []
        self._long_callback_count = 0
        self._background_manager = background_callback_manager or long_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)

        # 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 init_app(self, app=None, **kwargs):
        """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

                # 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, view_func, methods=("GET",)):
        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)
        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
            )

        # 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):
        return self._layout

    def _layout_value(self):
        layout = self._layout() if self._layout_is_function else self._layout

        # Add any extra components
        if self._extra_components:
            layout = html.Div(children=[layout] + self._extra_components)

        return layout

    @layout.setter
    def layout(self, value):
        _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

    @property
    def index_string(self):
        return self._index_string

    @index_string.setter
    def index_string(self, value):
        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()

        # 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,
            "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,
        }
        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):
        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(
                os.path.dirname(sys.modules[namespace].__file__), 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

    def _generate_css_dist_html(self):
        external_links = self.config.external_stylesheets
        links = self._collect_and_register_resources(self.css.get_all_css())

        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):
        # 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)
            )
            + 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._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):
        return f'<script id="_dash-config" type="application/json">{to_json(self._config())}</script>'

    def _generate_renderer(self):
        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,
        )

        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):
        """
        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,
        )

    def long_callback(
        self,
        *_args,
        manager=None,
        interval=1000,
        running=None,
        cancel=None,
        progress=None,
        progress_default=None,
        cache_args_to_ignore=None,
        **_kwargs,
    ):
        """
        Deprecated: long callbacks are now supported natively with regular callbacks,
        use `background=True` with `dash.callback` or `app.callback` instead.
        """
        return _callback.callback(
            *_args,
            background=True,
            manager=manager,
            interval=interval,
            progress=progress,
            progress_default=progress_default,
            running=running,
            cancel=cancel,
            cache_args_to_ignore=cache_args_to_ignore,
            callback_map=self.callback_map,
            callback_list=self._callback_list,
            config_prevent_initial_callbacks=self.config.prevent_initial_callbacks,
            **_kwargs,
        )

    # pylint: disable=R0915
    def dispatch(self):
        body = flask.request.get_json()

        g = AttributeDict({})

        g.inputs_list = inputs = body.get(  # pylint: disable=assigning-non-slot
            "inputs", []
        )
        g.states_list = state = body.get(  # pylint: disable=assigning-non-slot
            "state", []
        )
        output = body["output"]
        outputs_list = body.get("outputs")
        g.outputs_list = outputs_list  # pylint: disable=assigning-non-slot

        g.input_values = (  # pylint: disable=assigning-non-slot
            input_values
        ) = inputs_to_dict(inputs)
        g.state_values = inputs_to_dict(state)  # pylint: disable=assigning-non-slot
        changed_props = body.get("changedPropIds", [])
        g.triggered_inputs = [  # pylint: disable=assigning-non-slot
            {"prop_id": x, "value": input_values.get(x)} for x in changed_props
        ]

        response = (
            g.dash_response  # pylint: disable=assigning-non-slot
        ) = flask.Response(mimetype="application/json")

        args = inputs_to_vals(inputs + state)

        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("long", False)

            # Add args_grouping
            inputs_state_indices = cb["inputs_state_indices"]
            inputs_state = inputs + state
            inputs_state = convert_to_AttributeDict(inputs_state)

            if cb.get("no_output"):
                outputs_list = []
            elif not outputs_list:
                # FIXME Old renderer support?
                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, changed_props)
                update_args_group(s, changed_props)

            args_grouping = map_grouping(
                lambda ind: inputs_state[ind], inputs_state_indices
            )

            g.args_grouping = args_grouping  # pylint: disable=assigning-non-slot
            g.using_args_grouping = (  # pylint: disable=assigning-non-slot
                not isinstance(inputs_state_indices, int)
                and (
                    inputs_state_indices
                    != list(range(grouping_len(inputs_state_indices)))
                )
            )

            # Add outputs_grouping
            outputs_indices = cb["outputs_indices"]
            if not isinstance(outputs_list, list):
                flat_outputs = [outputs_list]
            else:
                flat_outputs = outputs_list

            if len(flat_outputs) > 0:
                outputs_grouping = map_grouping(
                    lambda ind: flat_outputs[ind], outputs_indices
                )
                g.outputs_grouping = (
                    outputs_grouping  # pylint: disable=assigning-non-slot
                )
                g.using_outputs_grouping = (  # pylint: disable=assigning-non-slot
                    not isinstance(outputs_indices, int)
                    and outputs_indices != list(range(grouping_len(outputs_indices)))
                )
            else:
                g.outputs_grouping = []
                g.using_outputs_grouping = []
            g.updated_props = {}

        except KeyError as missing_callback_function:
            msg = f"Callback function not found for output '{output}', perhaps you forgot to prepend the '@'?"
            raise KeyError(msg) from missing_callback_function

        ctx = copy_context()
        # noinspection PyArgumentList
        response.set_data(
            ctx.run(
                functools.partial(
                    func,
                    *args,
                    outputs_list=outputs_list,
                    long_callback_manager=self._background_manager,
                    callback_context=g,
                    app=self,
                    app_on_error=self._on_error,
                )
            )
        )
        return 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_long_callbacks(self.callback_map)

        cancels = {}

        for callback in self.callback_map.values():
            long = callback.get("long")
            if not long:
                continue
            if "cancel_inputs" in long:
                cancel = long.pop("cancel_inputs")
                for c in cancel:
                    cancels[c] = long.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_filter = re.compile(ignore_str) if ignore_str else None

        for current, _, files in sorted(os.walk(walk_dir)):
            if current == walk_dir:
                base = ""
            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]

            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))
                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"):
        """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):
        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):
        """
        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, view_func, methods):
        """
        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):
        """
        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)
            )

        return dev_tools

    def enable_dev_tools(
        self,
        debug=None,
        dev_tools_ui=None,
        dev_tools_props_check=None,
        dev_tools_serve_dev_bundles=None,
        dev_tools_hot_reload=None,
        dev_tools_hot_reload_interval=None,
        dev_tools_hot_reload_watch_interval=None,
        dev_tools_hot_reload_max_retry=None,
        dev_tools_silence_routes_logging=None,
        dev_tools_prune_errors=None,
    ):
        """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_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_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,
            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,
                )

                for index, package in enumerate(packages):
                    if isinstance(package, AssertionRewritingHook):
                        dash_spec = importlib.util.find_spec("dash")
                        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
                if isinstance(package, ModuleSpec)
                else os.path.dirname(package.path)
                if hasattr(package, "path")
                else os.path.dirname(
                    package._path[0]  # pylint: disable=protected-access
                )
                if hasattr(package, "_path")
                else package.filename
                for package in packages
            ]

            for i, package in enumerate(packages):
                if hasattr(package, "path") and "dash/dash" in os.path.dirname(
                    package.path
                ):
                    component_packages_dist[i : i + 1] = [
                        os.path.join(os.path.dirname(package.path), x)
                        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)

                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)

    def run(
        self,
        host="127.0.0.1",
        port="8050",
        proxy=None,
        debug=None,
        jupyter_mode: Optional[JupyterDisplayMode] = None,
        jupyter_width="100%",
        jupyter_height=650,
        jupyter_server_url=None,
        dev_tools_ui=None,
        dev_tools_props_check=None,
        dev_tools_serve_dev_bundles=None,
        dev_tools_hot_reload=None,
        dev_tools_hot_reload_interval=None,
        dev_tools_hot_reload_watch_interval=None,
        dev_tools_hot_reload_max_retry=None,
        dev_tools_silence_routes_logging=None,
        dev_tools_prune_errors=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
            env: ``HOST``
        :type host: string

        :param port: Port used to serve the application
            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_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_prune_errors,
        )

        # Evaluate the env variables at runtime

        host = os.getenv("HOST", host)
        port = os.getenv("PORT", port)
        proxy = os.getenv("DASH_PROXY", 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):
        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)

            @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, **query_parameters, **states)
                        if path_variables
                        else layout(**query_parameters, **states)
                    )
                if callable(title):
                    title = title(**path_variables) if path_variables else title()

                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(
                    [
                        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")

            # 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 run_server(self, *args, **kwargs):
        """`run_server` is a deprecated alias of `run` and may be removed in a
        future version. We recommend using `app.run` instead.

        See `app.run` for usage information.
        """
        warnings.warn(
            DeprecationWarning(
                "Dash.run_server is deprecated and will be removed in Dash 3.0"
            )
        )
        self.run(*args, **kwargs)



File: dash/_watch.py

import collections
import os
import re
import time


def watch(folders, on_change, pattern=None, sleep_time=0.1):
    pattern = re.compile(pattern) if pattern else None
    watched = collections.defaultdict(lambda: -1)

    def walk():
        walked = []
        for folder in folders:
            for current, _, files in os.walk(folder):
                for f in files:
                    if pattern and not pattern.search(f):
                        continue
                    path = os.path.join(current, f)

                    info = os.stat(path)
                    new_time = info.st_mtime

                    if new_time > watched[path] > 0:
                        on_change(path, new_time, False)

                    watched[path] = new_time
                    walked.append(path)

        # Look for deleted files
        for w in [x for x in watched.keys() if x not in walked]:
            del watched[w]
            on_change(w, -1, True)

    while True:
        walk()
        time.sleep(sleep_time)



File: dash/_validate.py

import sys
from collections.abc import MutableSequence
import re
from textwrap import dedent
from keyword import iskeyword
import flask

from ._grouping import grouping_len, map_grouping
from .development.base_component import Component
from . import exceptions
from ._utils import (
    patch_collections_abc,
    stringify_id,
    to_json,
    coerce_to_list,
    clean_property_name,
)


def validate_callback(outputs, inputs, state, extra_args, types):
    Input, Output, State = types
    if extra_args:
        if not isinstance(extra_args[0], (Output, Input, State)):
            raise exceptions.IncorrectTypeException(
                dedent(
                    f"""
                    Callback arguments must be `Output`, `Input`, or `State` objects,
                    optionally wrapped in a list or tuple. We found (possibly after
                    unwrapping a list or tuple):
                    {repr(extra_args[0])}
                    """
                )
            )

        raise exceptions.IncorrectTypeException(
            dedent(
                f"""
                In a callback definition, you must provide all Outputs first,
                then all Inputs, then all States. After this item:
                {(outputs + inputs + state)[-1]!r}
                we found this item next:
                {extra_args[0]!r}
                """
            )
        )

    for args in [outputs, inputs, state]:
        for arg in args:
            validate_callback_arg(arg)


def validate_callback_arg(arg):
    if not isinstance(getattr(arg, "component_property", None), str):
        raise exceptions.IncorrectTypeException(
            dedent(
                f"""
                component_property must be a string, found {arg.component_property!r}
                """
            )
        )

    if hasattr(arg, "component_event"):
        raise exceptions.NonExistentEventException(
            """
            Events have been removed.
            Use the associated property instead.
            """
        )

    if isinstance(arg.component_id, dict):
        validate_id_dict(arg)

    elif isinstance(arg.component_id, str):
        validate_id_string(arg)

    else:
        raise exceptions.IncorrectTypeException(
            dedent(
                f"""
                component_id must be a string or dict, found {arg.component_id!r}
                """
            )
        )


def validate_id_dict(arg):
    arg_id = arg.component_id

    for k in arg_id:
        # Need to keep key type validation on the Python side, since
        # non-string keys will be converted to strings in json.dumps and may
        # cause unwanted collisions
        if not isinstance(k, str):
            raise exceptions.IncorrectTypeException(
                dedent(
                    f"""
                    Wildcard ID keys must be non-empty strings,
                    found {k!r} in id {arg_id!r}
                    """
                )
            )


def validate_id_string(arg):
    arg_id = arg.component_id

    invalid_chars = ".{"
    invalid_found = [x for x in invalid_chars if x in arg_id]
    if invalid_found:
        raise exceptions.InvalidComponentIdError(
            f"""
            The element `{arg_id}` contains `{"`, `".join(invalid_found)}` in its ID.
            Characters `{"`, `".join(invalid_chars)}` are not allowed in IDs.
            """
        )


def validate_output_spec(output, output_spec, Output):
    """
    This validation is for security and internal debugging, not for users,
    so the messages are not intended to be clear.
    `output` comes from the callback definition, `output_spec` from the request.
    """
    if not isinstance(output, (list, tuple)):
        output, output_spec = [output], [output_spec]
    elif len(output) != len(output_spec):
        raise exceptions.CallbackException("Wrong length output_spec")

    for outi, speci in zip(output, output_spec):
        speci_list = speci if isinstance(speci, (list, tuple)) else [speci]
        for specij in speci_list:
            if (
                not Output(specij["id"], clean_property_name(specij["property"]))
                == outi
            ):
                raise exceptions.CallbackException(
                    "Output does not match callback definition"
                )


def validate_and_group_input_args(flat_args, arg_index_grouping):
    if grouping_len(arg_index_grouping) != len(flat_args):
        raise exceptions.CallbackException("Inputs do not match callback definition")

    args_grouping = map_grouping(lambda ind: flat_args[ind], arg_index_grouping)
    if isinstance(arg_index_grouping, dict):
        func_args = []
        func_kwargs = args_grouping
        for key in func_kwargs:
            if not key.isidentifier():
                raise exceptions.CallbackException(
                    f"{key} is not a valid Python variable name"
                )
    elif isinstance(arg_index_grouping, (tuple, list)):
        func_args = list(args_grouping)
        func_kwargs = {}
    else:
        # Scalar input
        func_args = [args_grouping]
        func_kwargs = {}

    return func_args, func_kwargs


def validate_multi_return(output_lists, output_values, callback_id):
    if not isinstance(output_values, (list, tuple)):
        raise exceptions.InvalidCallbackReturnValue(
            dedent(
                f"""
                The callback {callback_id} is a multi-output.
                Expected the output type to be a list or tuple but got:
                {output_values!r}.
                """
            )
        )

    if len(output_values) != len(output_lists):
        raise exceptions.InvalidCallbackReturnValue(
            f"""
            Invalid number of output values for {callback_id}.
            Expected {len(output_lists)}, got {len(output_values)}
            """
        )

    for i, output_spec in enumerate(output_lists):
        if isinstance(output_spec, list):
            output_value = output_values[i]
            if not isinstance(output_value, (list, tuple)):
                raise exceptions.InvalidCallbackReturnValue(
                    dedent(
                        f"""
                        The callback {callback_id} output {i} is a wildcard multi-output.
                        Expected the output type to be a list or tuple but got:
                        {output_value!r}.
                        output spec: {output_spec!r}
                        """
                    )
                )

            if len(output_value) != len(output_spec):
                raise exceptions.InvalidCallbackReturnValue(
                    dedent(
                        f"""
                        Invalid number of output values for {callback_id} item {i}.
                        Expected {len(output_spec)}, got {len(output_value)}
                        output spec: {output_spec!r}
                        output value: {output_value!r}
                        """
                    )
                )


def fail_callback_output(output_value, output):
    valid_children = (str, int, float, type(None), Component)
    valid_props = (str, int, float, type(None), tuple, MutableSequence)

    def _raise_invalid(bad_val, outer_val, path, index=None, toplevel=False):
        bad_type = type(bad_val).__name__
        outer_id = f"(id={outer_val.id:s})" if getattr(outer_val, "id", False) else ""
        outer_type = type(outer_val).__name__
        if toplevel:
            location = dedent(
                """
                The value in question is either the only value returned,
                or is in the top level of the returned list,
                """
            )
        else:
            index_string = "[*]" if index is None else f"[{index:d}]"
            location = dedent(
                f"""
                The value in question is located at
                {index_string} {outer_type} {outer_id}
                {path},
                """
            )

        obj = "tree with one value" if not toplevel else "value"
        raise exceptions.InvalidCallbackReturnValue(
            dedent(
                f"""
                The callback for `{output!r}`
                returned a {obj:s} having type `{bad_type}`
                which is not JSON serializable.

                {location}
                and has string representation
                `{bad_val}`

                In general, Dash properties can only be
                dash components, strings, dictionaries, numbers, None,
                or lists of those.
                """
            )
        )

    def _valid_child(val):
        return isinstance(val, valid_children)

    def _valid_prop(val):
        return isinstance(val, valid_props)

    def _can_serialize(val):
        if not (_valid_child(val) or _valid_prop(val)):
            return False
        try:
            to_json(val)
        except TypeError:
            return False
        return True

    def _validate_value(val, index=None):
        # val is a Component
        if isinstance(val, Component):
            unserializable_items = []
            # pylint: disable=protected-access
            for p, j in val._traverse_with_paths():
                # check each component value in the tree
                if not _valid_child(j):
                    _raise_invalid(bad_val=j, outer_val=val, path=p, index=index)

                if not _can_serialize(j):
                    # collect unserializable items separately, so we can report
                    # only the deepest level, not all the parent components that
                    # are just unserializable because of their children.
                    unserializable_items = [
                        i for i in unserializable_items if not p.startswith(i[0])
                    ]
                    if unserializable_items:
                        # we already have something unserializable in a different
                        # branch - time to stop and fail
                        break
                    if all(not i[0].startswith(p) for i in unserializable_items):
                        unserializable_items.append((p, j))

                # Children that are not of type Component or
                # list/tuple not returned by traverse
                child = getattr(j, "children", None)
                if not isinstance(child, (tuple, MutableSequence)):
                    if child and not _can_serialize(child):
                        _raise_invalid(
                            bad_val=child,
                            outer_val=val,
                            path=p + "\n" + "[*] " + type(child).__name__,
                            index=index,
                        )
            if unserializable_items:
                p, j = unserializable_items[0]
                # just report the first one, even if there are multiple,
                # as that's how all the other errors work
                _raise_invalid(bad_val=j, outer_val=val, path=p, index=index)

            # Also check the child of val, as it will not be returned
            child = getattr(val, "children", None)
            if not isinstance(child, (tuple, MutableSequence)):
                if child and not _can_serialize(val):
                    _raise_invalid(
                        bad_val=child,
                        outer_val=val,
                        path=type(child).__name__,
                        index=index,
                    )

        if not _can_serialize(val):
            _raise_invalid(
                bad_val=val,
                outer_val=type(val).__name__,
                path="",
                index=index,
                toplevel=True,
            )

    if isinstance(output_value, list):
        for i, val in enumerate(output_value):
            _validate_value(val, index=i)
    else:
        _validate_value(output_value)

    # if we got this far, raise a generic JSON error
    raise exceptions.InvalidCallbackReturnValue(
        f"""
        The callback for output `{output!r}`
        returned a value which is not JSON serializable.

        In general, Dash properties can only be dash components, strings,
        dictionaries, numbers, None, or lists of those.
        """
    )


def check_obsolete(kwargs):
    for key in kwargs:
        if key in ["components_cache_max_age", "static_folder"]:
            raise exceptions.ObsoleteKwargException(
                f"""
                {key} is no longer a valid keyword argument in Dash since v1.0.
                See https://dash.plotly.com for details.
                """
            )
        if key in ["dynamic_loading", "preloaded_libraries"]:
            # Only warns as this was only available for a short time.
            print(
                f"{key} has been removed and no longer a valid keyword argument in Dash.",
                file=sys.stderr,
            )
            continue
        # any other kwarg mimic the built-in exception
        raise TypeError(f"Dash() got an unexpected keyword argument '{key}'")


def validate_js_path(registered_paths, package_name, path_in_package_dist):
    if package_name not in registered_paths:
        raise exceptions.DependencyException(
            f"""
            Error loading dependency. "{package_name}" is not a registered library.
            Registered libraries are:
            {list(registered_paths.keys())}
            """
        )

    if path_in_package_dist not in registered_paths[package_name]:
        raise exceptions.DependencyException(
            f"""
            "{package_name}" is registered but the path requested is not valid.
            The path requested: "{path_in_package_dist}"
            List of registered paths: {registered_paths}
            """
        )


def validate_index(name, checks, index):
    missing = [i for check, i in checks if not re.compile(check).search(index)]
    if missing:
        plural = "s" if len(missing) > 1 else ""
        raise exceptions.InvalidIndexException(
            f"Missing item{plural} {', '.join(missing)} in {name}."
        )


def validate_layout_type(value):
    if not isinstance(
        value, (Component, patch_collections_abc("Callable"), list, tuple)
    ):
        raise exceptions.NoLayoutException(
            """
            Layout must be a single dash component, a list of dash components,
            or a function that returns a dash component.
            """
        )


def validate_layout(layout, layout_value):
    if layout is None:
        raise exceptions.NoLayoutException(
            """
            The layout was `None` at the time that `run_server` was called.
            Make sure to set the `layout` attribute of your application
            before running the server.
            """
        )

    component_ids = set()

    def _validate(value):
        def _validate_id(comp):
            component_id = stringify_id(getattr(comp, "id", None))
            if component_id and component_id in component_ids:
                raise exceptions.DuplicateIdError(
                    f"""
                    Duplicate component id found in the initial layout: `{component_id}`
                    """
                )
            component_ids.add(component_id)

        _validate_id(value)

        for component in value._traverse():  # pylint: disable=protected-access
            _validate_id(component)

    if isinstance(layout_value, (list, tuple)):
        for component in layout_value:
            if isinstance(component, (str,)):
                continue
            if isinstance(component, (Component,)):
                _validate(component)
            else:
                raise exceptions.NoLayoutException(
                    "Only strings and components are allowed in a list layout."
                )
    else:
        _validate(layout_value)


def validate_template(template):
    variable_names = re.findall("<(.*?)>", template)

    for name in variable_names:
        if not name.isidentifier() or iskeyword(name):
            raise Exception(
                f'`{name}` is not a valid Python variable name in `path_template`: "{template}".'
            )


def check_for_duplicate_pathnames(registry):
    path_to_module = {}
    for page in registry.values():
        if page["path"] not in path_to_module:
            path_to_module[page["path"]] = [page["module"]]
        else:
            path_to_module[page["path"]].append(page["module"])

    for modules in path_to_module.values():
        if len(modules) > 1:
            raise Exception(f"modules {modules} have duplicate paths")


def validate_registry(registry):
    for page in registry.values():
        if "layout" not in page:
            raise exceptions.NoLayoutException(
                f"No layout in module `{page['module']}` in dash.page_registry"
            )
        if page["module"] == "__main__":
            raise Exception(
                """
                When registering pages from app.py, `__name__` is not a valid module name.  Use a string instead.
                For example, `dash.register_page("my_module_name")`, rather than `dash.register_page(__name__)`
                """
            )


def validate_pages_layout(module, page):
    if not hasattr(page, "layout"):
        raise exceptions.NoLayoutException(
            f"""
            No layout found in module {module}
            A variable or a function named "layout" is required.
            """
        )


def validate_use_pages(config):
    if not config.get("assets_folder", None):
        raise exceptions.PageError(
            "`dash.register_page()` must be called after app instantiation"
        )

    if flask.has_request_context():
        raise exceptions.PageError(
            """
            dash.register_page() can’t be called within a callback as it updates dash.page_registry, which is a global variable.
             For more details, see https://dash.plotly.com/sharing-data-between-callbacks#why-global-variables-will-break-your-app
            """
        )


def validate_module_name(module):
    if not isinstance(module, str):
        raise exceptions.PageError(
            "The first attribute of dash.register_page() must be a string or '__name__'"
        )
    return module


def validate_long_callbacks(callback_map):
    # Validate that long callback side output & inputs are not circular
    # If circular, triggering a long callback would result in a fatal server/computer crash.
    all_outputs = set()
    input_indexed = {}
    for callback in callback_map.values():
        out = coerce_to_list(callback["output"])
        all_outputs.update(out)
        for o in out:
            input_indexed.setdefault(o, set())
            input_indexed[o].update(coerce_to_list(callback["raw_inputs"]))

    for callback in (x for x in callback_map.values() if x.get("long")):
        long_info = callback["long"]
        progress = long_info.get("progress", [])
        running = long_info.get("running", [])

        long_inputs = coerce_to_list(callback["raw_inputs"])
        outputs = set([x[0] for x in running] + progress)
        circular = [
            x
            for x in set(k for k, v in input_indexed.items() if v.intersection(outputs))
            if x in long_inputs
        ]

        if circular:
            raise exceptions.LongCallbackError(
                f"Long callback circular error!\n{circular} is used as input for a long callback"
                f" but also used as output from an input that is updated with progress or running argument."
            )


def validate_duplicate_output(
    output, prevent_initial_call, config_prevent_initial_call
):
    if "initial_duplicate" in (prevent_initial_call, config_prevent_initial_call):
        return

    def _valid(out):
        if (
            out.allow_duplicate
            and not prevent_initial_call
            and not config_prevent_initial_call
        ):
            raise exceptions.DuplicateCallback(
                "allow_duplicate requires prevent_initial_call to be True. The order of the call is not"
                " guaranteed to be the same on every page load. "
                "To enable duplicate callback with initial call, set prevent_initial_call='initial_duplicate' "
                " or globally in the config prevent_initial_callbacks='initial_duplicate'"
            )

    if isinstance(output, (list, tuple)):
        for o in output:
            _valid(o)

        return

    _valid(output)



File: dash/exceptions.py

from textwrap import dedent


class DashException(Exception):
    def __init__(self, msg=""):
        super().__init__(dedent(msg).strip())


class ObsoleteKwargException(DashException):
    pass


class NoLayoutException(DashException):
    pass


class CallbackException(DashException):
    pass


class NonExistentEventException(CallbackException):
    pass


class IncorrectTypeException(CallbackException):
    pass


class IDsCantContainPeriods(CallbackException):
    pass


class WildcardInLongCallback(CallbackException):
    pass


# Better error name now that more than periods are not permitted.
class InvalidComponentIdError(IDsCantContainPeriods):
    pass


class PreventUpdate(CallbackException):
    pass


class DuplicateIdError(DashException):
    pass


class InvalidCallbackReturnValue(CallbackException):
    pass


class InvalidConfig(DashException):
    pass


class InvalidResourceError(DashException):
    pass


class InvalidIndexException(DashException):
    pass


class DependencyException(DashException):
    pass


class ResourceException(DashException):
    pass


class MissingCallbackContextException(CallbackException):
    pass


class UnsupportedRelativePath(CallbackException):
    pass


class ProxyError(DashException):
    pass


class DuplicateCallback(DashException):
    pass


class LongCallbackError(DashException):
    pass


class MissingLongCallbackManagerError(DashException):
    pass


class PageError(DashException):
    pass


class ImportedInsideCallbackError(DashException):
    pass



File: dash/resources.py

import json
import warnings
import os

from .development.base_component import ComponentRegistry
from . import exceptions


class Resources:
    def __init__(self, resource_name):
        self._resources = []
        self.resource_name = resource_name

    def append_resource(self, resource):
        self._resources.append(resource)

    # pylint: disable=too-many-branches
    def _filter_resources(self, all_resources, dev_bundles=False):
        filtered_resources = []
        for s in all_resources:
            filtered_resource = {}
            if "dynamic" in s:
                filtered_resource["dynamic"] = s["dynamic"]
            if "async" in s:
                if "dynamic" in s:
                    raise exceptions.ResourceException(
                        f"""
                        Can't have both 'dynamic' and 'async'.
                        {json.dumps(filtered_resource)}
                        """
                    )

                # Async assigns a value dynamically to 'dynamic'
                # based on the value of 'async' and config.eager_loading
                #
                # True -> dynamic if the server is not eager, False otherwise
                # 'lazy' -> always dynamic
                # 'eager' -> dynamic if server is not eager
                # (to prevent ever loading it)
                filtered_resource["dynamic"] = (
                    not self.config.eager_loading
                    if s["async"] is True
                    else (s["async"] == "eager" and not self.config.eager_loading)
                    or s["async"] == "lazy"
                )
            if "namespace" in s:
                filtered_resource["namespace"] = s["namespace"]
            if "external_url" in s and not self.config.serve_locally:
                filtered_resource["external_url"] = s["external_url"]
            elif "dev_package_path" in s and dev_bundles:
                filtered_resource["relative_package_path"] = s["dev_package_path"]
            elif "relative_package_path" in s:
                filtered_resource["relative_package_path"] = s["relative_package_path"]
            elif "absolute_path" in s:
                filtered_resource["absolute_path"] = s["absolute_path"]
            elif "asset_path" in s:
                info = os.stat(s["filepath"])
                filtered_resource["asset_path"] = s["asset_path"]
                filtered_resource["ts"] = info.st_mtime
            elif self.config.serve_locally:
                warnings.warn(
                    (
                        "You have set your config to `serve_locally=True` but "
                        f"A local version of {s['external_url']} is not available.\n"
                        "If you added this file with "
                        "`app.scripts.append_script` "
                        "or `app.css.append_css`, use `external_scripts` "
                        "or `external_stylesheets` instead.\n"
                        "See https://dash.plotly.com/external-resources"
                    )
                )
                continue
            else:
                raise exceptions.ResourceException(
                    f"""
                    {json.dumps(filtered_resource)} does not have a
                    relative_package_path, absolute_path, or an external_url.
                    """
                )

            filtered_resources.append(filtered_resource)

        return filtered_resources

    def get_all_resources(self, dev_bundles=False):
        lib_resources = ComponentRegistry.get_resources(self.resource_name)
        all_resources = lib_resources + self._resources

        return self._filter_resources(all_resources, dev_bundles)

    def get_library_resources(self, libraries, dev_bundles=False):
        lib_resources = ComponentRegistry.get_resources(self.resource_name, libraries)
        all_resources = lib_resources + self._resources

        return self._filter_resources(all_resources, dev_bundles)


# pylint: disable=too-few-public-methods
class _Config:
    def __init__(self, serve_locally, eager_loading):
        self.eager_loading = eager_loading
        self.serve_locally = serve_locally


class Css:
    def __init__(self, serve_locally):
        self._resources = Resources("_css_dist")
        self._resources.config = self.config = _Config(serve_locally, True)

    def append_css(self, stylesheet):
        self._resources.append_resource(stylesheet)

    def get_all_css(self):
        return self._resources.get_all_resources()

    def get_library_css(self, libraries):
        return self._resources.get_library_resources(libraries)


class Scripts:
    def __init__(self, serve_locally, eager):
        self._resources = Resources("_js_dist")
        self._resources.config = self.config = _Config(serve_locally, eager)

    def append_script(self, script):
        self._resources.append_resource(script)

    def get_all_scripts(self, dev_bundles=False):
        return self._resources.get_all_resources(dev_bundles)

    def get_library_scripts(self, libraries, dev_bundles=False):
        return self._resources.get_library_resources(libraries, dev_bundles)



File: dash/_jupyter.py

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 = "/"

        # 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}/_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_server."
                    )
            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()



File: dash/_get_app.py

from textwrap import dedent

APP = None


def get_app():
    if APP is None:
        raise Exception(
            dedent(
                """
                App object is not yet defined.  `app = dash.Dash()` needs to be run
                before `dash.get_app()` is called and can only be used within apps that use
                the `pages` multi-page app feature: `dash.Dash(use_pages=True)`.

                `dash.get_app()` is used to get around circular import issues when Python files
                within the pages/` folder need to reference the `app` object.
                """
            )
        )
    return APP



File: dash/dependencies.py

from dash.development.base_component import Component

from ._validate import validate_callback
from ._grouping import flatten_grouping, make_grouping_by_index
from ._utils import stringify_id


class _Wildcard:  # pylint: disable=too-few-public-methods
    def __init__(self, name):
        self._name = name

    def __str__(self):
        return self._name

    def __repr__(self):
        return f"<{self}>"

    def to_json(self):
        # used in serializing wildcards - arrays are not allowed as
        # id values, so make the wildcards look like length-1 arrays.
        return f'["{self._name}"]'


MATCH = _Wildcard("MATCH")
ALL = _Wildcard("ALL")
ALLSMALLER = _Wildcard("ALLSMALLER")


class DashDependency:  # pylint: disable=too-few-public-methods
    def __init__(self, component_id, component_property):

        if isinstance(component_id, Component):
            self.component_id = component_id._set_random_id()
        else:
            self.component_id = component_id

        self.component_property = component_property
        self.allow_duplicate = False

    def __str__(self):
        return f"{self.component_id_str()}.{self.component_property}"

    def __repr__(self):
        return f"<{self.__class__.__name__} `{self}`>"

    def component_id_str(self):
        return stringify_id(self.component_id)

    def to_dict(self):
        return {"id": self.component_id_str(), "property": self.component_property}

    def __eq__(self, other):
        """
        We use "==" to denote two deps that refer to the same prop on
        the same component. In the case of wildcard deps, this means
        the same prop on *at least one* of the same components.
        """
        return (
            isinstance(other, DashDependency)
            and self.component_property == other.component_property
            and self._id_matches(other)
        )

    def _id_matches(self, other):
        my_id = self.component_id
        other_id = other.component_id
        self_dict = isinstance(my_id, dict)
        other_dict = isinstance(other_id, dict)

        if self_dict != other_dict:
            return False
        if self_dict:
            if set(my_id.keys()) != set(other_id.keys()):
                return False

            for k, v in my_id.items():
                other_v = other_id[k]
                if v == other_v:
                    continue
                v_wild = isinstance(v, _Wildcard)
                other_wild = isinstance(other_v, _Wildcard)
                if v_wild or other_wild:
                    if not (v_wild and other_wild):
                        continue  # one wild, one not
                    if v is ALL or other_v is ALL:
                        continue  # either ALL
                    if v is MATCH or other_v is MATCH:
                        return False  # one MATCH, one ALLSMALLER
                else:
                    return False
            return True

        # both strings
        return my_id == other_id

    def __hash__(self):
        return hash(str(self))

    def has_wildcard(self):
        """
        Return true if id contains a wildcard (MATCH, ALL, or ALLSMALLER)
        """
        if isinstance(self.component_id, dict):
            for v in self.component_id.values():
                if isinstance(v, _Wildcard):
                    return True
        return False


class Output(DashDependency):  # pylint: disable=too-few-public-methods
    """Output of a callback."""

    allowed_wildcards = (MATCH, ALL)

    def __init__(self, component_id, component_property, allow_duplicate=False):
        super().__init__(component_id, component_property)
        self.allow_duplicate = allow_duplicate


class Input(DashDependency):  # pylint: disable=too-few-public-methods
    """Input of callback: trigger an update when it is updated."""

    allowed_wildcards = (MATCH, ALL, ALLSMALLER)


class State(DashDependency):  # pylint: disable=too-few-public-methods
    """Use the value of a State in a callback but don't trigger updates."""

    allowed_wildcards = (MATCH, ALL, ALLSMALLER)


class ClientsideFunction:  # pylint: disable=too-few-public-methods
    def __init__(self, namespace=None, function_name=None):

        if namespace.startswith("_dashprivate_"):
            raise ValueError("Namespaces cannot start with '_dashprivate_'.")

        if namespace in ["PreventUpdate", "no_update"]:
            raise ValueError(
                f'"{namespace}" is a forbidden namespace in dash_clientside.'
            )

        self.namespace = namespace
        self.function_name = function_name

    def __repr__(self):
        return f"ClientsideFunction({self.namespace}, {self.function_name})"


def extract_grouped_output_callback_args(args, kwargs):
    if "output" in kwargs:
        parameters = kwargs["output"]
        # Normalize list/tuple of multiple positional outputs to a tuple
        if isinstance(parameters, (list, tuple)):
            parameters = list(parameters)

        # Make sure dependency grouping contains only Output objects
        for dep in flatten_grouping(parameters):
            if not isinstance(dep, Output):
                raise ValueError(
                    f"Invalid value provided where an Output dependency "
                    f"object was expected: {dep}"
                )

        return parameters

    parameters = []
    while args:
        next_deps = flatten_grouping(args[0])
        if all(isinstance(d, Output) for d in next_deps):
            parameters.append(args.pop(0))
        else:
            break
    return parameters


def extract_grouped_input_state_callback_args_from_kwargs(kwargs):
    input_parameters = kwargs["inputs"]
    if isinstance(input_parameters, DashDependency):
        input_parameters = [input_parameters]

    state_parameters = kwargs.get("state", None)
    if isinstance(state_parameters, DashDependency):
        state_parameters = [state_parameters]

    if isinstance(input_parameters, dict):
        # Wrapped function will be called with named keyword arguments
        if state_parameters:
            if not isinstance(state_parameters, dict):
                raise ValueError(
                    "The input argument to app.callback was a dict, "
                    "but the state argument was not.\n"
                    "input and state arguments must have the same type"
                )

            # Merge into state dependencies
            parameters = state_parameters
            parameters.update(input_parameters)
        else:
            parameters = input_parameters

        return parameters

    if isinstance(input_parameters, (list, tuple)):
        # Wrapped function will be called with positional arguments
        parameters = list(input_parameters)
        if state_parameters:
            if not isinstance(state_parameters, (list, tuple)):
                raise ValueError(
                    "The input argument to app.callback was a list, "
                    "but the state argument was not.\n"
                    "input and state arguments must have the same type"
                )

            parameters += list(state_parameters)

        return parameters

    raise ValueError(
        "The input argument to app.callback may be a dict, list, or tuple,\n"
        f"but received value of type {type(input_parameters)}"
    )


def extract_grouped_input_state_callback_args_from_args(args):
    # Collect input and state from args
    parameters = []
    while args:
        next_deps = flatten_grouping(args[0])
        if all(isinstance(d, (Input, State)) for d in next_deps):
            parameters.append(args.pop(0))
        else:
            break

    if len(parameters) == 1:
        # Only one output grouping, return as-is
        return parameters[0]

    # Multiple output groupings, return wrap in tuple
    return parameters


def extract_grouped_input_state_callback_args(args, kwargs):
    if "inputs" in kwargs:
        return extract_grouped_input_state_callback_args_from_kwargs(kwargs)

    if "state" in kwargs:
        # Not valid to provide state as kwarg without input as kwarg
        raise ValueError(
            "The state keyword argument may not be provided without "
            "the input keyword argument"
        )

    return extract_grouped_input_state_callback_args_from_args(args)


def compute_input_state_grouping_indices(input_state_grouping):
    # Flatten grouping of Input and State dependencies into a flat list
    flat_deps = flatten_grouping(input_state_grouping)

    # Split into separate flat lists of Input and State dependencies
    flat_inputs = [dep for dep in flat_deps if isinstance(dep, Input)]
    flat_state = [dep for dep in flat_deps if isinstance(dep, State)]

    # For each entry in the grouping, compute the index into the
    # concatenation of flat_inputs and flat_state
    total_inputs = len(flat_inputs)
    input_count = 0
    state_count = 0
    flat_inds = []
    for dep in flat_deps:
        if isinstance(dep, Input):
            flat_inds.append(input_count)
            input_count += 1
        else:
            flat_inds.append(total_inputs + state_count)
            state_count += 1

    # Reshape this flat list of indices to match the input grouping
    grouping_inds = make_grouping_by_index(input_state_grouping, flat_inds)
    return flat_inputs, flat_state, grouping_inds


def handle_grouped_callback_args(args, kwargs):
    """Split args into outputs, inputs and states"""
    prevent_initial_call = kwargs.get("prevent_initial_call", None)
    if prevent_initial_call is None and args and isinstance(args[-1], bool):
        args, prevent_initial_call = args[:-1], args[-1]

    # flatten args, to support the older syntax where outputs, inputs, and states
    # each needed to be in their own list
    flat_args = []
    for arg in args:
        flat_args += arg if isinstance(arg, (list, tuple)) else [arg]

    outputs = extract_grouped_output_callback_args(flat_args, kwargs)
    flat_outputs = flatten_grouping(outputs)

    if isinstance(outputs, (list, tuple)) and len(outputs) == 1:
        out0 = kwargs.get("output", args[0] if args else None)
        if not isinstance(out0, (list, tuple)):
            # unless it was explicitly provided as a list, a single output
            # should be unwrapped. That ensures the return value of the
            # callback is also not expected to be wrapped in a list.
            outputs = outputs[0]

    inputs_state = extract_grouped_input_state_callback_args(flat_args, kwargs)
    flat_inputs, flat_state, input_state_indices = compute_input_state_grouping_indices(
        inputs_state
    )

    types = Input, Output, State
    validate_callback(flat_outputs, flat_inputs, flat_state, flat_args, types)

    return outputs, flat_inputs, flat_state, input_state_indices, prevent_initial_call


def extract_callback_args(args, kwargs, name, type_):
    """Extract arguments for callback from a name and type"""
    parameters = kwargs.get(name, [])
    if parameters:
        if not isinstance(parameters, (list, tuple)):
            # accept a single item, not wrapped in a list, for any of the
            # categories as a named arg (even though previously only output
            # could be given unwrapped)
            return [parameters]
    else:
        while args and isinstance(args[0], type_):
            parameters.append(args.pop(0))
    return parameters


def handle_callback_args(args, kwargs):
    """Split args into outputs, inputs and states"""
    prevent_initial_call = kwargs.get("prevent_initial_call", None)
    if prevent_initial_call is None and args and isinstance(args[-1], bool):
        args, prevent_initial_call = args[:-1], args[-1]

    # flatten args, to support the older syntax where outputs, inputs, and states
    # each needed to be in their own list
    flat_args = []
    for arg in args:
        flat_args += arg if isinstance(arg, (list, tuple)) else [arg]

    outputs = extract_callback_args(flat_args, kwargs, "output", Output)
    validate_outputs = outputs
    if len(outputs) == 1:
        out0 = kwargs.get("output", args[0] if args else None)
        if not isinstance(out0, (list, tuple)):
            # unless it was explicitly provided as a list, a single output
            # should be unwrapped. That ensures the return value of the
            # callback is also not expected to be wrapped in a list.
            outputs = outputs[0]

    inputs = extract_callback_args(flat_args, kwargs, "inputs", Input)
    states = extract_callback_args(flat_args, kwargs, "state", State)

    types = Input, Output, State
    validate_callback(validate_outputs, inputs, states, flat_args, types)

    return outputs, inputs, states, prevent_initial_call



File: dash/_utils.py

# -*- coding: utf-8 -*-
import shlex
import sys
import uuid
import hashlib
import collections
import subprocess
import logging
import io
import json
import secrets
import string
import inspect
from html import escape
from functools import wraps
from typing import Union
from dash.types import RendererHooks

logger = logging.getLogger()


def to_json(value):
    # pylint: disable=import-outside-toplevel
    from plotly.io.json import to_json_plotly

    return to_json_plotly(value)


def interpolate_str(template, **data):
    s = template
    for k, v in data.items():
        key = "{%" + k + "%}"
        s = s.replace(key, v)
    return s


def format_tag(
    tag_name, attributes, inner="", closed=False, opened=False, sanitize=False
):
    attributes = " ".join(
        [f'{k}="{escape(v) if sanitize else v}"' for k, v in attributes.items()]
    )
    tag = f"<{tag_name} {attributes}"
    if closed:
        tag += "/>"
    elif opened:
        tag += ">"
    else:
        tag += ">" + inner + f"</{tag_name}>"
    return tag


def generate_hash():
    return str(uuid.uuid4().hex).strip("-")


# pylint: disable=no-member
def patch_collections_abc(member):
    return getattr(collections.abc, member)


class AttributeDict(dict):
    """Dictionary subclass enabling attribute lookup/assignment of keys/values.

    For example::
        >>> m = AttributeDict({'foo': 'bar'})
        >>> m.foo
        'bar'
        >>> m.foo = 'not bar'
        >>> m['foo']
        'not bar'
    ``AttributeDict`` objects also provide ``.first()`` which acts like
    ``.get()`` but accepts multiple keys as arguments, and returns the value of
    the first hit, e.g.::
        >>> m = AttributeDict({'foo': 'bar', 'biz': 'baz'})
        >>> m.first('wrong', 'incorrect', 'foo', 'biz')
        'bar'
    """

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            pass
        # to conform with __getattr__ spec
        # but get out of the except block so it doesn't look like a nested err
        raise AttributeError(key)

    def set_read_only(self, names, msg="Attribute is read-only"):
        """
        Designate named attributes as read-only with the corresponding msg

        Method is additive. Making additional calls to this method will update
        existing messages and add to the current set of _read_only names.
        """
        new_read_only = {name: msg for name in names}
        if getattr(self, "_read_only", False):
            self._read_only.update(new_read_only)
        else:
            object.__setattr__(self, "_read_only", new_read_only)

    def finalize(self, msg="Object is final: No new keys may be added."):
        """Prevent any new keys being set."""
        object.__setattr__(self, "_final", msg)

    def __setitem__(self, key, val):
        if key in self.__dict__.get("_read_only", {}):
            raise AttributeError(self._read_only[key], key)

        final_msg = self.__dict__.get("_final")
        if final_msg and key not in self:
            raise AttributeError(final_msg, key)

        return super().__setitem__(key, val)

    def update(self, other):
        # Overrides dict.update() to use __setitem__ above
        for k, v in other.items():
            self[k] = v

    # pylint: disable=inconsistent-return-statements
    def first(self, *names):
        for name in names:
            value = self.get(name)
            if value:
                return value
        if not names:
            return next(iter(self), {})


def create_callback_id(output, inputs, no_output=False):
    # A single dot within a dict id key or value is OK
    # but in case of multiple dots together escape each dot
    # with `\` so we don't mistake it for multi-outputs
    hashed_inputs = None

    def _hash_inputs():
        return hashlib.sha256(
            ".".join(str(x) for x in inputs).encode("utf-8")
        ).hexdigest()

    def _concat(x):
        nonlocal hashed_inputs
        _id = x.component_id_str().replace(".", "\\.") + "." + x.component_property
        if x.allow_duplicate:
            if not hashed_inputs:
                hashed_inputs = _hash_inputs()
            # Actually adds on the property part.
            _id += f"@{hashed_inputs}"
        return _id

    if no_output:
        # No output will hash the inputs.
        return _hash_inputs()

    if isinstance(output, (list, tuple)):
        return ".." + "...".join(_concat(x) for x in output) + ".."

    return _concat(output)


# inverse of create_callback_id - should only be relevant if an old renderer is
# hooked up to a new back end, which will only happen in special cases like
# embedded
def split_callback_id(callback_id):
    if callback_id.startswith(".."):
        return [split_callback_id(oi) for oi in callback_id[2:-2].split("...")]

    id_, prop = callback_id.rsplit(".", 1)
    return {"id": id_, "property": prop}


def stringify_id(id_):
    def _json(k, v):
        vstr = v.to_json() if hasattr(v, "to_json") else json.dumps(v)
        return f"{json.dumps(k)}:{vstr}"

    if isinstance(id_, dict):
        return "{" + ",".join(_json(k, id_[k]) for k in sorted(id_)) + "}"
    return id_


def inputs_to_dict(inputs_list):
    inputs = AttributeDict()
    for i in inputs_list:
        inputsi = i if isinstance(i, list) else [i]
        for ii in inputsi:
            id_str = stringify_id(ii["id"])
            inputs[f'{id_str}.{ii["property"]}'] = ii.get("value")
    return inputs


def convert_to_AttributeDict(nested_list):
    new_dict = []
    for i in nested_list:
        if isinstance(i, dict):
            new_dict.append(AttributeDict(i))
        else:
            new_dict.append([AttributeDict(ii) for ii in i])
    return new_dict


def inputs_to_vals(inputs):
    return [
        [ii.get("value") for ii in i] if isinstance(i, list) else i.get("value")
        for i in inputs
    ]


def run_command_with_process(cmd):
    is_win = sys.platform == "win32"
    with subprocess.Popen(shlex.split(cmd, posix=is_win), shell=is_win) as proc:
        proc.wait()
        if proc.poll() is None:
            logger.warning("🚨 trying to terminate subprocess in safe way")
            try:
                proc.communicate()
            except Exception:  # pylint: disable=broad-except
                logger.exception("🚨 first try communicate failed")
                proc.kill()
                proc.communicate()


def compute_hash(path):
    with io.open(path, encoding="utf-8") as fp:
        return hashlib.sha256(fp.read().encode("utf-8")).hexdigest()


def job(msg=""):
    def wrapper(func):
        @wraps(func)
        def _wrapper(*args, **kwargs):
            logger.info("🏗️  [%s] 🏗️️  - %s", func.__name__, msg)
            res = func(*args, **kwargs)
            logger.info("::: 🍻🍻🍻 [%s] job done 🍻🍻🍻 :::", func.__name__)
            return res

        return _wrapper

    return wrapper


def gen_salt(chars):
    return "".join(
        secrets.choice(string.ascii_letters + string.digits) for _ in range(chars)
    )


class OrderedSet(collections.abc.MutableSet):
    def __init__(self, *args):
        self._data = []
        for i in args:
            self.add(i)

    def add(self, value):
        if value not in self._data:
            self._data.append(value)

    def discard(self, value):
        self._data.remove(value)

    def __contains__(self, x):
        return x in self._data

    def __len__(self):
        return len(self._data)

    def __iter__(self):
        for i in self._data:
            yield i


def coerce_to_list(obj):
    if not isinstance(obj, (list, tuple)):
        return [obj]
    return obj


def clean_property_name(name: str):
    return name.split("@")[0]


def hooks_to_js_object(hooks: Union[RendererHooks, None]) -> str:
    if hooks is None:
        return ""
    hook_str = ",".join(f"{key}: {val}" for key, val in hooks.items())

    return f"{{{hook_str}}}"


def parse_version(version):
    return tuple(int(s) for s in version.split("."))


def get_caller_name():
    stack = inspect.stack()
    for s in stack:
        if s.function == "<module>":
            return s.frame.f_locals.get("__name__", "__main__")

    return "__main__"



File: dash/_pages.py

import collections
import importlib
import os
import re
import sys
from fnmatch import fnmatch
from pathlib import Path
from os.path import isfile, join
from urllib.parse import parse_qs

import flask

from . import _validate
from ._utils import AttributeDict
from ._get_paths import get_relative_path
from ._callback_context import context_value
from ._get_app import get_app


CONFIG = AttributeDict()
PAGE_REGISTRY = collections.OrderedDict()


def _infer_image(module):
    """
    Return:
    - A page specific image: `assets/<module>.<extension>` is used, e.g. `assets/weekly_analytics.png`
    - A generic app image at `assets/app.<extension>`
    - A logo at `assets/logo.<extension>`
    """
    assets_folder = CONFIG.assets_folder
    valid_extensions = ["apng", "avif", "gif", "jpeg", "jpg", "png", "svg", "webp"]
    page_id = module.split(".")[-1]
    files_in_assets = []

    if os.path.exists(assets_folder):
        files_in_assets = [
            f for f in os.listdir(assets_folder) if isfile(join(assets_folder, f))
        ]
    app_file = None
    logo_file = None
    for fn in files_in_assets:
        fn_without_extension, _, extension = fn.partition(".")
        if extension.lower() in valid_extensions:
            if (
                fn_without_extension == page_id
                or fn_without_extension == page_id.replace("_", "-")
            ):
                return fn

            if fn_without_extension == "app":
                app_file = fn

            if fn_without_extension == "logo":
                logo_file = fn

    if app_file:
        return app_file

    return logo_file


def _module_name_to_page_name(module_name):
    return module_name.split(".")[-1].replace("_", " ").capitalize()


def _infer_path(module_name, template):
    if template is None:
        if CONFIG.pages_folder:
            pages_module = str(Path(CONFIG.pages_folder).name)
            path = (
                module_name.split(pages_module)[-1]
                .replace("_", "-")
                .replace(".", "/")
                .lower()
            )
        else:
            path = module_name.replace("_", "-").replace(".", "/").lower()
    else:
        # replace the variables in the template with "none" to create a default path if
        # no path is supplied
        path = re.sub("<.*?>", "none", template)
    path = "/" + path if not path.startswith("/") else path
    return path


def _module_name_is_package(module_name):
    return (
        module_name in sys.modules
        and Path(sys.modules[module_name].__file__).name == "__init__.py"
    )


def _path_to_module_name(path):
    return str(path).replace(".py", "").strip(os.sep).replace(os.sep, ".")


def _infer_module_name(page_path):
    relative_path = page_path.split(CONFIG.pages_folder)[-1]
    module = _path_to_module_name(relative_path)
    proj_root = flask.helpers.get_root_path(CONFIG.name)
    if CONFIG.pages_folder.startswith(proj_root):
        parent_path = CONFIG.pages_folder[len(proj_root) :]
    else:
        parent_path = CONFIG.pages_folder
    parent_module = _path_to_module_name(parent_path)

    module_name = f"{parent_module}.{module}"
    if _module_name_is_package(CONFIG.name):
        # Only prefix with CONFIG.name when it's an imported package name
        module_name = f"{CONFIG.name}.{module_name}"
    return module_name


def _parse_query_string(search):
    if search and len(search) > 0 and search[0] == "?":
        search = search[1:]
    else:
        return {}

    parsed_qs = {}
    for (k, v) in parse_qs(search).items():
        v = v[0] if len(v) == 1 else v
        parsed_qs[k] = v
    return parsed_qs


def _parse_path_variables(pathname, path_template):
    """
    creates the dict of path variables passed to the layout
    e.g. path_template= "/asset/<asset_id>"
         if pathname provided by the browser is "/assets/a100"
         returns **{"asset_id": "a100"}
    """

    # parse variable definitions e.g. <var_name> from template
    # and create pattern to match
    wildcard_pattern = re.sub("<.*?>", "*", path_template)
    var_pattern = re.sub("<.*?>", "(.*)", path_template)

    # check that static sections of the pathname match the template
    if not fnmatch(pathname, wildcard_pattern):
        return None

    # parse variable names e.g. var_name from template
    var_names = re.findall("<(.*?)>", path_template)

    # parse variables from path
    variables = re.findall(var_pattern, pathname)
    variables = variables[0] if isinstance(variables[0], tuple) else variables

    return dict(zip(var_names, variables))


def _create_redirect_function(redirect_to):
    def redirect():
        return flask.redirect(redirect_to, code=301)

    return redirect


def _set_redirect(redirect_from, path):
    app = get_app()
    if redirect_from and len(redirect_from):
        for redirect in redirect_from:
            fullname = app.get_relative_path(redirect)
            app.server.add_url_rule(
                fullname,
                fullname,
                _create_redirect_function(app.get_relative_path(path)),
            )


def register_page(
    module,
    path=None,
    path_template=None,
    name=None,
    order=None,
    title=None,
    description=None,
    image=None,
    image_url=None,
    redirect_from=None,
    layout=None,
    **kwargs,
):
    """
    Assigns the variables to `dash.page_registry` as an `OrderedDict`
    (ordered by `order`).

    `dash.page_registry` is used by `pages_plugin` to set up the layouts as
    a multi-page Dash app. This includes the URL routing callbacks
    (using `dcc.Location`) and the HTML templates to include title,
    meta description, and the meta description image.

    `dash.page_registry` can also be used by Dash developers to create the
    page navigation links or by template authors.

    - `module`:
       The module path where this page's `layout` is defined. Often `__name__`.

    - `path`:
       URL Path, e.g. `/` or `/home-page`.
       If not supplied, will be inferred from the `path_template` or `module`,
       e.g. based on path_template: `/asset/<asset_id` to `/asset/none`
       e.g. based on module: `pages.weekly_analytics` to `/weekly-analytics`

    - `relative_path`:
        The path with `requests_pathname_prefix` prefixed before it.
        Use this path 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
        `relative_path` will be `/my-dash-app/page-2`.

    - `path_template`:
       Add variables to a URL by marking sections with <variable_name>. The layout function
       then receives the <variable_name> as a keyword argument.
       e.g. path_template= "/asset/<asset_id>"
            then if pathname in browser is "/assets/a100" then layout will receive **{"asset_id":"a100"}

    - `name`:
       The name of the link.
       If not supplied, will be inferred from `module`,
       e.g. `pages.weekly_analytics` to `Weekly analytics`

    - `order`:
       The order of the pages in `page_registry`.
       If not supplied, then the filename is used and the page with path `/` has
       order `0`

    - `title`:
       (string or function) Specifies the page title displayed in the browser tab.
        If not supplied, the app's title is used if different from the default "Dash".
        Otherwise, the title is the given `name` or inferred from the module name.
        For example, `pages.weekly_analytics` is inferred as "Weekly Analytics".

    - `description`:
       (string or function) The <meta type="description"></meta>.
       If not defined, the application description will be used if available.

    - `image`:
       The meta description image used by social media platforms.
       If not supplied, then it looks for the following images in `assets/`:
        - A page specific image: `assets/<module>.<extension>` is used, e.g. `assets/weekly_analytics.png`
        - A generic app image at `assets/app.<extension>`
        - A logo at `assets/logo.<extension>`
        When inferring the image file, it will look for the following extensions:
        APNG, AVIF, GIF, JPEG, JPG, PNG, SVG, WebP.

    -  `image_url`:
       Overrides the image property and sets the `<image>` meta tag to the provided image URL.

    - `redirect_from`:
       A list of paths that should redirect to this page.
       For example: `redirect_from=['/v2', '/v3']`

    - `layout`:
       The layout function or component for this page.
       If not supplied, then looks for `layout` from within the supplied `module`.

    - `**kwargs`:
       Arbitrary keyword arguments that can be stored

    ***

    `page_registry` stores the original property that was passed in under
    `supplied_<property>` and the coerced property under `<property>`.
    For example, if this was called:
    ```
    register_page(
        'pages.historical_outlook',
        name='Our historical view',
        custom_key='custom value'
    )
    ```
    Then this will appear in `page_registry`:
    ```
    OrderedDict([
        (
            'pages.historical_outlook',
            dict(
                module='pages.historical_outlook',

                supplied_path=None,
                path='/historical-outlook',

                supplied_name='Our historical view',
                name='Our historical view',

                supplied_title=None,
                title='Our historical view'

                supplied_layout=None,
                layout=<function pages.historical_outlook.layout>,

                custom_key='custom value'
            )
        ),
    ])
    ```
    """
    if context_value.get().get("ignore_register_page"):
        return

    _validate.validate_use_pages(CONFIG)

    page = dict(
        module=_validate.validate_module_name(module),
        supplied_path=path,
        path_template=path_template,
        path=path if path is not None else _infer_path(module, path_template),
        supplied_name=name,
        name=name if name is not None else _module_name_to_page_name(module),
    )
    page.update(
        supplied_title=title,
        title=title
        if title is not None
        else CONFIG.title
        if CONFIG.title != "Dash"
        else page["name"],
    )
    page.update(
        description=description
        if description
        else CONFIG.description
        if CONFIG.description
        else "",
        order=order,
        supplied_order=order,
        supplied_layout=layout,
        **kwargs,
    )
    page.update(
        supplied_image=image,
        image=(image if image is not None else _infer_image(module)),
        image_url=image_url,
    )
    page.update(redirect_from=_set_redirect(redirect_from, page["path"]))

    PAGE_REGISTRY[module] = page

    if page["path_template"]:
        _validate.validate_template(page["path_template"])

    if layout is not None:
        # Override the layout found in the file set during `plug`
        PAGE_REGISTRY[module]["layout"] = layout

    # set home page order
    order_supplied = any(
        p["supplied_order"] is not None for p in PAGE_REGISTRY.values()
    )

    for p in PAGE_REGISTRY.values():
        p["order"] = (
            0 if p["path"] == "/" and not order_supplied else p["supplied_order"]
        )
        p["relative_path"] = get_relative_path(p["path"])

    # Sort numeric orders first, then string orders, then no order,
    # finally by module name for matching orders
    for page in sorted(
        PAGE_REGISTRY.values(),
        key=lambda i: (
            i["order"] is None,  # False (order given) sorts before True
            i["order"] if isinstance(i["order"], (int, float)) else float("inf"),
            str(i["order"]),
            i["module"],
        ),
    ):
        PAGE_REGISTRY.move_to_end(page["module"])


def _path_to_page(path_id):
    path_variables = None
    for page in PAGE_REGISTRY.values():
        if page["path_template"]:
            template_id = page["path_template"].strip("/")
            path_variables = _parse_path_variables(path_id, template_id)
            if path_variables:
                return page, path_variables
        if path_id == page["path"].strip("/"):
            return page, path_variables
    return {}, None


def _page_meta_tags(app):
    start_page, path_variables = _path_to_page(flask.request.path.strip("/"))

    # use the supplied image_url or create url based on image in the assets folder
    image = start_page.get("image", "")
    if image:
        image = app.get_asset_url(image)
    assets_image_url = (
        "".join([flask.request.url_root, image.lstrip("/")]) if image else None
    )
    supplied_image_url = start_page.get("image_url")
    image_url = supplied_image_url if supplied_image_url else assets_image_url

    title = start_page.get("title", app.title)
    if callable(title):
        title = title(**path_variables) if path_variables else title()

    description = start_page.get("description", "")
    if callable(description):
        description = description(**path_variables) if path_variables else description()

    return [
        {"name": "description", "content": description},
        {"property": "twitter:card", "content": "summary_large_image"},
        {"property": "twitter:url", "content": flask.request.url},
        {"property": "twitter:title", "content": title},
        {"property": "twitter:description", "content": description},
        {"property": "twitter:image", "content": image_url or ""},
        {"property": "og:title", "content": title},
        {"property": "og:type", "content": "website"},
        {"property": "og:description", "content": description},
        {"property": "og:image", "content": image_url or ""},
    ]


def _import_layouts_from_pages(pages_folder):
    for root, dirs, files in os.walk(pages_folder):
        dirs[:] = [d for d in dirs if not d.startswith(".") and not d.startswith("_")]
        for file in files:
            if file.startswith("_") or file.startswith(".") or not file.endswith(".py"):
                continue
            page_path = os.path.join(root, file)
            with open(page_path, encoding="utf-8") as f:
                content = f.read()
                if "register_page" not in content:
                    continue

            module_name = _infer_module_name(page_path)
            spec = importlib.util.spec_from_file_location(module_name, page_path)
            page_module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(page_module)
            sys.modules[module_name] = page_module

            if (
                module_name in PAGE_REGISTRY
                and not PAGE_REGISTRY[module_name]["supplied_layout"]
            ):
                _validate.validate_pages_layout(module_name, page_module)
                PAGE_REGISTRY[module_name]["layout"] = getattr(page_module, "layout")



File: dash/development/_collect_nodes.py

def is_node(value):
    return value in ("node", "element")


def is_shape(value):
    return value in ("shape", "exact")


def collect_array(a_value, base, nodes):
    a_type = a_value["name"]
    if is_node(a_type):
        nodes.append(base)
    elif a_type in ("shape", "exact"):
        nodes = collect_nodes(a_value["value"], base + "[]", nodes)
    elif a_type == "union":
        nodes = collect_union(a_value["value"], base + "[]", nodes)
    elif a_type == "objectOf":
        nodes = collect_object(a_value["value"], base + "[]", nodes)
    return nodes


def collect_union(type_list, base, nodes):
    for t in type_list:
        if is_node(t["name"]):
            nodes.append(base)
        elif is_shape(t["name"]):
            nodes = collect_nodes(t["value"], base, nodes)
        elif t["name"] == "arrayOf":
            nodes = collect_array(t["value"], base, nodes)
        elif t["name"] == "objectOf":
            nodes = collect_object(t["value"], base, nodes)
    return nodes


def collect_object(o_value, base, nodes):
    o_name = o_value.get("name")
    o_key = base + "{}"
    if is_node(o_name):
        nodes.append(o_key)
    elif is_shape(o_name):
        nodes = collect_nodes(o_value.get("value", {}), o_key, nodes)
    elif o_name == "union":
        nodes = collect_union(o_value.get("value"), o_key, nodes)
    elif o_name == "arrayOf":
        nodes = collect_array(o_value, o_key, nodes)
    return nodes


def collect_nodes(metadata, base="", nodes=None):
    nodes = nodes or []

    for prop_name, value in metadata.items():
        # Support for recursive shapes, the type is directly in the field.
        t_value = value.get("type", value)
        p_type = t_value.get("name")

        if base:
            key = f"{base}.{prop_name}"
        else:
            key = prop_name
        if is_node(p_type):
            nodes.append(key)
        elif p_type == "arrayOf":
            a_value = t_value.get("value", t_value)
            nodes = collect_array(a_value, key, nodes)
        elif is_shape(p_type):
            nodes = collect_nodes(t_value["value"], key, nodes)
        elif p_type == "union":
            nodes = collect_union(t_value["value"], key, nodes)
        elif p_type == "objectOf":
            o_value = t_value.get("value", {})
            nodes = collect_object(o_value, key, nodes)

    return nodes


def filter_base_nodes(nodes):
    return [n for n in nodes if not any(e in n for e in ("[]", ".", "{}"))]



File: dash/development/component_loader.py

import collections
import json
import os
import warnings


from ._py_components_generation import (
    generate_class_file,
    generate_imports,
    generate_classes_files,
    generate_class,
)
from .base_component import ComponentRegistry


def _get_metadata(metadata_path):
    # Start processing
    with open(metadata_path, encoding="utf-8") as data_file:
        json_string = data_file.read()
        data = json.JSONDecoder(object_pairs_hook=collections.OrderedDict).decode(
            json_string
        )
    return data


def load_components(metadata_path, namespace="default_namespace"):
    """Load React component metadata into a format Dash can parse.

    Usage: load_components('../../component-suites/lib/metadata.json')

    Keyword arguments:
    metadata_path -- a path to a JSON file created by
    [`react-docgen`](https://github.com/reactjs/react-docgen).

    Returns:
    components -- a list of component objects with keys
    `type`, `valid_kwargs`, and `setup`.
    """
    warnings.warn(
        DeprecationWarning(
            "Dynamic components loading has been deprecated and will be removed"
            " in dash 3.0.\n"
            f"Update {namespace} to generate components with dash-generate-components"
        )
    )
    # Register the component lib for index include.
    ComponentRegistry.registry.add(namespace)
    components = []

    data = _get_metadata(metadata_path)

    # Iterate over each property name (which is a path to the component)
    for componentPath in data:
        componentData = data[componentPath]

        # Extract component name from path
        # e.g. src/components/MyControl.react.js
        # TODO Make more robust - some folks will write .jsx and others
        # will be on windows. Unfortunately react-docgen doesn't include
        # the name of the component atm.
        name = componentPath.split("/").pop().split(".")[0]
        component = generate_class(
            name, componentData["props"], componentData["description"], namespace, None
        )

        components.append(component)

    return components


def generate_classes(namespace, metadata_path="lib/metadata.json"):
    """Load React component metadata into a format Dash can parse, then create
    Python class files.

    Usage: generate_classes()

    Keyword arguments:
    namespace -- name of the generated Python package (also output dir)

    metadata_path -- a path to a JSON file created by
    [`react-docgen`](https://github.com/reactjs/react-docgen).

    Returns:
    """

    data = _get_metadata(metadata_path)
    imports_path = os.path.join(namespace, "_imports_.py")

    # Make sure the file doesn't exist, as we use append write
    if os.path.exists(imports_path):
        os.remove(imports_path)

    components = generate_classes_files(namespace, data, generate_class_file)

    # Add the __all__ value so we can import * from _imports_
    generate_imports(namespace, components)



File: dash/development/base_component.py

import abc
import collections
import inspect
import sys
import uuid
import random
import warnings
import textwrap

from .._utils import patch_collections_abc, stringify_id, OrderedSet

MutableSequence = patch_collections_abc("MutableSequence")

rd = random.Random(0)

_deprecated_components = {
    "dash_core_components": {
        "LogoutButton": textwrap.dedent(
            """
        The Logout Button is no longer used with Dash Enterprise and can be replaced with a html.Button or html.A.
        eg: html.A(href=os.getenv('DASH_LOGOUT_URL'))
    """
        )
    }
}


# pylint: disable=no-init,too-few-public-methods
class ComponentRegistry:
    """Holds a registry of the namespaces used by components."""

    registry = OrderedSet()
    children_props = collections.defaultdict(dict)
    namespace_to_package = {}

    @classmethod
    def get_resources(cls, resource_name, includes=None):
        resources = []

        for module_name in cls.registry:
            if includes is not None and module_name not in includes:
                continue
            module = sys.modules[module_name]
            resources.extend(getattr(module, resource_name, []))

        return resources


class ComponentMeta(abc.ABCMeta):

    # pylint: disable=arguments-differ
    def __new__(mcs, name, bases, attributes):
        component = abc.ABCMeta.__new__(mcs, name, bases, attributes)
        module = attributes["__module__"].split(".")[0]
        if name == "Component" or module == "builtins":
            # Don't do the base component
            # and the components loaded dynamically by load_component
            # as it doesn't have the namespace.
            return component

        _namespace = attributes.get("_namespace", module)
        ComponentRegistry.namespace_to_package[_namespace] = module
        ComponentRegistry.registry.add(module)
        ComponentRegistry.children_props[_namespace][name] = attributes.get(
            "_children_props"
        )

        return component


def is_number(s):
    try:
        float(s)
        return True
    except ValueError:
        return False


def _check_if_has_indexable_children(item):
    if not hasattr(item, "children") or (
        not isinstance(item.children, Component)
        and not isinstance(item.children, (tuple, MutableSequence))
    ):

        raise KeyError


class Component(metaclass=ComponentMeta):
    _children_props = []
    _base_nodes = ["children"]

    class _UNDEFINED:
        def __repr__(self):
            return "undefined"

        def __str__(self):
            return "undefined"

    UNDEFINED = _UNDEFINED()

    class _REQUIRED:
        def __repr__(self):
            return "required"

        def __str__(self):
            return "required"

    REQUIRED = _REQUIRED()

    def __init__(self, **kwargs):
        self._validate_deprecation()
        import dash  # pylint: disable=import-outside-toplevel, cyclic-import

        # pylint: disable=super-init-not-called
        for k, v in list(kwargs.items()):
            # pylint: disable=no-member
            k_in_propnames = k in self._prop_names
            k_in_wildcards = any(
                k.startswith(w) for w in self._valid_wildcard_attributes
            )
            # e.g. "The dash_core_components.Dropdown component (version 1.6.0)
            # with the ID "my-dropdown"
            id_suffix = f' with the ID "{kwargs["id"]}"' if "id" in kwargs else ""
            try:
                # Get fancy error strings that have the version numbers
                error_string_prefix = "The `{}.{}` component (version {}){}"
                # These components are part of dash now, so extract the dash version:
                dash_packages = {
                    "dash_html_components": "html",
                    "dash_core_components": "dcc",
                    "dash_table": "dash_table",
                }
                if self._namespace in dash_packages:
                    error_string_prefix = error_string_prefix.format(
                        dash_packages[self._namespace],
                        self._type,
                        dash.__version__,
                        id_suffix,
                    )
                else:
                    # Otherwise import the package and extract the version number
                    error_string_prefix = error_string_prefix.format(
                        self._namespace,
                        self._type,
                        getattr(__import__(self._namespace), "__version__", "unknown"),
                        id_suffix,
                    )
            except ImportError:
                # Our tests create mock components with libraries that
                # aren't importable
                error_string_prefix = f"The `{self._type}` component{id_suffix}"

            if not k_in_propnames and not k_in_wildcards:
                allowed_args = ", ".join(
                    sorted(self._prop_names)
                )  # pylint: disable=no-member
                raise TypeError(
                    f"{error_string_prefix} received an unexpected keyword argument: `{k}`"
                    f"\nAllowed arguments: {allowed_args}"
                )

            if k not in self._base_nodes and isinstance(v, Component):
                raise TypeError(
                    error_string_prefix
                    + " detected a Component for a prop other than `children`\n"
                    + f"Prop {k} has value {v!r}\n\n"
                    + "Did you forget to wrap multiple `children` in an array?\n"
                    + 'For example, it must be html.Div(["a", "b", "c"]) not html.Div("a", "b", "c")\n'
                )

            if k == "id":
                if isinstance(v, dict):
                    for id_key, id_val in v.items():
                        if not isinstance(id_key, str):
                            raise TypeError(
                                "dict id keys must be strings,\n"
                                + f"found {id_key!r} in id {v!r}"
                            )
                        if not isinstance(id_val, (str, int, float, bool)):
                            raise TypeError(
                                "dict id values must be strings, numbers or bools,\n"
                                + f"found {id_val!r} in id {v!r}"
                            )
                elif not isinstance(v, str):
                    raise TypeError(f"`id` prop must be a string or dict, not {v!r}")

            setattr(self, k, v)

    def _set_random_id(self):

        if hasattr(self, "id"):
            return getattr(self, "id")

        kind = f"`{self._namespace}.{self._type}`"  # pylint: disable=no-member

        if getattr(self, "persistence", False):
            raise RuntimeError(
                f"""
                Attempting to use an auto-generated ID with the `persistence` prop.
                This is prohibited because persistence is tied to component IDs and
                auto-generated IDs can easily change.

                Please assign an explicit ID to this {kind} component.
                """
            )
        if "dash_snapshots" in sys.modules:
            raise RuntimeError(
                f"""
                Attempting to use an auto-generated ID in an app with `dash_snapshots`.
                This is prohibited because snapshots saves the whole app layout,
                including component IDs, and auto-generated IDs can easily change.
                Callbacks referencing the new IDs will not work with old snapshots.

                Please assign an explicit ID to this {kind} component.
                """
            )

        v = str(uuid.UUID(int=rd.randint(0, 2**128)))
        setattr(self, "id", v)
        return v

    def to_plotly_json(self):
        # Add normal properties
        props = {
            p: getattr(self, p)
            for p in self._prop_names  # pylint: disable=no-member
            if hasattr(self, p)
        }
        # Add the wildcard properties data-* and aria-*
        props.update(
            {
                k: getattr(self, k)
                for k in self.__dict__
                if any(
                    k.startswith(w)
                    # pylint:disable=no-member
                    for w in self._valid_wildcard_attributes
                )
            }
        )
        as_json = {
            "props": props,
            "type": self._type,  # pylint: disable=no-member
            "namespace": self._namespace,  # pylint: disable=no-member
        }

        return as_json

    # pylint: disable=too-many-branches, too-many-return-statements
    # pylint: disable=redefined-builtin, inconsistent-return-statements
    def _get_set_or_delete(self, id, operation, new_item=None):
        _check_if_has_indexable_children(self)

        # pylint: disable=access-member-before-definition,
        # pylint: disable=attribute-defined-outside-init
        if isinstance(self.children, Component):
            if getattr(self.children, "id", None) is not None:
                # Woohoo! It's the item that we're looking for
                if self.children.id == id:
                    if operation == "get":
                        return self.children
                    if operation == "set":
                        self.children = new_item
                        return
                    if operation == "delete":
                        self.children = None
                        return

            # Recursively dig into its subtree
            try:
                if operation == "get":
                    return self.children.__getitem__(id)
                if operation == "set":
                    self.children.__setitem__(id, new_item)
                    return
                if operation == "delete":
                    self.children.__delitem__(id)
                    return
            except KeyError:
                pass

        # if children is like a list
        if isinstance(self.children, (tuple, MutableSequence)):
            for i, item in enumerate(self.children):
                # If the item itself is the one we're looking for
                if getattr(item, "id", None) == id:
                    if operation == "get":
                        return item
                    if operation == "set":
                        self.children[i] = new_item
                        return
                    if operation == "delete":
                        del self.children[i]
                        return

                # Otherwise, recursively dig into that item's subtree
                # Make sure it's not like a string
                elif isinstance(item, Component):
                    try:
                        if operation == "get":
                            return item.__getitem__(id)
                        if operation == "set":
                            item.__setitem__(id, new_item)
                            return
                        if operation == "delete":
                            item.__delitem__(id)
                            return
                    except KeyError:
                        pass

        # The end of our branch
        # If we were in a list, then this exception will get caught
        raise KeyError(id)

    # Magic methods for a mapping interface:
    # - __getitem__
    # - __setitem__
    # - __delitem__
    # - __iter__
    # - __len__

    def __getitem__(self, id):  # pylint: disable=redefined-builtin
        """Recursively find the element with the given ID through the tree of
        children."""

        # A component's children can be undefined, a string, another component,
        # or a list of components.
        return self._get_set_or_delete(id, "get")

    def __setitem__(self, id, item):  # pylint: disable=redefined-builtin
        """Set an element by its ID."""
        return self._get_set_or_delete(id, "set", item)

    def __delitem__(self, id):  # pylint: disable=redefined-builtin
        """Delete items by ID in the tree of children."""
        return self._get_set_or_delete(id, "delete")

    def _traverse(self):
        """Yield each item in the tree."""
        for t in self._traverse_with_paths():
            yield t[1]

    @staticmethod
    def _id_str(component):
        id_ = stringify_id(getattr(component, "id", ""))
        return id_ and f" (id={id_:s})"

    def _traverse_with_paths(self):
        """Yield each item with its path in the tree."""
        children = getattr(self, "children", None)
        children_type = type(children).__name__
        children_string = children_type + self._id_str(children)

        # children is just a component
        if isinstance(children, Component):
            yield "[*] " + children_string, children
            # pylint: disable=protected-access
            for p, t in children._traverse_with_paths():
                yield "\n".join(["[*] " + children_string, p]), t

        # children is a list of components
        elif isinstance(children, (tuple, MutableSequence)):
            for idx, i in enumerate(children):
                list_path = f"[{idx:d}] {type(i).__name__:s}{self._id_str(i)}"
                yield list_path, i

                if isinstance(i, Component):
                    # pylint: disable=protected-access
                    for p, t in i._traverse_with_paths():
                        yield "\n".join([list_path, p]), t

    def _traverse_ids(self):
        """Yield components with IDs in the tree of children."""
        for t in self._traverse():
            if isinstance(t, Component) and getattr(t, "id", None) is not None:
                yield t

    def __iter__(self):
        """Yield IDs in the tree of children."""
        for t in self._traverse_ids():
            yield t.id

    def __len__(self):
        """Return the number of items in the tree."""
        # TODO - Should we return the number of items that have IDs
        # or just the number of items?
        # The number of items is more intuitive but returning the number
        # of IDs matches __iter__ better.
        length = 0
        if getattr(self, "children", None) is None:
            length = 0
        elif isinstance(self.children, Component):
            length = 1
            length += len(self.children)
        elif isinstance(self.children, (tuple, MutableSequence)):
            for c in self.children:
                length += 1
                if isinstance(c, Component):
                    length += len(c)
        else:
            # string or number
            length = 1
        return length

    def __repr__(self):
        # pylint: disable=no-member
        props_with_values = [
            c for c in self._prop_names if getattr(self, c, None) is not None
        ] + [
            c
            for c in self.__dict__
            if any(c.startswith(wc_attr) for wc_attr in self._valid_wildcard_attributes)
        ]
        if any(p != "children" for p in props_with_values):
            props_string = ", ".join(
                f"{p}={getattr(self, p)!r}" for p in props_with_values
            )
        else:
            props_string = repr(getattr(self, "children", None))
        return f"{self._type}({props_string})"

    def _validate_deprecation(self):
        _type = getattr(self, "_type", "")
        _ns = getattr(self, "_namespace", "")
        deprecation_message = _deprecated_components.get(_ns, {}).get(_type)
        if deprecation_message:
            warnings.warn(DeprecationWarning(textwrap.dedent(deprecation_message)))


def _explicitize_args(func):
    # Python 2
    if hasattr(func, "func_code"):
        varnames = func.func_code.co_varnames
    # Python 3
    else:
        varnames = func.__code__.co_varnames

    def wrapper(*args, **kwargs):
        if "_explicit_args" in kwargs:
            raise Exception("Variable _explicit_args should not be set.")
        kwargs["_explicit_args"] = list(
            set(list(varnames[: len(args)]) + [k for k, _ in kwargs.items()])
        )
        if "self" in kwargs["_explicit_args"]:
            kwargs["_explicit_args"].remove("self")
        return func(*args, **kwargs)

    # If Python 3, we can set the function signature to be correct
    if hasattr(inspect, "signature"):
        # pylint: disable=no-member
        new_sig = inspect.signature(wrapper).replace(
            parameters=inspect.signature(func).parameters.values()
        )
        wrapper.__signature__ = new_sig
    return wrapper



File: dash/development/_jl_components_generation.py

# pylint: disable=consider-using-f-string
import copy
import os
import shutil
import warnings
import sys
import importlib
import uuid
import hashlib

from ._all_keywords import julia_keywords
from ._py_components_generation import reorder_props

# uuid of DashBase Julia package.
jl_dash_base_uuid = "03207cf0-e2b3-4b91-9ca8-690cf0fb507e"

# uuid of Dash Julia package. Used as base for component package uuid
jl_dash_uuid = "1b08a953-4be3-4667-9a23-3db579824955"

# Declaring longer string templates as globals to improve
# readability, make method logic clearer to anyone inspecting
# code below
jl_component_string = '''
export {funcname}

"""
    {funcname}(;kwargs...){children_signatures}

{docstring}
"""
function {funcname}(; kwargs...)
        available_props = Symbol[{component_props}]
        wild_props = Symbol[{wildcard_symbols}]
        return Component("{funcname}", "{element_name}", "{module_name}", available_props, wild_props; kwargs...)
end
{children_definitions}
'''  # noqa:E501

jl_children_signatures = """
    {funcname}(children::Any;kwargs...)
    {funcname}(children_maker::Function;kwargs...)
"""

jl_children_definitions = """
{funcname}(children::Any; kwargs...) = {funcname}(;kwargs..., children = children)
{funcname}(children_maker::Function; kwargs...) = {funcname}(children_maker(); kwargs...)
"""

jl_package_file_string = """
module {package_name}
using {base_package}

const resources_path = realpath(joinpath( @__DIR__, "..", "deps"))
const version = "{version}"

{component_includes}

function __init__()
    DashBase.register_package(
        DashBase.ResourcePkg(
            "{project_shortname}",
            resources_path,
            version = version,
            [
                {resources_dist}
            ]
        )

    )
end
end
"""

jl_projecttoml_string = """
name = "{package_name}"
uuid = "{package_uuid}"
{authors}version = "{version}"

[deps]
{base_package} = "{dash_uuid}"

[compat]
julia = "1.2"
{base_package} = "{base_version}"
"""

jl_base_version = {
    "Dash": "0.1.3, 1.0",
    "DashBase": "0.1",
}

jl_component_include_string = 'include("jl/{name}.jl")'

jl_resource_tuple_string = """DashBase.Resource(
    relative_package_path = {relative_package_path},
    external_url = {external_url},
    dynamic = {dynamic},
    async = {async_string},
    type = :{type}
)"""

core_packages = ["dash_html_components", "dash_core_components", "dash_table"]


def jl_package_name(namestring):
    s = namestring.split("_")
    return "".join(w.capitalize() for w in s)


def stringify_wildcards(wclist, no_symbol=False):
    if no_symbol:
        wcstring = "|".join("{}-".format(item) for item in wclist)
    else:
        wcstring = ", ".join('Symbol("{}-")'.format(item) for item in wclist)
    return wcstring


def get_wildcards_jl(props):
    return [key.replace("-*", "") for key in props if key.endswith("-*")]


def get_jl_prop_types(type_object):
    """Mapping from the PropTypes js type object to the Julia type."""

    def shape_or_exact():
        return "lists containing elements {}.\n{}".format(
            ", ".join("'{}'".format(t) for t in type_object["value"]),
            "Those elements have the following types:\n{}".format(
                "\n".join(
                    create_prop_docstring_jl(
                        prop_name=prop_name,
                        type_object=prop,
                        required=prop["required"],
                        description=prop.get("description", ""),
                        indent_num=1,
                    )
                    for prop_name, prop in type_object["value"].items()
                )
            ),
        )

    return dict(
        array=lambda: "Array",
        bool=lambda: "Bool",
        number=lambda: "Real",
        string=lambda: "String",
        object=lambda: "Dict",
        any=lambda: "Bool | Real | String | Dict | Array",
        element=lambda: "dash component",
        node=lambda: "a list of or a singular dash component, string or number",
        # React's PropTypes.oneOf
        enum=lambda: "a value equal to: {}".format(
            ", ".join("{}".format(str(t["value"])) for t in type_object["value"])
        ),
        # React's PropTypes.oneOfType
        union=lambda: "{}".format(
            " | ".join(
                "{}".format(get_jl_type(subType))
                for subType in type_object["value"]
                if get_jl_type(subType) != ""
            )
        ),
        # React's PropTypes.arrayOf
        arrayOf=lambda: (
            "Array"
            + (
                " of {}s".format(get_jl_type(type_object["value"]))
                if get_jl_type(type_object["value"]) != ""
                else ""
            )
        ),
        # React's PropTypes.objectOf
        objectOf=lambda: "Dict with Strings as keys and values of type {}".format(
            get_jl_type(type_object["value"])
        ),
        # React's PropTypes.shape
        shape=shape_or_exact,
        # React's PropTypes.exact
        exact=shape_or_exact,
    )


def filter_props(props):
    """Filter props from the Component arguments to exclude:
        - Those without a "type" or a "flowType" field
        - Those with arg.type.name in {'func', 'symbol', 'instanceOf'}
    Parameters
    ----------
    props: dict
        Dictionary with {propName: propMetadata} structure
    Returns
    -------
    dict
        Filtered dictionary with {propName: propMetadata} structure
    """
    filtered_props = copy.deepcopy(props)

    for arg_name, arg in list(filtered_props.items()):
        if "type" not in arg and "flowType" not in arg:
            filtered_props.pop(arg_name)
            continue

        # Filter out functions and instances --
        if "type" in arg:  # These come from PropTypes
            arg_type = arg["type"]["name"]
            if arg_type in {"func", "symbol", "instanceOf"}:
                filtered_props.pop(arg_name)
        elif "flowType" in arg:  # These come from Flow & handled differently
            arg_type_name = arg["flowType"]["name"]
            if arg_type_name == "signature":
                # This does the same as the PropTypes filter above, but "func"
                # is under "type" if "name" is "signature" vs just in "name"
                if "type" not in arg["flowType"] or arg["flowType"]["type"] != "object":
                    filtered_props.pop(arg_name)
        else:
            raise ValueError

    return filtered_props


def get_jl_type(type_object):
    """
    Convert JS types to Julia types for the component definition
    Parameters
    ----------
    type_object: dict
        react-docgen-generated prop type dictionary
    Returns
    -------
    str
        Julia type string
    """
    js_type_name = type_object["name"]
    js_to_jl_types = get_jl_prop_types(type_object=type_object)
    if js_type_name in js_to_jl_types:
        prop_type = js_to_jl_types[js_type_name]()
        return prop_type
    return ""


def print_jl_type(typedata):
    typestring = get_jl_type(typedata).capitalize()
    if typestring:
        typestring += ". "
    return typestring


def create_docstring_jl(component_name, props, description):
    """Create the Dash component docstring.
    Parameters
    ----------
    component_name: str
        Component name
    props: dict
        Dictionary with {propName: propMetadata} structure
    description: str
        Component description
    Returns
    -------
    str
        Dash component docstring
    """
    # Ensure props are ordered with children first
    props = reorder_props(props=props)

    return "A{n} {name} component.\n{description}\nKeyword arguments:\n{args}".format(
        n="n" if component_name[0].lower() in "aeiou" else "",
        name=component_name,
        description=description,
        args="\n".join(
            create_prop_docstring_jl(
                prop_name=p,
                type_object=prop["type"] if "type" in prop else prop["flowType"],
                required=prop["required"],
                description=prop["description"],
                indent_num=0,
            )
            for p, prop in filter_props(props).items()
        ),
    )


def create_prop_docstring_jl(
    prop_name,
    type_object,
    required,
    description,
    indent_num,
):
    """
    Create the Dash component prop docstring
    Parameters
    ----------
    prop_name: str
        Name of the Dash component prop
    type_object: dict
        react-docgen-generated prop type dictionary
    required: bool
        Component is required?
    description: str
        Dash component description
    indent_num: int
        Number of indents to use for the context block
        (creates 2 spaces for every indent)
    is_flow_type: bool
        Does the prop use Flow types? Otherwise, uses PropTypes
    Returns
    -------
    str
        Dash component prop docstring
    """
    jl_type_name = get_jl_type(type_object=type_object)

    indent_spacing = "  " * indent_num
    if "\n" in jl_type_name:
        return (
            "{indent_spacing}- `{name}` ({is_required}): {description}. "
            "{name} has the following type: {type}".format(
                indent_spacing=indent_spacing,
                name=prop_name,
                type=jl_type_name,
                description=description,
                is_required="required" if required else "optional",
            )
        )
    return "{indent_spacing}- `{name}` ({type}{is_required}){description}".format(
        indent_spacing=indent_spacing,
        name=prop_name,
        type="{}; ".format(jl_type_name) if jl_type_name else "",
        description=(": {}".format(description) if description != "" else ""),
        is_required="required" if required else "optional",
    )


# this logic will permit passing blank Julia prefixes to
# dash-generate-components, while also enforcing
# lower case names for the resulting functions; if a prefix
# is supplied, leave it as-is
def format_fn_name(prefix, name):
    if prefix:
        return "{}_{}".format(prefix, name.lower())
    return name.lower()


def generate_metadata_strings(resources, metatype):
    def nothing_or_string(v):
        return '"{}"'.format(v) if v else "nothing"

    return [
        jl_resource_tuple_string.format(
            relative_package_path=nothing_or_string(
                resource.get("relative_package_path", "")
            ),
            external_url=nothing_or_string(resource.get("external_url", "")),
            dynamic=str(resource.get("dynamic", "nothing")).lower(),
            type=metatype,
            async_string=":{}".format(str(resource.get("async")).lower())
            if "async" in resource.keys()
            else "nothing",
        )
        for resource in resources
    ]


def is_core_package(project_shortname):
    return project_shortname in core_packages


def base_package_name(project_shortname):
    return "DashBase" if is_core_package(project_shortname) else "Dash"


def base_package_uid(project_shortname):
    return jl_dash_base_uuid if is_core_package(project_shortname) else jl_dash_uuid


def generate_package_file(project_shortname, components, pkg_data, prefix):
    package_name = jl_package_name(project_shortname)

    sys.path.insert(0, os.getcwd())
    mod = importlib.import_module(project_shortname)
    js_dist = getattr(mod, "_js_dist", [])
    css_dist = getattr(mod, "_css_dist", [])
    project_ver = pkg_data.get("version")

    resources_dist = ",\n".join(
        generate_metadata_strings(js_dist, "js")
        + generate_metadata_strings(css_dist, "css")
    )

    package_string = jl_package_file_string.format(
        package_name=package_name,
        component_includes="\n".join(
            [
                jl_component_include_string.format(
                    name=format_fn_name(prefix, comp_name)
                )
                for comp_name in components
            ]
        ),
        resources_dist=resources_dist,
        version=project_ver,
        project_shortname=project_shortname,
        base_package=base_package_name(project_shortname),
    )
    file_path = os.path.join("src", package_name + ".jl")
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(package_string)
    print("Generated {}".format(file_path))


def generate_toml_file(project_shortname, pkg_data):
    package_author = pkg_data.get("author", "")
    project_ver = pkg_data.get("version")
    package_name = jl_package_name(project_shortname)
    u = uuid.UUID(jl_dash_uuid)

    package_uuid = uuid.UUID(
        hex=u.hex[:-12] + hashlib.sha256(package_name.encode("utf-8")).hexdigest()[-12:]
    )

    authors_string = (
        'authors = ["{}"]\n'.format(package_author) if package_author else ""
    )

    base_package = base_package_name(project_shortname)

    toml_string = jl_projecttoml_string.format(
        package_name=package_name,
        package_uuid=package_uuid,
        version=project_ver,
        authors=authors_string,
        base_package=base_package,
        base_version=jl_base_version[base_package],
        dash_uuid=base_package_uid(project_shortname),
    )
    file_path = "Project.toml"
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(toml_string)
    print("Generated {}".format(file_path))


def generate_class_string(name, props, description, project_shortname, prefix):
    # Ensure props are ordered with children first
    filtered_props = reorder_props(filter_props(props))

    prop_keys = list(filtered_props.keys())

    docstring = (
        create_docstring_jl(
            component_name=name, props=filtered_props, description=description
        )
        .replace("\r\n", "\n")
        .replace("$", "\\$")
    )

    wclist = get_wildcards_jl(props)
    default_paramtext = ""

    # Filter props to remove those we don't want to expose
    for item in prop_keys[:]:
        if item.endswith("-*") or item == "setProps":
            prop_keys.remove(item)
        elif item in julia_keywords:
            prop_keys.remove(item)
            warnings.warn(
                (
                    'WARNING: prop "{}" in component "{}" is a Julia keyword'
                    " - REMOVED FROM THE JULIA COMPONENT"
                ).format(item, name)
            )

    default_paramtext += ", ".join(":{}".format(p) for p in prop_keys)

    has_children = "children" in prop_keys
    funcname = format_fn_name(prefix, name)
    children_signatures = (
        jl_children_signatures.format(funcname=funcname) if has_children else ""
    )
    children_definitions = (
        jl_children_definitions.format(funcname=funcname) if has_children else ""
    )
    return jl_component_string.format(
        funcname=format_fn_name(prefix, name),
        docstring=docstring,
        component_props=default_paramtext,
        wildcard_symbols=stringify_wildcards(wclist, no_symbol=False),
        wildcard_names=stringify_wildcards(wclist, no_symbol=True),
        element_name=name,
        module_name=project_shortname,
        children_signatures=children_signatures,
        children_definitions=children_definitions,
    )


def generate_struct_file(name, props, description, project_shortname, prefix):
    props = reorder_props(props=props)
    import_string = "# AUTO GENERATED FILE - DO NOT EDIT\n"
    class_string = generate_class_string(
        name, props, description, project_shortname, prefix
    )

    file_name = format_fn_name(prefix, name) + ".jl"

    # put component files in src/jl subdir,
    # this also creates the Julia source directory for the package
    # if it is missing
    if not os.path.exists("src/jl"):
        os.makedirs("src/jl")

    file_path = os.path.join("src", "jl", file_name)
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(import_string)
        f.write(class_string)

    print("Generated {}".format(file_name))


# pylint: disable=unused-argument
def generate_module(
    project_shortname, components, metadata, pkg_data, prefix, **kwargs
):
    # copy over all JS dependencies from the (Python) components dir
    # the inst/lib directory for the package won't exist on first call
    # create this directory if it is missing
    if os.path.exists("deps"):
        shutil.rmtree("deps")

    os.makedirs("deps")

    for rel_dirname, _, filenames in os.walk(project_shortname):
        for filename in filenames:
            extension = os.path.splitext(filename)[1]

            if extension in [".py", ".pyc", ".json"]:
                continue

            target_dirname = os.path.join(
                "deps/", os.path.relpath(rel_dirname, project_shortname)
            )

            if not os.path.exists(target_dirname):
                os.makedirs(target_dirname)

            shutil.copy(os.path.join(rel_dirname, filename), target_dirname)

    generate_package_file(project_shortname, components, pkg_data, prefix)
    generate_toml_file(project_shortname, pkg_data)



File: dash/development/__init__.py

from . import base_component  # noqa:F401
from . import component_loader  # noqa:F401



File: dash/development/update_components.py

import sys
import subprocess
import shlex
import os
import argparse
import shutil
import logging
import coloredlogs


class _CombinedFormatter(
    argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter
):
    pass


logger = logging.getLogger(__name__)
coloredlogs.install(
    fmt="%(asctime)s,%(msecs)03d %(levelname)s - %(message)s", datefmt="%H:%M:%S"
)

dest_dir_map = {
    "dash-core-components": "dcc",
    "dash-html-components": "html",
    "dash-table": "dash_table",
}


def status_print(msg, **kwargs):
    try:
        print(msg, **kwargs)
    except UnicodeEncodeError:
        print(msg.encode("ascii", "ignore"), **kwargs)


def bootstrap_components(components_source, concurrency, install_type):

    is_windows = sys.platform == "win32"

    source_glob = (
        components_source
        if components_source != "all"
        else "{dash-core-components,dash-html-components,dash-table}"
    )

    cmdstr = f"npx lerna exec --concurrency {concurrency} --scope='{source_glob}' -- npm {install_type}"
    cmd = shlex.split(cmdstr, posix=not is_windows)
    status_print(cmdstr)

    with subprocess.Popen(
        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=is_windows
    ) as proc:
        out, err = proc.communicate()
        status = proc.poll()

    if err:
        status_print(("🛑 " if status else "") + err.decode(), file=sys.stderr)

    if status or not out:
        status_print(
            f"🚨 Failed installing npm dependencies for component packages: {source_glob} (status={status}) 🚨",
            file=sys.stderr,
        )
        sys.exit(1)
    else:
        status_print(
            f"🟢 Finished installing npm dependencies for component packages: {source_glob} 🟢",
            file=sys.stderr,
        )


def build_components(components_source, concurrency):

    is_windows = sys.platform == "win32"

    source_glob = (
        components_source
        if components_source != "all"
        else "{dash-core-components,dash-html-components,dash-table}"
    )

    cmdstr = f"npx lerna exec --concurrency {concurrency} --scope='{source_glob}' -- npm run build"
    cmd = shlex.split(cmdstr, posix=not is_windows)
    status_print(cmdstr)

    with subprocess.Popen(
        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=is_windows
    ) as proc:
        out, err = proc.communicate()
        status = proc.poll()

    if err:
        status_print(("🛑 " if status else "") + err.decode(), file=sys.stderr)

    if status or not out:
        status_print(
            f"🚨 Finished updating component packages: {source_glob} (status={status}) 🚨",
            file=sys.stderr,
        )
        sys.exit(1)

    if "{" in source_glob:
        source_glob = source_glob.split("{")[1].split("}")[0]

    for package in source_glob.split(","):
        build_directory = os.path.join(
            "components", package, package.replace("-", "_").rstrip("/\\")
        )

        dest_dir = dest_dir_map.get(package) or package

        dest_path = os.path.join("dash", dest_dir)

        if not os.path.exists(dest_path):
            try:
                os.makedirs(dest_path)
            except OSError:
                logger.exception("🚨 Having issues manipulating %s", dest_path)
                sys.exit(1)

        if not os.path.exists(build_directory):
            status_print(
                "🚨 Could not locate build artifacts."
                + " Check that the npm build process completed"
                + f" successfully for package: {package} 🚨"
            )
            sys.exit(1)
        else:
            status_print(f"🚚 Moving build artifacts from {build_directory} to Dash 🚚")
            shutil.rmtree(dest_path)
            shutil.copytree(build_directory, dest_path)
            with open(os.path.join(dest_path, ".gitkeep"), "w", encoding="utf-8"):
                pass
            status_print(
                f"🟢 Finished moving build artifacts from {build_directory} to Dash 🟢"
            )


def cli():
    parser = argparse.ArgumentParser(
        prog="dash-update-components",
        formatter_class=_CombinedFormatter,
        description="Update the specified subcomponent libraries within Dash"
        " by copying over build artifacts, dependencies, and dependency metadata.",
    )
    parser.add_argument(
        "components_source",
        help="A glob string that matches the Dash component libraries to be updated"
        " (eg.'dash-table' // 'dash-core-components|dash-html-components' // 'all')."
        " The default argument is 'all'.",
        default="all",
    )
    parser.add_argument(
        "--concurrency",
        type=int,
        default=3,
        help="Maximum concurrent steps, up to 3 (ie all components in parallel)",
    )
    parser.add_argument(
        "--ci",
        help="For clean-install use '--ci True'",
        default="False",
    )

    args = parser.parse_args()

    if sys.platform == "win32":
        args.components_source = args.components_source.replace('"', "").replace(
            "'", ""
        )

    bootstrap_components(
        args.components_source, args.concurrency, "ci" if args.ci == "True" else "i"
    )
    build_components(args.components_source, args.concurrency)


if __name__ == "__main__":
    cli()



File: dash/development/component_generator.py

from collections import OrderedDict

import json
import sys
import subprocess
import shlex
import os
import argparse
import shutil
import functools
import pkg_resources
import yaml

from ._r_components_generation import write_class_file
from ._r_components_generation import generate_exports
from ._py_components_generation import generate_class_file
from ._py_components_generation import generate_imports
from ._py_components_generation import generate_classes_files
from ._jl_components_generation import generate_struct_file
from ._jl_components_generation import generate_module

reserved_words = [
    "UNDEFINED",
    "REQUIRED",
    "to_plotly_json",
    "available_properties",
    "available_wildcard_properties",
    "_.*",
]


class _CombinedFormatter(
    argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter
):
    pass


# pylint: disable=too-many-locals, too-many-arguments, too-many-branches, too-many-statements
def generate_components(
    components_source,
    project_shortname,
    package_info_filename="package.json",
    ignore="^_",
    rprefix=None,
    rdepends="",
    rimports="",
    rsuggests="",
    jlprefix=None,
    metadata=None,
    keep_prop_order=None,
    max_props=None,
):

    project_shortname = project_shortname.replace("-", "_").rstrip("/\\")

    is_windows = sys.platform == "win32"

    extract_path = pkg_resources.resource_filename("dash", "extract-meta.js")

    reserved_patterns = "|".join(f"^{p}$" for p in reserved_words)

    os.environ["NODE_PATH"] = "node_modules"

    shutil.copyfile(
        "package.json", os.path.join(project_shortname, package_info_filename)
    )

    if not metadata:
        env = os.environ.copy()

        # Ensure local node modules is used when the script is packaged.
        env["MODULES_PATH"] = os.path.abspath("./node_modules")

        cmd = shlex.split(
            f'node {extract_path} "{ignore}" "{reserved_patterns}" {components_source}',
            posix=not is_windows,
        )

        proc = subprocess.Popen(  # pylint: disable=consider-using-with
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            shell=is_windows,
            env=env,
        )
        out, err = proc.communicate()
        status = proc.poll()

        if err:
            print(err.decode(), file=sys.stderr)

        if not out:
            print(
                f"Error generating metadata in {project_shortname} (status={status})",
                file=sys.stderr,
            )
            sys.exit(1)

        metadata = safe_json_loads(out.decode("utf-8"))

    py_generator_kwargs = {}
    if keep_prop_order is not None:
        keep_prop_order = [
            component.strip(" ") for component in keep_prop_order.split(",")
        ]
        py_generator_kwargs["prop_reorder_exceptions"] = keep_prop_order

    if max_props:
        py_generator_kwargs["max_props"] = max_props

    generator_methods = [functools.partial(generate_class_file, **py_generator_kwargs)]

    if rprefix is not None or jlprefix is not None:
        with open("package.json", "r", encoding="utf-8") as f:
            pkg_data = safe_json_loads(f.read())

    if rprefix is not None:
        if not os.path.exists("man"):
            os.makedirs("man")
        if not os.path.exists("R"):
            os.makedirs("R")
        if os.path.isfile("dash-info.yaml"):
            with open("dash-info.yaml", encoding="utf-8") as yamldata:
                rpkg_data = yaml.safe_load(yamldata)
        else:
            rpkg_data = None
        generator_methods.append(
            functools.partial(write_class_file, prefix=rprefix, rpkg_data=rpkg_data)
        )

    if jlprefix is not None:
        generator_methods.append(
            functools.partial(generate_struct_file, prefix=jlprefix)
        )

    components = generate_classes_files(project_shortname, metadata, *generator_methods)

    with open(
        os.path.join(project_shortname, "metadata.json"), "w", encoding="utf-8"
    ) as f:
        json.dump(metadata, f, indent=2)

    generate_imports(project_shortname, components)

    if rprefix is not None:
        generate_exports(
            project_shortname,
            components,
            metadata,
            pkg_data,
            rpkg_data,
            rprefix,
            rdepends,
            rimports,
            rsuggests,
        )

    if jlprefix is not None:
        generate_module(project_shortname, components, metadata, pkg_data, jlprefix)


def safe_json_loads(s):
    jsondata_unicode = json.loads(s, object_pairs_hook=OrderedDict)
    if sys.version_info[0] >= 3:
        return jsondata_unicode
    return byteify(jsondata_unicode)


def component_build_arg_parser():
    parser = argparse.ArgumentParser(
        prog="dash-generate-components",
        formatter_class=_CombinedFormatter,
        description="Generate dash components by extracting the metadata "
        "using react-docgen. Then map the metadata to Python classes.",
    )
    parser.add_argument("components_source", help="React components source directory.")
    parser.add_argument(
        "project_shortname", help="Name of the project to export the classes files."
    )
    parser.add_argument(
        "-p",
        "--package-info-filename",
        default="package.json",
        help="The filename of the copied `package.json` to `project_shortname`",
    )
    parser.add_argument(
        "-i",
        "--ignore",
        default="^_",
        help="Files/directories matching the pattern will be ignored",
    )
    parser.add_argument(
        "--r-prefix",
        help="Specify a prefix for Dash for R component names, write "
        "components to R dir, create R package.",
    )
    parser.add_argument(
        "--r-depends",
        default="",
        help="Specify a comma-separated list of R packages to be "
        "inserted into the Depends field of the DESCRIPTION file.",
    )
    parser.add_argument(
        "--r-imports",
        default="",
        help="Specify a comma-separated list of R packages to be "
        "inserted into the Imports field of the DESCRIPTION file.",
    )
    parser.add_argument(
        "--r-suggests",
        default="",
        help="Specify a comma-separated list of R packages to be "
        "inserted into the Suggests field of the DESCRIPTION file.",
    )
    parser.add_argument(
        "--jl-prefix",
        help="Specify a prefix for Dash for R component names, write "
        "components to R dir, create R package.",
    )
    parser.add_argument(
        "-k",
        "--keep-prop-order",
        default=None,
        help="Specify a comma-separated list of components which will use the prop "
        "order described in the component proptypes instead of alphabetically reordered "
        "props. Pass the 'ALL' keyword to have every component retain "
        "its original prop order.",
    )
    parser.add_argument(
        "--max-props",
        type=int,
        default=250,
        help="Specify the max number of props to list in the component signature. "
        "More props will still be shown in the docstring, and will still work when "
        "provided as kwargs to the component. Python <3.7 only supports 255 args, "
        "but you may also want to reduce further for improved readability at the "
        "expense of auto-completion for the later props. Use 0 to include all props.",
    )
    return parser


def cli():
    args = component_build_arg_parser().parse_args()
    generate_components(
        args.components_source,
        args.project_shortname,
        package_info_filename=args.package_info_filename,
        ignore=args.ignore,
        rprefix=args.r_prefix,
        rdepends=args.r_depends,
        rimports=args.r_imports,
        rsuggests=args.r_suggests,
        jlprefix=args.jl_prefix,
        keep_prop_order=args.keep_prop_order,
        max_props=args.max_props,
    )


# pylint: disable=undefined-variable
def byteify(input_object):
    if isinstance(input_object, dict):
        return OrderedDict(
            [(byteify(key), byteify(value)) for key, value in input_object.iteritems()]
        )
    if isinstance(input_object, list):
        return [byteify(element) for element in input_object]
    if isinstance(input_object, unicode):  # noqa:F821
        return input_object.encode("utf-8")
    return input_object


if __name__ == "__main__":
    cli()



File: dash/development/_r_components_generation.py

# pylint: disable=consider-using-f-string
import os
import sys
import shutil
import importlib
import textwrap
import re
import warnings

from ._all_keywords import r_keywords
from ._py_components_generation import reorder_props


# Declaring longer string templates as globals to improve
# readability, make method logic clearer to anyone inspecting
# code below
r_component_string = """#' @export
{funcname} <- function({default_argtext}{wildcards}) {{
    {wildcard_declaration}
    props <- list({default_paramtext}{wildcards})
    if (length(props) > 0) {{
        props <- props[!vapply(props, is.null, logical(1))]
    }}
    component <- list(
        props = props,
        type = '{name}',
        namespace = '{project_shortname}',
        propNames = c({prop_names}{wildcard_names}),
        package = '{package_name}'
        )

    structure(component, class = c('dash_component', 'list'))
}}
"""  # noqa:E501

# the following strings represent all the elements in an object
# of the html_dependency class, which will be propagated by
# iterating over _js_dist in __init__.py
frame_open_template = """.{rpkgname}_js_metadata <- function() {{
deps_metadata <- list("""

frame_element_template = """`{dep_name}` = structure(list(name = "{dep_name}",
version = "{project_ver}", src = list(href = NULL,
file = "deps"), meta = NULL,
script = {script_name},
stylesheet = {css_name}, head = NULL, attachment = NULL, package = "{rpkgname}",
all_files = FALSE{async_or_dynamic}), class = "html_dependency")"""  # noqa:E501

frame_body_template = """`{project_shortname}` = structure(list(name = "{project_shortname}",
version = "{project_ver}", src = list(href = NULL,
file = "deps"), meta = NULL,
script = {script_name},
stylesheet = {css_name}, head = NULL, attachment = NULL, package = "{rpkgname}",
all_files = FALSE{async_or_dynamic}), class = "html_dependency")"""  # noqa:E501

frame_close_template = """)
return(deps_metadata)
}
"""

help_string = """% Auto-generated: do not edit by hand
\\name{{{funcname}}}

\\alias{{{funcname}}}

\\title{{{name} component}}

\\description{{
{description}
}}

\\usage{{
{funcname}({default_argtext})
}}

\\arguments{{
{item_text}
}}

\\value{{{value_text}}}

"""

description_template = """Package: {package_name}
Title: {package_title}
Version: {package_version}
Description: {package_description}
Depends: R (>= 3.0.2){package_depends}
Imports: {package_imports}
Suggests: {package_suggests}{package_rauthors}
License: {package_license}{package_copyright}
URL: {package_url}
BugReports: {package_issues}
Encoding: UTF-8
LazyData: true{vignette_builder}
KeepSource: true
"""

rbuild_ignore_string = r"""# ignore JS config files/folders
node_modules/
coverage/
src/
lib/
.babelrc
.builderrc
.eslintrc
.npmignore
.editorconfig
.eslintignore
.prettierrc
.circleci
.github

# demo folder has special meaning in R
# this should hopefully make it still
# allow for the possibility to make R demos
demo/.*\.js
demo/.*\.html
demo/.*\.css

# ignore Python files/folders
setup.py
usage.py
setup.py
requirements.txt
MANIFEST.in
CHANGELOG.md
test/
# CRAN has weird LICENSE requirements
LICENSE.txt
^.*\.Rproj$
^\.Rproj\.user$
"""

pkghelp_stub = """% Auto-generated: do not edit by hand
\\docType{{package}}
\\name{{{package_name}-package}}
\\alias{{{package_name}}}
\\title{{{pkg_help_title}}}
\\description{{
{pkg_help_description}
}}
\\author{{
\\strong{{Maintainer}}: {maintainer}
}}
"""

wildcard_helper = """
dash_assert_valid_wildcards <- function (attrib = list("data", "aria"), ...)
{
    args <- list(...)
    validation_results <- lapply(names(args), function(x) {
        grepl(paste0("^(", paste0(attrib, collapse="|"), ")-[a-zA-Z0-9_-]+$"),
            x)
    })
    if (FALSE %in% validation_results) {
        stop(sprintf("The following props are not valid in this component: '%s'",
            paste(names(args)[grepl(FALSE, unlist(validation_results))],
                collapse = ", ")), call. = FALSE)
    }
    else {
        return(args)
    }
}
"""  # noqa:E501

wildcard_template = """
    wildcard_names = names(dash_assert_valid_wildcards(attrib = list({}), ...))
"""

wildcard_help_template = """


\\item{{...}}{{wildcards allowed have the form: `{}`}}
"""


# pylint: disable=R0914
def generate_class_string(name, props, project_shortname, prefix):
    # Here we convert from snake case to camel case
    package_name = snake_case_to_camel_case(project_shortname)

    # Ensure props are ordered with children first
    props = reorder_props(props=props)

    prop_keys = list(props.keys())

    wildcards = ""
    wildcard_declaration = ""
    wildcard_names = ""
    default_paramtext = ""
    default_argtext = ""
    accepted_wildcards = ""

    if any(key.endswith("-*") for key in prop_keys):
        accepted_wildcards = get_wildcards_r(prop_keys)
        wildcards = ", ..."
        wildcard_declaration = wildcard_template.format(
            accepted_wildcards.replace("-*", "")
        )
        wildcard_names = ", wildcard_names"

    # Produce a string with all property names other than WCs
    prop_names = ", ".join(
        "'{}'".format(p) for p in prop_keys if "*" not in p and p not in ["setProps"]
    )

    # Filter props to remove those we don't want to expose
    for item in prop_keys[:]:
        if item.endswith("-*") or item == "setProps":
            prop_keys.remove(item)
        elif item in r_keywords:
            prop_keys.remove(item)
            warnings.warn(
                (
                    'WARNING: prop "{}" in component "{}" is an R keyword'
                    " - REMOVED FROM THE R COMPONENT"
                ).format(item, name)
            )

    default_argtext += ", ".join("{}=NULL".format(p) for p in prop_keys)

    # pylint: disable=C0301
    default_paramtext += ", ".join(
        "{0}={0}".format(p) if p != "children" else "{}=children".format(p)
        for p in prop_keys
    )

    return r_component_string.format(
        funcname=format_fn_name(prefix, name),
        name=name,
        default_argtext=default_argtext,
        wildcards=wildcards,
        wildcard_declaration=wildcard_declaration,
        default_paramtext=default_paramtext,
        project_shortname=project_shortname,
        prop_names=prop_names,
        wildcard_names=wildcard_names,
        package_name=package_name,
    )


# pylint: disable=R0914
def generate_js_metadata(pkg_data, project_shortname):
    """Dynamically generate R function to supply JavaScript and CSS dependency
    information required by the dash package for R.

    Parameters
    ----------
    project_shortname = component library name, in snake case

    Returns
    -------
    function_string = complete R function code to provide component features
    """
    # make sure the module we're building is available to Python,
    # even if it hasn't been installed yet
    sys.path.insert(0, os.getcwd())
    mod = importlib.import_module(project_shortname)

    alldist = getattr(mod, "_js_dist", []) + getattr(mod, "_css_dist", [])

    project_ver = pkg_data.get("version")

    rpkgname = snake_case_to_camel_case(project_shortname)

    # since _js_dist may suggest more than one dependency, need
    # a way to iterate over all dependencies for a given set.
    # here we define an opening, element, and closing string --
    # if the total number of dependencies > 1, we can concatenate
    # them and write a list object in R with multiple elements
    function_frame_open = frame_open_template.format(rpkgname=rpkgname)

    function_frame = []
    function_frame_body = []

    # pylint: disable=consider-using-enumerate
    if len(alldist) > 1:
        for dep in range(len(alldist)):
            curr_dep = alldist[dep]
            rpp = curr_dep["relative_package_path"]

            async_or_dynamic = get_async_type(curr_dep)

            if "dash_" in rpp:
                dep_name = rpp.split(".")[0]
            else:
                dep_name = "{}".format(project_shortname)

            if "css" in rpp:
                css_name = "'{}'".format(rpp)
                script_name = "NULL"
            else:
                script_name = "'{}'".format(rpp)
                css_name = "NULL"

            function_frame += [
                frame_element_template.format(
                    dep_name=dep_name,
                    project_ver=project_ver,
                    rpkgname=rpkgname,
                    project_shortname=project_shortname,
                    script_name=script_name,
                    css_name=css_name,
                    async_or_dynamic=async_or_dynamic,
                )
            ]
            function_frame_body = ",\n".join(function_frame)
    elif len(alldist) == 1:
        dep = alldist[0]
        rpp = dep["relative_package_path"]

        async_or_dynamic = get_async_type(dep)

        if "css" in rpp:
            css_name = "'{}'".format(rpp)
            script_name = "NULL"
        else:
            script_name = "'{}'".format(rpp)
            css_name = "NULL"

        function_frame_body = frame_body_template.format(
            project_shortname=project_shortname,
            project_ver=project_ver,
            rpkgname=rpkgname,
            script_name=script_name,
            css_name=css_name,
            async_or_dynamic=async_or_dynamic,
        )

    function_string = "".join(
        [function_frame_open, function_frame_body, frame_close_template]
    )

    return function_string


# determine whether dependency uses async or dynamic flag
# then return the properly formatted string if so, i.e.
# " async = TRUE,". a dependency can have async or
# dynamic elements, neither of these, but never both.
def get_async_type(dep):
    async_or_dynamic = ""
    for key in dep.keys():
        if key in ["async", "dynamic"]:
            keyval = dep[key]
            if not isinstance(keyval, bool):
                keyval = "'{}'".format(keyval.lower())
            else:
                keyval = str(keyval).upper()
            async_or_dynamic = ", {} = {}".format(key, keyval)
    return async_or_dynamic


# This method wraps code within arbitrary LaTeX-like tags, which are used
# by R's internal help parser for constructing man pages
def wrap(tag, code):
    if tag == "":
        return code
    return "\\{}{{\n{}}}".format(tag, code)


def write_help_file(name, props, description, prefix, rpkg_data):
    """Write R documentation file (.Rd) given component name and properties.

    Parameters
    ----------
    name = the name of the Dash component for which a help file is generated
    props = the properties of the component
    description = the component's description, inserted into help file header
    prefix = the DashR library prefix (optional, can be a blank string)
    rpkg_data = package metadata (optional)

    Returns
    -------
    writes an R help file to the man directory for the generated R package
    """
    funcname = format_fn_name(prefix, name)
    file_name = funcname + ".Rd"

    wildcards = ""
    default_argtext = ""
    item_text = ""
    accepted_wildcards = ""

    # the return value of all Dash components should be the same,
    # in an abstract sense -- they produce a list
    value_text = "named list of JSON elements corresponding to React.js properties and their values"  # noqa:E501

    prop_keys = list(props.keys())

    if any(key.endswith("-*") for key in prop_keys):
        accepted_wildcards = get_wildcards_r(prop_keys)
        wildcards = ", ..."

    # Filter props to remove those we don't want to expose
    for item in prop_keys[:]:
        if item.endswith("-*") or item in r_keywords or item == "setProps":
            prop_keys.remove(item)

    default_argtext += ", ".join("{}=NULL".format(p) for p in prop_keys)

    item_text += "\n\n".join(
        "\\item{{{}}}{{{}{}}}".format(
            p, print_r_type(props[p]["type"]), props[p]["description"]
        )
        for p in prop_keys
    )

    # auto-replace any unescaped backslashes for compatibility with R docs
    description = re.sub(r"(?<!\\)%", "\\%", description)
    item_text = re.sub(r"(?<!\\)%", "\\%", item_text)

    # scrub examples which begin with **Example Usage**, as these should be
    # provided as R code within dash-info.yaml
    if "**Example Usage**" in description:
        description = description.split("**Example Usage**")[0].rstrip()

    if wildcards == ", ...":
        default_argtext += wildcards
        item_text += wildcard_help_template.format(accepted_wildcards)

    # in R, the online help viewer does not properly wrap lines for
    # the usage string -- we will hard wrap at 60 characters using
    # textwrap.fill, starting from the beginning of the usage string

    file_path = os.path.join("man", file_name)
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(
            help_string.format(
                funcname=funcname,
                name=name,
                default_argtext=textwrap.fill(
                    default_argtext, width=60, break_long_words=False
                ),
                item_text=item_text,
                value_text=value_text,
                description=description.replace("\n", " "),
            )
        )
    if rpkg_data is not None and "r_examples" in rpkg_data:
        ex = rpkg_data.get("r_examples")
        the_ex = ([e for e in ex if e.get("name") == funcname] or [None])[0]
        result = ""
        if the_ex and "code" in the_ex.keys():
            result += wrap(
                "examples",
                wrap("dontrun" if the_ex.get("dontrun") else "", the_ex["code"]),
            )
            with open(file_path, "a+", encoding="utf-8") as fa:
                fa.write(result + "\n")


# pylint: disable=too-many-arguments
def write_class_file(
    name,
    props,
    description,
    project_shortname,
    prefix=None,
    rpkg_data=None,
):
    props = reorder_props(props=props)

    # generate the R help pages for each of the Dash components that we
    # are transpiling -- this is done to avoid using Roxygen2 syntax,
    # we may eventually be able to generate similar documentation using
    # doxygen and an R plugin, but for now we'll just do it on our own
    # from within Python
    write_help_file(name, props, description, prefix, rpkg_data)

    import_string = "# AUTO GENERATED FILE - DO NOT EDIT\n\n"
    class_string = generate_class_string(name, props, project_shortname, prefix)

    file_name = format_fn_name(prefix, name) + ".R"

    file_path = os.path.join("R", file_name)
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(import_string)
        f.write(class_string)

    print("Generated {}".format(file_name))


def write_js_metadata(pkg_data, project_shortname, has_wildcards):
    """Write an internal (not exported) R function to return all JS
    dependencies as required by dash.

    Parameters
    ----------
    project_shortname = hyphenated string, e.g. dash-html-components

    Returns
    -------
    """
    function_string = generate_js_metadata(
        pkg_data=pkg_data, project_shortname=project_shortname
    )
    file_name = "internal.R"

    # the R source directory for the package won't exist on first call
    # create the R directory if it is missing
    if not os.path.exists("R"):
        os.makedirs("R")

    file_path = os.path.join("R", file_name)
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(function_string)
        if has_wildcards:
            f.write(wildcard_helper)

    # now copy over all JS dependencies from the (Python) components dir
    # the inst/lib directory for the package won't exist on first call
    # create this directory if it is missing
    if os.path.exists("inst/deps"):
        shutil.rmtree("inst/deps")

    os.makedirs("inst/deps")

    for rel_dirname, _, filenames in os.walk(project_shortname):
        for filename in filenames:
            extension = os.path.splitext(filename)[1]

            if extension in [".py", ".pyc", ".json"]:
                continue

            target_dirname = os.path.join(
                os.path.join(
                    "inst/deps/", os.path.relpath(rel_dirname, project_shortname)
                )
            )

            if not os.path.exists(target_dirname):
                os.makedirs(target_dirname)

            shutil.copy(os.path.join(rel_dirname, filename), target_dirname)


# pylint: disable=R0914, R0913, R0912, R0915
def generate_rpkg(
    pkg_data,
    rpkg_data,
    project_shortname,
    export_string,
    package_depends,
    package_imports,
    package_suggests,
    has_wildcards,
):
    """Generate documents for R package creation.

    Parameters
    ----------
    pkg_data
    rpkg_data
    project_shortname
    export_string
    package_depends
    package_imports
    package_suggests
    has_wildcards

    Returns
    -------
    """
    # Leverage package.json to import specifics which are also applicable
    # to R package that we're generating here, use .get in case the key
    # does not exist in package.json

    package_name = snake_case_to_camel_case(project_shortname)
    package_copyright = ""
    package_rauthors = ""
    lib_name = pkg_data.get("name")

    if rpkg_data is not None:
        if rpkg_data.get("pkg_help_title"):
            package_title = rpkg_data.get(
                "pkg_help_title", pkg_data.get("description", "")
            )
        if rpkg_data.get("pkg_help_description"):
            package_description = rpkg_data.get(
                "pkg_help_description", pkg_data.get("description", "")
            )
        if rpkg_data.get("pkg_copyright"):
            package_copyright = "\nCopyright: {}".format(
                rpkg_data.get("pkg_copyright", "")
            )
    else:
        # fall back to using description in package.json, if present
        package_title = pkg_data.get("description", "")
        package_description = pkg_data.get("description", "")

    package_version = pkg_data.get("version", "0.0.1")

    # remove leading and trailing commas, add space after comma if missing
    if package_depends:
        package_depends = ", " + package_depends.strip(",").lstrip()
        package_depends = re.sub(r"(,(?![ ]))", ", ", package_depends)

    if package_imports:
        package_imports = package_imports.strip(",").lstrip()
        package_imports = re.sub(r"(,(?![ ]))", ", ", package_imports)

    if package_suggests:
        package_suggests = package_suggests.strip(",").lstrip()
        package_suggests = re.sub(r"(,(?![ ]))", ", ", package_suggests)

    if "bugs" in pkg_data:
        package_issues = pkg_data["bugs"].get("url", "")
    else:
        package_issues = ""
        print(
            "Warning: a URL for bug reports was "
            "not provided. Empty string inserted.",
            file=sys.stderr,
        )

    if "homepage" in pkg_data:
        package_url = pkg_data.get("homepage", "")
    else:
        package_url = ""
        print(
            "Warning: a homepage URL was not provided. Empty string inserted.",
            file=sys.stderr,
        )

    package_author = pkg_data.get("author")

    package_author_name = package_author.split(" <")[0]
    package_author_email = package_author.split(" <")[1][:-1]

    package_author_fn = package_author_name.split(" ")[0]
    package_author_ln = package_author_name.rsplit(" ", 2)[-1]

    maintainer = pkg_data.get("maintainer", pkg_data.get("author"))

    if "<" not in package_author:
        print(
            "Error, aborting R package generation: "
            "R packages require a properly formatted author field "
            "or installation will fail. Please include an email "
            "address enclosed within < > brackets in package.json. ",
            file=sys.stderr,
        )
        sys.exit(1)

    if rpkg_data is not None:
        if rpkg_data.get("pkg_authors"):
            package_rauthors = "\nAuthors@R: {}".format(
                rpkg_data.get("pkg_authors", "")
            )
        else:
            package_rauthors = '\nAuthors@R: person("{}", "{}", role = c("aut", "cre"), email = "{}")'.format(
                package_author_fn, package_author_ln, package_author_email
            )

    if not (os.path.isfile("LICENSE") or os.path.isfile("LICENSE.txt")):
        package_license = pkg_data.get("license", "")
    else:
        package_license = pkg_data.get("license", "") + " + file LICENSE"
        # R requires that the LICENSE.txt file be named LICENSE
        if not os.path.isfile("LICENSE"):
            os.symlink("LICENSE.txt", "LICENSE")

    import_string = "# AUTO GENERATED FILE - DO NOT EDIT\n\n"
    packages_string = ""

    rpackage_list = package_depends.split(", ") + package_imports.split(", ")
    rpackage_list = filter(bool, rpackage_list)

    if rpackage_list:
        for rpackage in rpackage_list:
            packages_string += "\nimport({})\n".format(rpackage)

    if os.path.exists("vignettes"):
        vignette_builder = "\nVignetteBuilder: knitr"
        if "knitr" not in package_suggests and "rmarkdown" not in package_suggests:
            package_suggests += ", knitr, rmarkdown"
            package_suggests = package_suggests.lstrip(", ")
    else:
        vignette_builder = ""

    pkghelp_stub_path = os.path.join("man", package_name + "-package.Rd")

    # generate the internal (not exported to the user) functions which
    # supply the JavaScript dependencies to the dash package.
    # this avoids having to generate an RData file from within Python.
    write_js_metadata(pkg_data, project_shortname, has_wildcards)

    with open("NAMESPACE", "w+", encoding="utf-8") as f:
        f.write(import_string)
        f.write(export_string)
        f.write(packages_string)

    with open(".Rbuildignore", "w+", encoding="utf-8") as f2:
        f2.write(rbuild_ignore_string)

    description_string = description_template.format(
        package_name=package_name,
        package_title=package_title,
        package_description=package_description,
        package_version=package_version,
        package_rauthors=package_rauthors,
        package_depends=package_depends,
        package_imports=package_imports,
        package_suggests=package_suggests,
        package_license=package_license,
        package_copyright=package_copyright,
        package_url=package_url,
        package_issues=package_issues,
        vignette_builder=vignette_builder,
    )

    with open("DESCRIPTION", "w+", encoding="utf-8") as f3:
        f3.write(description_string)

    if rpkg_data is not None:
        if rpkg_data.get("pkg_help_description"):
            pkghelp = pkghelp_stub.format(
                package_name=package_name,
                pkg_help_title=rpkg_data.get("pkg_help_title"),
                pkg_help_description=rpkg_data.get("pkg_help_description"),
                lib_name=lib_name,
                maintainer=maintainer,
            )
            with open(pkghelp_stub_path, "w", encoding="utf-8") as f4:
                f4.write(pkghelp)


# This converts a string from snake case to camel case
# Not required for R package name to be in camel case,
# but probably more conventional this way
def snake_case_to_camel_case(namestring):
    s = namestring.split("_")
    return s[0] + "".join(w.capitalize() for w in s[1:])


# this logic will permit passing blank R prefixes to
# dash-generate-components, while also enforcing
# camelCase for the resulting functions; if a prefix
# is supplied, leave it as-is
def format_fn_name(prefix, name):
    if prefix:
        return prefix + snake_case_to_camel_case(name)
    return snake_case_to_camel_case(name[0].lower() + name[1:])


# pylint: disable=unused-argument
def generate_exports(
    project_shortname,
    components,
    metadata,
    pkg_data,
    rpkg_data,
    prefix,
    package_depends,
    package_imports,
    package_suggests,
    **kwargs
):
    export_string = make_namespace_exports(components, prefix)

    # Look for wildcards in the metadata
    has_wildcards = False
    for component_data in metadata.values():
        if any(key.endswith("-*") for key in component_data["props"]):
            has_wildcards = True
            break

    # now, bundle up the package information and create all the requisite
    # elements of an R package, so that the end result is installable either
    # locally or directly from GitHub
    generate_rpkg(
        pkg_data,
        rpkg_data,
        project_shortname,
        export_string,
        package_depends,
        package_imports,
        package_suggests,
        has_wildcards,
    )


def make_namespace_exports(components, prefix):
    export_string = ""
    for component in components:
        if (
            not component.endswith("-*")
            and str(component) not in r_keywords
            and str(component) not in ["setProps", "children"]
        ):
            export_string += "export({}{})\n".format(prefix, component)

    # the following lines enable rudimentary support for bundling in
    # R functions that are not automatically generated by the transpiler
    # such that functions contained in the R subdirectory are exported,
    # so long as they are not in utils.R.
    rfilelist = []
    omitlist = ["utils.R", "internal.R"] + [
        "{}{}.R".format(prefix, component) for component in components
    ]
    fnlist = []

    for script in os.listdir("R"):
        if script.endswith(".R") and script not in omitlist:
            rfilelist += [os.path.join("R", script)]

    for rfile in rfilelist:
        with open(rfile, "r", encoding="utf-8") as script:
            s = script.read()

            # remove comments
            s = re.sub("#.*$", "", s, flags=re.M)

            # put the whole file on one line
            s = s.replace("\n", " ").replace("\r", " ")

            # empty out strings, in case of unmatched block terminators
            s = re.sub(r"'([^'\\]|\\'|\\[^'])*'", "''", s)
            s = re.sub(r'"([^"\\]|\\"|\\[^"])*"', '""', s)

            # empty out block terminators () and {}
            # so we don't catch nested functions, or functions as arguments
            # repeat until it stops changing, in case of multiply nested blocks
            prev_len = len(s) + 1
            while len(s) < prev_len:
                prev_len = len(s)
                s = re.sub(r"\(([^()]|\(\))*\)", "()", s)
                s = re.sub(r"\{([^{}]|\{\})*\}", "{}", s)

            # now, in whatever is left, look for functions
            matches = re.findall(
                # in R, either = or <- may be used to create and assign objects
                r"([^A-Za-z0-9._]|^)([A-Za-z0-9._]+)\s*(=|<-)\s*function",
                s,
            )
            for match in matches:
                fn = match[1]
                # Allow users to mark functions as private by prefixing with .
                if fn[0] != "." and fn not in fnlist:
                    fnlist.append(fn)

    export_string += "\n".join("export({})".format(function) for function in fnlist)
    return export_string


def get_r_prop_types(type_object):
    """Mapping from the PropTypes js type object to the R type."""

    def shape_or_exact():
        return "lists containing elements {}.\n{}".format(
            ", ".join("'{}'".format(t) for t in type_object["value"]),
            "Those elements have the following types:\n{}".format(
                "\n".join(
                    create_prop_docstring_r(
                        prop_name=prop_name,
                        type_object=prop,
                        required=prop["required"],
                        description=prop.get("description", ""),
                        indent_num=1,
                    )
                    for prop_name, prop in type_object["value"].items()
                )
            ),
        )

    return dict(
        array=lambda: "unnamed list",
        bool=lambda: "logical",
        number=lambda: "numeric",
        string=lambda: "character",
        object=lambda: "named list",
        any=lambda: "logical | numeric | character | named list | unnamed list",
        element=lambda: "dash component",
        node=lambda: "a list of or a singular dash component, string or number",
        # React's PropTypes.oneOf
        enum=lambda: "a value equal to: {}".format(
            ", ".join("{}".format(str(t["value"])) for t in type_object["value"])
        ),
        # React's PropTypes.oneOfType
        union=lambda: "{}".format(
            " | ".join(
                "{}".format(get_r_type(subType))
                for subType in type_object["value"]
                if get_r_type(subType) != ""
            )
        ),
        # React's PropTypes.arrayOf
        arrayOf=lambda: (
            "list"
            + (
                " of {}s".format(get_r_type(type_object["value"]))
                if get_r_type(type_object["value"]) != ""
                else ""
            )
        ),
        # React's PropTypes.objectOf
        objectOf=lambda: "list with named elements and values of type {}".format(
            get_r_type(type_object["value"])
        ),
        # React's PropTypes.shape
        shape=shape_or_exact,
        # React's PropTypes.exact
        exact=shape_or_exact,
    )


def get_r_type(type_object, is_flow_type=False, indent_num=0):
    """
    Convert JS types to R types for the component definition
    Parameters
    ----------
    type_object: dict
        react-docgen-generated prop type dictionary
    is_flow_type: bool
    indent_num: int
        Number of indents to use for the docstring for the prop
    Returns
    -------
    str
        Python type string
    """
    js_type_name = type_object["name"]
    js_to_r_types = get_r_prop_types(type_object=type_object)
    if (
        "computed" in type_object
        and type_object["computed"]
        or type_object.get("type", "") == "function"
    ):
        return ""
    if js_type_name in js_to_r_types:
        prop_type = js_to_r_types[js_type_name]()
        return prop_type
    return ""


def print_r_type(typedata):
    typestring = get_r_type(typedata).capitalize()
    if typestring:
        typestring += ". "
    return typestring


# pylint: disable=too-many-arguments
def create_prop_docstring_r(
    prop_name, type_object, required, description, indent_num, is_flow_type=False
):
    """
    Create the Dash component prop docstring
    Parameters
    ----------
    prop_name: str
        Name of the Dash component prop
    type_object: dict
        react-docgen-generated prop type dictionary
    required: bool
        Component is required?
    description: str
        Dash component description
    indent_num: int
        Number of indents to use for the context block
        (creates 2 spaces for every indent)
    is_flow_type: bool
        Does the prop use Flow types? Otherwise, uses PropTypes
    Returns
    -------
    str
        Dash component prop docstring
    """
    r_type_name = get_r_type(
        type_object=type_object, is_flow_type=is_flow_type, indent_num=indent_num + 1
    )

    indent_spacing = "  " * indent_num
    if "\n" in r_type_name:
        return (
            "{indent_spacing}- {name} ({is_required}): {description}. "
            "{name} has the following type: {type}".format(
                indent_spacing=indent_spacing,
                name=prop_name,
                type=r_type_name,
                description=description,
                is_required="required" if required else "optional",
            )
        )
    return "{indent_spacing}- {name} ({type}{is_required}){description}".format(
        indent_spacing=indent_spacing,
        name=prop_name,
        type="{}; ".format(r_type_name) if r_type_name else "",
        description=(": {}".format(description) if description != "" else ""),
        is_required="required" if required else "optional",
    )


def get_wildcards_r(prop_keys):
    wildcards = ""
    wildcards += ", ".join("'{}'".format(p) for p in prop_keys if p.endswith("-*"))

    if wildcards == "":
        wildcards = "NULL"
    return wildcards



File: dash/development/_all_keywords.py

# keyword.kwlist for both Python 2 and 3
python_keywords = {
    "False",
    "None",
    "True",
    "and",
    "as",
    "assert",
    "async",
    "await",
    "break",
    "class",
    "continue",
    "def",
    "del",
    "elif",
    "else",
    "except",
    "exec",
    "finally",
    "for",
    "from",
    "global",
    "if",
    "import",
    "in",
    "is",
    "lambda",
    "nonlocal",
    "not",
    "or",
    "pass",
    "print",
    "raise",
    "return",
    "try",
    "while",
    "with",
    "yield",
}

# This is a set of R reserved words that cannot be used as function
# argument names.
#
# Reserved words can be obtained from R's help pages by executing the
# statement below:
# > ?reserved

r_keywords = {
    "if",
    "else",
    "repeat",
    "while",
    "function",
    "for",
    "in",
    "next",
    "break",
    "TRUE",
    "FALSE",
    "NULL",
    "Inf",
    "NaN",
    "NA",
    "NA_integer_",
    "NA_real_",
    "NA_complex_",
    "NA_character_",
    "...",
}

# This is a set of Julia reserved words that cannot be used as function
# argument names.

julia_keywords = {
    "baremodule",
    "begin",
    "break",
    "catch",
    "const",
    "continue",
    "do",
    "else",
    "elseif",
    "end",
    "export",
    "false",
    "finally",
    "for",
    "function",
    "global",
    "if",
    "import",
    "let",
    "local",
    "macro",
    "module",
    "quote",
    "return",
    "struct",
    "true",
    "try",
    "using",
    "while",
}



File: dash/development/_py_components_generation.py

from collections import OrderedDict
import copy
import os
from textwrap import fill, dedent

from dash.development.base_component import _explicitize_args
from dash.exceptions import NonExistentEventException
from ._all_keywords import python_keywords
from ._collect_nodes import collect_nodes, filter_base_nodes
from .base_component import Component


# pylint: disable=unused-argument,too-many-locals
def generate_class_string(
    typename,
    props,
    description,
    namespace,
    prop_reorder_exceptions=None,
    max_props=None,
):
    """Dynamically generate class strings to have nicely formatted docstrings,
    keyword arguments, and repr.
    Inspired by http://jameso.be/2013/08/06/namedtuple.html
    Parameters
    ----------
    typename
    props
    description
    namespace
    prop_reorder_exceptions
    Returns
    -------
    string
    """
    # TODO _prop_names, _type, _namespace, and available_properties
    # can be modified by a Dash JS developer via setattr
    # TODO - Tab out the repr for the repr of these components to make it
    # look more like a hierarchical tree
    # TODO - Include "description" "defaultValue" in the repr and docstring
    #
    # TODO - Handle "required"
    #
    # TODO - How to handle user-given `null` values? I want to include
    # an expanded docstring like Dropdown(value=None, id=None)
    # but by templating in those None values, I have no way of knowing
    # whether a property is None because the user explicitly wanted
    # it to be `null` or whether that was just the default value.
    # The solution might be to deal with default values better although
    # not all component authors will supply those.
    c = '''class {typename}(Component):
    """{docstring}"""
    _children_props = {children_props}
    _base_nodes = {base_nodes}
    _namespace = '{namespace}'
    _type = '{typename}'
    @_explicitize_args
    def __init__(self, {default_argtext}):
        self._prop_names = {list_of_valid_keys}
        self._valid_wildcard_attributes =\
            {list_of_valid_wildcard_attr_prefixes}
        self.available_properties = {list_of_valid_keys}
        self.available_wildcard_properties =\
            {list_of_valid_wildcard_attr_prefixes}
        _explicit_args = kwargs.pop('_explicit_args')
        _locals = locals()
        _locals.update(kwargs)  # For wildcard attrs and excess named props
        args = {args}
        {required_validation}
        super({typename}, self).__init__({argtext})
'''

    filtered_props = (
        filter_props(props)
        if (prop_reorder_exceptions is not None and typename in prop_reorder_exceptions)
        or (prop_reorder_exceptions is not None and "ALL" in prop_reorder_exceptions)
        else reorder_props(filter_props(props))
    )
    wildcard_prefixes = repr(parse_wildcards(props))
    list_of_valid_keys = repr(list(map(str, filtered_props.keys())))
    docstring = create_docstring(
        component_name=typename,
        props=filtered_props,
        description=description,
        prop_reorder_exceptions=prop_reorder_exceptions,
    ).replace("\r\n", "\n")
    required_args = required_props(filtered_props)
    is_children_required = "children" in required_args
    required_args = [arg for arg in required_args if arg != "children"]

    prohibit_events(props)

    # pylint: disable=unused-variable
    prop_keys = list(props.keys())
    if "children" in props and "children" in list_of_valid_keys:
        prop_keys.remove("children")
        default_argtext = "children=None, "
        args = "{k: _locals[k] for k in _explicit_args if k != 'children'}"
        argtext = "children=children, **args"
    else:
        default_argtext = ""
        args = "{k: _locals[k] for k in _explicit_args}"
        argtext = "**args"

    if len(required_args) == 0:
        required_validation = ""
    else:
        required_validation = f"""
        for k in {required_args}:
            if k not in args:
                raise TypeError(
                    'Required argument `' + k + '` was not specified.')
        """

    if is_children_required:
        required_validation += """
        if 'children' not in _explicit_args:
            raise TypeError('Required argument children was not specified.')
        """

    default_arglist = [
        (
            f"{p:s}=Component.REQUIRED"
            if props[p]["required"]
            else f"{p:s}=Component.UNDEFINED"
        )
        for p in prop_keys
        if not p.endswith("-*") and p not in python_keywords and p != "setProps"
    ]

    if max_props:
        final_max_props = max_props - (1 if "children" in props else 0)
        if len(default_arglist) > final_max_props:
            default_arglist = default_arglist[:final_max_props]
            docstring += (
                "\n\n"
                "Note: due to the large number of props for this component,\n"
                "not all of them appear in the constructor signature, but\n"
                "they may still be used as keyword arguments."
            )

    default_argtext += ", ".join(default_arglist + ["**kwargs"])
    nodes = collect_nodes({k: v for k, v in props.items() if k != "children"})

    return dedent(
        c.format(
            typename=typename,
            namespace=namespace,
            filtered_props=filtered_props,
            list_of_valid_wildcard_attr_prefixes=wildcard_prefixes,
            list_of_valid_keys=list_of_valid_keys,
            docstring=docstring,
            default_argtext=default_argtext,
            args=args,
            argtext=argtext,
            required_validation=required_validation,
            children_props=nodes,
            base_nodes=filter_base_nodes(nodes) + ["children"],
        )
    )


def generate_class_file(
    typename,
    props,
    description,
    namespace,
    prop_reorder_exceptions=None,
    max_props=None,
):
    """Generate a Python class file (.py) given a class string.
    Parameters
    ----------
    typename
    props
    description
    namespace
    prop_reorder_exceptions
    Returns
    -------
    """
    import_string = (
        "# AUTO GENERATED FILE - DO NOT EDIT\n\n"
        + "from dash.development.base_component import "
        + "Component, _explicitize_args\n\n\n"
    )

    class_string = generate_class_string(
        typename, props, description, namespace, prop_reorder_exceptions, max_props
    )
    file_name = f"{typename:s}.py"

    file_path = os.path.join(namespace, file_name)
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(import_string)
        f.write(class_string)

    print(f"Generated {file_name}")


def generate_imports(project_shortname, components):
    with open(
        os.path.join(project_shortname, "_imports_.py"), "w", encoding="utf-8"
    ) as f:
        component_imports = "\n".join(f"from .{x} import {x}" for x in components)
        all_list = ",\n".join(f'    "{x}"' for x in components)
        imports_string = f"{component_imports}\n\n__all__ = [\n{all_list}\n]"

        f.write(imports_string)


def generate_classes_files(project_shortname, metadata, *component_generators):
    components = []
    for component_path, component_data in metadata.items():
        component_name = component_path.split("/")[-1].split(".")[0]
        components.append(component_name)

        for generator in component_generators:
            generator(
                component_name,
                component_data["props"],
                component_data["description"],
                project_shortname,
            )

    return components


def generate_class(
    typename, props, description, namespace, prop_reorder_exceptions=None
):
    """Generate a Python class object given a class string.
    Parameters
    ----------
    typename
    props
    description
    namespace
    Returns
    -------
    """
    string = generate_class_string(
        typename, props, description, namespace, prop_reorder_exceptions
    )
    scope = {"Component": Component, "_explicitize_args": _explicitize_args}
    # pylint: disable=exec-used
    exec(string, scope)
    result = scope[typename]
    return result


def required_props(props):
    """Pull names of required props from the props object.
    Parameters
    ----------
    props: dict
    Returns
    -------
    list
        List of prop names (str) that are required for the Component
    """
    return [prop_name for prop_name, prop in list(props.items()) if prop["required"]]


def create_docstring(component_name, props, description, prop_reorder_exceptions=None):
    """Create the Dash component docstring.
    Parameters
    ----------
    component_name: str
        Component name
    props: dict
        Dictionary with {propName: propMetadata} structure
    description: str
        Component description
    Returns
    -------
    str
        Dash component docstring
    """
    # Ensure props are ordered with children first
    props = (
        props
        if (
            prop_reorder_exceptions is not None
            and component_name in prop_reorder_exceptions
        )
        or (prop_reorder_exceptions is not None and "ALL" in prop_reorder_exceptions)
        else reorder_props(props)
    )

    n = "n" if component_name[0].lower() in "aeiou" else ""
    args = "\n".join(
        create_prop_docstring(
            prop_name=p,
            type_object=prop["type"] if "type" in prop else prop["flowType"],
            required=prop["required"],
            description=prop["description"],
            default=prop.get("defaultValue"),
            indent_num=0,
            is_flow_type="flowType" in prop and "type" not in prop,
        )
        for p, prop in filter_props(props).items()
    )

    return (
        f"A{n} {component_name} component.\n{description}\n\nKeyword arguments:\n{args}"
    )


def prohibit_events(props):
    """Events have been removed. Raise an error if we see dashEvents or
    fireEvents.
    Parameters
    ----------
    props: dict
        Dictionary with {propName: propMetadata} structure
    Raises
    -------
    ?
    """
    if "dashEvents" in props or "fireEvents" in props:
        raise NonExistentEventException(
            "Events are no longer supported by dash. Use properties instead, "
            "eg `n_clicks` instead of a `click` event."
        )


def parse_wildcards(props):
    """Pull out the wildcard attributes from the Component props.
    Parameters
    ----------
    props: dict
        Dictionary with {propName: propMetadata} structure
    Returns
    -------
    list
        List of Dash valid wildcard prefixes
    """
    list_of_valid_wildcard_attr_prefixes = []
    for wildcard_attr in ["data-*", "aria-*"]:
        if wildcard_attr in props:
            list_of_valid_wildcard_attr_prefixes.append(wildcard_attr[:-1])
    return list_of_valid_wildcard_attr_prefixes


def reorder_props(props):
    """If "children" is in props, then move it to the front to respect dash
    convention, then 'id', then the remaining props sorted by prop name
    Parameters
    ----------
    props: dict
        Dictionary with {propName: propMetadata} structure
    Returns
    -------
    dict
        Dictionary with {propName: propMetadata} structure
    """

    # Constructing an OrderedDict with duplicate keys, you get the order
    # from the first one but the value from the last.
    # Doing this to avoid mutating props, which can cause confusion.
    props1 = [("children", "")] if "children" in props else []
    props2 = [("id", "")] if "id" in props else []
    return OrderedDict(props1 + props2 + sorted(list(props.items())))


def filter_props(props):
    """Filter props from the Component arguments to exclude:
        - Those without a "type" or a "flowType" field
        - Those with arg.type.name in {'func', 'symbol', 'instanceOf'}
    Parameters
    ----------
    props: dict
        Dictionary with {propName: propMetadata} structure
    Returns
    -------
    dict
        Filtered dictionary with {propName: propMetadata} structure
    Examples
    --------
    ```python
    prop_args = {
        'prop1': {
            'type': {'name': 'bool'},
            'required': False,
            'description': 'A description',
            'flowType': {},
            'defaultValue': {'value': 'false', 'computed': False},
        },
        'prop2': {'description': 'A prop without a type'},
        'prop3': {
            'type': {'name': 'func'},
            'description': 'A function prop',
        },
    }
    # filtered_prop_args is now
    # {
    #    'prop1': {
    #        'type': {'name': 'bool'},
    #        'required': False,
    #        'description': 'A description',
    #        'flowType': {},
    #        'defaultValue': {'value': 'false', 'computed': False},
    #    },
    # }
    filtered_prop_args = filter_props(prop_args)
    ```
    """
    filtered_props = copy.deepcopy(props)

    for arg_name, arg in list(filtered_props.items()):
        if "type" not in arg and "flowType" not in arg:
            filtered_props.pop(arg_name)
            continue

        # Filter out functions and instances --
        # these cannot be passed from Python
        if "type" in arg:  # These come from PropTypes
            arg_type = arg["type"]["name"]
            if arg_type in {"func", "symbol", "instanceOf"}:
                filtered_props.pop(arg_name)
        elif "flowType" in arg:  # These come from Flow & handled differently
            arg_type_name = arg["flowType"]["name"]
            if arg_type_name == "signature":
                # This does the same as the PropTypes filter above, but "func"
                # is under "type" if "name" is "signature" vs just in "name"
                if "type" not in arg["flowType"] or arg["flowType"]["type"] != "object":
                    filtered_props.pop(arg_name)
        else:
            raise ValueError

    return filtered_props


def fix_keywords(txt):
    """
    replaces javascript keywords true, false, null with Python keywords
    """
    fix_word = {"true": "True", "false": "False", "null": "None"}
    for js_keyword, python_keyword in fix_word.items():
        txt = txt.replace(js_keyword, python_keyword)
    return txt


# pylint: disable=too-many-arguments
# pylint: disable=too-many-locals
def create_prop_docstring(
    prop_name,
    type_object,
    required,
    description,
    default,
    indent_num,
    is_flow_type=False,
):
    """Create the Dash component prop docstring.
    Parameters
    ----------
    prop_name: str
        Name of the Dash component prop
    type_object: dict
        react-docgen-generated prop type dictionary
    required: bool
        Component is required?
    description: str
        Dash component description
    default: dict
        Either None if a default value is not defined, or
        dict containing the key 'value' that defines a
        default value for the prop
    indent_num: int
        Number of indents to use for the context block
        (creates 2 spaces for every indent)
    is_flow_type: bool
        Does the prop use Flow types? Otherwise, uses PropTypes
    Returns
    -------
    str
        Dash component prop docstring
    """
    py_type_name = js_to_py_type(
        type_object=type_object, is_flow_type=is_flow_type, indent_num=indent_num
    )
    indent_spacing = "  " * indent_num

    default = default["value"] if default else ""
    default = fix_keywords(default)

    is_required = "optional"
    if required:
        is_required = "required"
    elif default and default not in ["None", "{}", "[]"]:
        is_required = "default " + default.replace("\n", "")

    # formats description
    period = "." if description else ""
    description = description.strip().strip(".").replace('"', r"\"") + period
    desc_indent = indent_spacing + "    "
    description = fill(
        description,
        initial_indent=desc_indent,
        subsequent_indent=desc_indent,
        break_long_words=False,
        break_on_hyphens=False,
    )
    description = f"\n{description}" if description else ""
    colon = ":" if description else ""
    description = fix_keywords(description)

    if "\n" in py_type_name:
        # corrects the type
        dict_or_list = "list of dicts" if py_type_name.startswith("list") else "dict"

        # format and rewrite the intro to the nested dicts
        intro1, intro2, dict_descr = py_type_name.partition("with keys:")
        intro = f"`{prop_name}` is a {intro1}{intro2}"
        intro = fill(
            intro,
            initial_indent=desc_indent,
            subsequent_indent=desc_indent,
            break_long_words=False,
            break_on_hyphens=False,
        )

        # captures optional nested dict description and puts the "or" condition on a new line
        if "| dict with keys:" in dict_descr:
            dict_part1, dict_part2 = dict_descr.split(" |", 1)
            dict_part2 = "".join([desc_indent, "Or", dict_part2])
            dict_descr = f"{dict_part1}\n\n  {dict_part2}"

        # ensures indent is correct if there is a second nested list of dicts
        current_indent = dict_descr.lstrip("\n").find("-")
        if current_indent == len(indent_spacing):
            dict_descr = "".join(
                "\n\n    " + line for line in dict_descr.splitlines() if line != ""
            )

        return (
            f"\n{indent_spacing}- {prop_name} ({dict_or_list}; {is_required}){colon}"
            f"{description}"
            f"\n\n{intro}{dict_descr}"
        )
    tn = f"{py_type_name}; " if py_type_name else ""
    return f"\n{indent_spacing}- {prop_name} ({tn}{is_required}){colon}{description}"


def map_js_to_py_types_prop_types(type_object, indent_num):
    """Mapping from the PropTypes js type object to the Python type."""

    def shape_or_exact():
        return "dict with keys:\n" + "\n".join(
            create_prop_docstring(
                prop_name=prop_name,
                type_object=prop,
                required=prop["required"],
                description=prop.get("description", ""),
                default=prop.get("defaultValue"),
                indent_num=indent_num + 2,
            )
            for prop_name, prop in sorted(list(type_object["value"].items()))
        )

    def array_of():
        inner = js_to_py_type(type_object["value"])
        if inner:
            return "list of " + (
                inner + "s"
                if inner.split(" ")[0] != "dict"
                else inner.replace("dict", "dicts", 1)
            )
        return "list"

    def tuple_of():
        elements = [js_to_py_type(element) for element in type_object["elements"]]
        return f"list of {len(elements)} elements: [{', '.join(elements)}]"

    return dict(
        array=lambda: "list",
        bool=lambda: "boolean",
        number=lambda: "number",
        string=lambda: "string",
        object=lambda: "dict",
        any=lambda: "boolean | number | string | dict | list",
        element=lambda: "dash component",
        node=lambda: "a list of or a singular dash component, string or number",
        # React's PropTypes.oneOf
        enum=lambda: (
            "a value equal to: "
            + ", ".join(str(t["value"]) for t in type_object["value"])
        ),
        # React's PropTypes.oneOfType
        union=lambda: " | ".join(
            js_to_py_type(subType)
            for subType in type_object["value"]
            if js_to_py_type(subType) != ""
        ),
        # React's PropTypes.arrayOf
        arrayOf=array_of,
        # React's PropTypes.objectOf
        objectOf=lambda: (
            "dict with strings as keys and values of type "
            + js_to_py_type(type_object["value"])
        ),
        # React's PropTypes.shape
        shape=shape_or_exact,
        # React's PropTypes.exact
        exact=shape_or_exact,
        tuple=tuple_of,
    )


def map_js_to_py_types_flow_types(type_object):
    """Mapping from the Flow js types to the Python type."""
    return dict(
        array=lambda: "list",
        boolean=lambda: "boolean",
        number=lambda: "number",
        string=lambda: "string",
        Object=lambda: "dict",
        any=lambda: "bool | number | str | dict | list",
        Element=lambda: "dash component",
        Node=lambda: "a list of or a singular dash component, string or number",
        # React's PropTypes.oneOfType
        union=lambda: " | ".join(
            js_to_py_type(subType)
            for subType in type_object["elements"]
            if js_to_py_type(subType) != ""
        ),
        # Flow's Array type
        Array=lambda: "list"
        + (
            f' of {js_to_py_type(type_object["elements"][0])}s'
            if js_to_py_type(type_object["elements"][0]) != ""
            else ""
        ),
        # React's PropTypes.shape
        signature=lambda indent_num: (
            "dict with keys:\n"
            + "\n".join(
                create_prop_docstring(
                    prop_name=prop["key"],
                    type_object=prop["value"],
                    required=prop["value"]["required"],
                    description=prop["value"].get("description", ""),
                    default=prop.get("defaultValue"),
                    indent_num=indent_num + 2,
                    is_flow_type=True,
                )
                for prop in type_object["signature"]["properties"]
            )
        ),
    )


def js_to_py_type(type_object, is_flow_type=False, indent_num=0):
    """Convert JS types to Python types for the component definition.
    Parameters
    ----------
    type_object: dict
        react-docgen-generated prop type dictionary
    is_flow_type: bool
        Does the prop use Flow types? Otherwise, uses PropTypes
    indent_num: int
        Number of indents to use for the docstring for the prop
    Returns
    -------
    str
        Python type string
    """

    js_type_name = type_object["name"]
    js_to_py_types = (
        map_js_to_py_types_flow_types(type_object=type_object)
        if is_flow_type
        else map_js_to_py_types_prop_types(
            type_object=type_object, indent_num=indent_num
        )
    )

    if (
        "computed" in type_object
        and type_object["computed"]
        or type_object.get("type", "") == "function"
    ):
        return ""
    if js_type_name in js_to_py_types:
        if js_type_name == "signature":  # This is a Flow object w/ signature
            return js_to_py_types[js_type_name](indent_num)
        # All other types
        return js_to_py_types[js_type_name]()
    return ""



File: dash/development/build_process.py

import os
import sys
import json
import string
import shutil
import logging
import coloredlogs
import fire
import requests

from .._utils import run_command_with_process, compute_hash, job

logger = logging.getLogger(__name__)
coloredlogs.install(
    fmt="%(asctime)s,%(msecs)03d %(levelname)s - %(message)s", datefmt="%H:%M:%S"
)


class BuildProcess:
    def __init__(self, main, deps_info):
        self.logger = logger
        self.main = main
        self.build_folder = self._concat(self.main, "build")
        self.deps_info = deps_info
        self.npm_modules = self._concat(self.main, "node_modules")
        self.package_lock = self._concat(self.main, "package-lock.json")
        self.package = self._concat(self.main, "package.json")
        self._parse_package(path=self.package)
        self.asset_paths = (self.deps_folder, self.npm_modules)

    def _parse_package(self, path):
        with open(path, "r", encoding="utf-8") as fp:
            package = json.load(fp)
            self.version = package["version"]
            self.name = package["name"]
            self.deps_folder = self._concat(self.main, os.pardir, "deps")
            self.deps = package["dependencies"]

    @staticmethod
    def _concat(*paths):
        return os.path.realpath(os.path.sep.join((path for path in paths if path)))

    @staticmethod
    def _clean_path(path):
        if os.path.exists(path):
            logger.warning("🚨 %s already exists, remove it!", path)
            try:
                if os.path.isfile(path):
                    os.remove(path)
                if os.path.isdir(path):
                    shutil.rmtree(path)
            except OSError:
                sys.exit(1)
        else:
            logger.warning("🚨 %s doesn't exist, no action taken", path)

    @job("clean all the previous assets generated by build tool")
    def clean(self):
        for path in self.asset_paths:
            self._clean_path(path)

    @job("run `npm ci`")
    def npm(self):
        """Job to install npm packages."""
        os.chdir(self.main)
        run_command_with_process("npm ci")

    @job("build the renderer in dev mode")
    def watch(self):
        os.chdir(self.main)
        os.system("npm run build:dev")

    @job("run the whole building process in sequence")
    def build(self, build=None):
        self.clean()
        self.npm()
        self.bundles(build)
        self.digest()

    @job("compute the hash digest for assets")
    def digest(self):
        if not os.path.exists(self.deps_folder):
            try:
                os.makedirs(self.deps_folder)
            except OSError:
                logger.exception("🚨 having issues manipulating %s", self.deps_folder)
                sys.exit(1)

        payload = {self.name: self.version}

        for folder in (self.deps_folder, self.build_folder):
            copies = tuple(
                _
                for _ in os.listdir(folder)
                if os.path.splitext(_)[-1] in {".js", ".map"}
            )
            logger.info("bundles in %s %s", folder, copies)

            for copy in copies:
                payload[f"SHA256 ({copy})"] = compute_hash(self._concat(folder, copy))

        with open(self._concat(self.main, "digest.json"), "w", encoding="utf-8") as fp:
            json.dump(payload, fp, sort_keys=True, indent=4, separators=(",", ":"))
        logger.info(
            "bundle digest in digest.json:\n%s",
            json.dumps(payload, sort_keys=True, indent=4),
        )

    @job("copy and generate the bundles")
    def bundles(self, build=None):  # pylint:disable=too-many-locals
        if not os.path.exists(self.deps_folder):
            try:
                os.makedirs(self.deps_folder)
            except OSError:
                logger.exception("🚨 having issues manipulating %s", self.deps_folder)
                sys.exit(1)

        self._parse_package(self.package_lock)

        getattr(self, "_bundles_extra", lambda: None)()

        versions = {
            "version": self.version,
            "package": self.name.replace(" ", "_").replace("-", "_"),
        }

        for scope, name, subfolder, filename, extras in self.deps_info:
            version = self.deps["/".join(filter(None, [scope, name]))]["version"]
            name_squashed = name.replace("-", "").replace(".", "")
            versions[name_squashed] = version

            logger.info("copy npm dependency => %s", filename)
            ext = "min.js" if "min" in filename.split(".") else "js"
            target = f"{name}@{version}.{ext}"

            shutil.copyfile(
                self._concat(self.npm_modules, scope, name, subfolder, filename),
                self._concat(self.deps_folder, target),
            )

            if extras:
                extras_str = '", "'.join(extras)
                versions[f"extra_{name_squashed}_versions"] = f'"{extras_str}"'

                for extra_version in extras:
                    url = f"https://unpkg.com/{name}@{extra_version}/umd/{filename}"
                    res = requests.get(url)
                    extra_target = f"{name}@{extra_version}.{ext}"
                    extra_path = self._concat(self.deps_folder, extra_target)
                    with open(extra_path, "wb") as fp:
                        fp.write(res.content)

        _script = "build:dev" if build == "local" else "build:js"
        logger.info("run `npm run %s`", _script)
        os.chdir(self.main)
        run_command_with_process(f"npm run {_script}")

        logger.info("generate the `__init__.py` from template and versions")
        with open(self._concat(self.main, "init.template"), encoding="utf-8") as fp:
            t = string.Template(fp.read())

        renderer_init = self._concat(self.deps_folder, os.pardir, "_dash_renderer.py")
        with open(renderer_init, "w", encoding="utf-8") as fp:
            fp.write(t.safe_substitute(versions))


class Renderer(BuildProcess):
    def __init__(self):
        """dash-renderer's path is binding with the dash folder hierarchy."""
        extras = ["18.2.0"]  # versions to include beyond what's in package.json
        super().__init__(
            self._concat(os.path.dirname(__file__), os.pardir, "dash-renderer"),
            (
                ("@babel", "polyfill", "dist", "polyfill.min.js", None),
                (None, "react", "umd", "react.production.min.js", extras),
                (None, "react", "umd", "react.development.js", extras),
                (None, "react-dom", "umd", "react-dom.production.min.js", extras),
                (None, "react-dom", "umd", "react-dom.development.js", extras),
                (None, "prop-types", None, "prop-types.min.js", None),
                (None, "prop-types", None, "prop-types.js", None),
            ),
        )


def renderer():
    fire.Fire(Renderer)



File: dash/dash-renderer/setup.py

import json
from setuptools import setup

with open("package.json") as fp:
    package = json.load(fp)

setup(
    name="dash_renderer",
    version=package["version"],
    author="Chris Parmer",
    author_email="chris@plotly.com",
    packages=["dash_renderer"],
    include_package_data=True,
    license="MIT",
    description="Front-end component renderer for Dash",
    install_requires=[],
)



File: dash/long_callback/_proxy_set_props.py

class ProxySetProps(dict):
    """
    Defer dictionary item setter to run a custom function on change.
    Used by background callback manager to save the `set_props` data.
    """

    def __init__(self, on_change):
        super().__init__()
        self.on_change = on_change
        self._data = {}

    def __setitem__(self, key, value):
        self.on_change(key, value)
        self._data.setdefault(key, {})
        self._data[key] = {**self._data[key], **value}

    def get(self, key):
        return self._data.get(key)



File: dash/long_callback/__init__.py

from .managers.celery_manager import (  # noqa: F401,E402
    CeleryLongCallbackManager,
    CeleryManager,
)
from .managers.diskcache_manager import (  # noqa: F401,E402
    DiskcacheLongCallbackManager,
    DiskcacheManager,
)



File: dash/long_callback/managers/__init__.py

from abc import ABC
import inspect
import hashlib


class BaseLongCallbackManager(ABC):
    UNDEFINED = object()

    # Keep a ref to all the ref to register every callback to every manager.
    managers = []

    # Keep every function for late registering.
    functions = []

    def __init__(self, cache_by):
        if cache_by is not None and not isinstance(cache_by, list):
            cache_by = [cache_by]

        self.cache_by = cache_by

        BaseLongCallbackManager.managers.append(self)

        self.func_registry = {}

        # Register all funcs that were added before instantiation.
        # Ensure all celery task are registered.
        for fdetails in self.functions:
            self.register(*fdetails)

    def terminate_job(self, job):
        raise NotImplementedError

    def terminate_unhealthy_job(self, job):
        raise NotImplementedError

    def job_running(self, job):
        raise NotImplementedError

    def make_job_fn(self, fn, progress, key=None):
        raise NotImplementedError

    def call_job_fn(self, key, job_fn, args, context):
        raise NotImplementedError

    def get_progress(self, key):
        raise NotImplementedError

    def result_ready(self, key):
        raise NotImplementedError

    def get_result(self, key, job):
        raise NotImplementedError

    def get_updated_props(self, key):
        raise NotImplementedError

    def build_cache_key(self, fn, args, cache_args_to_ignore):
        fn_source = inspect.getsource(fn)

        if not isinstance(cache_args_to_ignore, (list, tuple)):
            cache_args_to_ignore = [cache_args_to_ignore]

        if cache_args_to_ignore:
            if isinstance(args, dict):
                args = {k: v for k, v in args.items() if k not in cache_args_to_ignore}
            else:
                args = [
                    arg for i, arg in enumerate(args) if i not in cache_args_to_ignore
                ]

        hash_dict = dict(args=args, fn_source=fn_source)

        if self.cache_by is not None:
            # Caching enabled
            for i, cache_item in enumerate(self.cache_by):
                # Call cache function
                hash_dict[f"cache_key_{i}"] = cache_item()

        return hashlib.sha256(str(hash_dict).encode("utf-8")).hexdigest()

    def register(self, key, fn, progress):
        self.func_registry[key] = self.make_job_fn(fn, progress, key)

    @staticmethod
    def register_func(fn, progress, callback_id):
        key = BaseLongCallbackManager.hash_function(fn, callback_id)
        BaseLongCallbackManager.functions.append(
            (
                key,
                fn,
                progress,
            )
        )

        for manager in BaseLongCallbackManager.managers:
            manager.register(key, fn, progress)

        return key

    @staticmethod
    def _make_progress_key(key):
        return key + "-progress"

    @staticmethod
    def _make_set_props_key(key):
        return f"{key}-set_props"

    @staticmethod
    def hash_function(fn, callback_id=""):
        try:
            fn_source = inspect.getsource(fn)
            fn_str = fn_source
        except OSError:  # pylint: disable=too-broad-exception
            fn_str = getattr(fn, "__name__", "")
        return hashlib.sha256(
            callback_id.encode("utf-8") + fn_str.encode("utf-8")
        ).hexdigest()



File: dash/long_callback/managers/diskcache_manager.py

import traceback
from contextvars import copy_context

from . import BaseLongCallbackManager
from .._proxy_set_props import ProxySetProps
from ..._callback_context import context_value
from ..._utils import AttributeDict
from ...exceptions import PreventUpdate

_pending_value = "__$pending__"


class DiskcacheManager(BaseLongCallbackManager):
    """Manage the background execution of callbacks with subprocesses and a diskcache result backend."""

    def __init__(self, cache=None, cache_by=None, expire=None):
        """
        Long callback manager that runs callback logic in a subprocess and stores
        results on disk using diskcache

        :param cache:
            A diskcache.Cache or diskcache.FanoutCache instance. See the diskcache
            documentation for information on configuration options. If not provided,
            a diskcache.Cache instance will be created with default values.
        :param cache_by:
            A list of zero-argument functions.  When provided, caching is enabled and
            the return values of these functions are combined with the callback
            function's input arguments and source code to generate cache keys.
        :param expire:
            If provided, a cache entry will be removed when it has not been accessed
            for ``expire`` seconds.  If not provided, the lifetime of cache entries
            is determined by the default behavior of the ``cache`` instance.
        """
        try:
            import diskcache  # pylint: disable=import-outside-toplevel
            import psutil  # noqa: F401,E402 pylint: disable=import-outside-toplevel,unused-import,unused-variable,import-error
            import multiprocess  # noqa: F401,E402 pylint: disable=import-outside-toplevel,unused-import,unused-variable
        except ImportError as missing_imports:
            raise ImportError(
                """\
DiskcacheLongCallbackManager requires extra dependencies which can be installed doing

    $ pip install "dash[diskcache]"\n"""
            ) from missing_imports

        if cache is None:
            self.handle = diskcache.Cache()
        else:
            if not isinstance(cache, (diskcache.Cache, diskcache.FanoutCache)):
                raise ValueError(
                    "First argument must be a diskcache.Cache "
                    "or diskcache.FanoutCache object"
                )
            self.handle = cache

        self.expire = expire
        super().__init__(cache_by)

    def terminate_job(self, job):
        import psutil  # pylint: disable=import-outside-toplevel,import-error

        if job is None:
            return

        job = int(job)

        # Use diskcache transaction so multiple process don't try to kill the
        # process at the same time
        with self.handle.transact():
            if psutil.pid_exists(job):
                process = psutil.Process(job)

                for proc in process.children(recursive=True):
                    try:
                        proc.kill()
                    except psutil.NoSuchProcess:
                        pass

                try:
                    process.kill()
                except psutil.NoSuchProcess:
                    pass

                try:
                    process.wait(1)
                except (psutil.TimeoutExpired, psutil.NoSuchProcess):
                    pass

    def terminate_unhealthy_job(self, job):
        import psutil  # pylint: disable=import-outside-toplevel,import-error

        job = int(job)

        if job and psutil.pid_exists(job):
            if not self.job_running(job):
                self.terminate_job(job)
                return True

        return False

    def job_running(self, job):
        import psutil  # pylint: disable=import-outside-toplevel,import-error

        job = int(job)

        if job and psutil.pid_exists(job):
            proc = psutil.Process(job)
            return proc.status() != psutil.STATUS_ZOMBIE
        return False

    def make_job_fn(self, fn, progress, key=None):
        return _make_job_fn(fn, self.handle, progress)

    def clear_cache_entry(self, key):
        self.handle.delete(key)

    # noinspection PyUnresolvedReferences
    def call_job_fn(self, key, job_fn, args, context):
        # pylint: disable-next=import-outside-toplevel,no-name-in-module,import-error
        from multiprocess import Process

        # pylint: disable-next=not-callable
        proc = Process(
            target=job_fn,
            args=(key, self._make_progress_key(key), args, context),
        )
        proc.start()
        return proc.pid

    def get_progress(self, key):
        progress_key = self._make_progress_key(key)
        progress_data = self.handle.get(progress_key)
        if progress_data:
            self.handle.delete(progress_key)

        return progress_data

    def result_ready(self, key):
        return self.handle.get(key) is not None

    def get_result(self, key, job):
        # Get result value
        result = self.handle.get(key, self.UNDEFINED)
        if result is self.UNDEFINED:
            return self.UNDEFINED

        # Clear result if not caching
        if self.cache_by is None:
            self.clear_cache_entry(key)
        else:
            if self.expire:
                self.handle.touch(key, expire=self.expire)

        self.clear_cache_entry(self._make_progress_key(key))

        if job:
            self.terminate_job(job)
        return result

    def get_updated_props(self, key):
        set_props_key = self._make_set_props_key(key)
        result = self.handle.get(set_props_key, self.UNDEFINED)
        if result is self.UNDEFINED:
            return {}

        self.clear_cache_entry(set_props_key)

        return result


def _make_job_fn(fn, cache, progress):
    def job_fn(result_key, progress_key, user_callback_args, context):
        def _set_progress(progress_value):
            if not isinstance(progress_value, (list, tuple)):
                progress_value = [progress_value]

            cache.set(progress_key, progress_value)

        maybe_progress = [_set_progress] if progress else []

        def _set_props(_id, props):
            cache.set(f"{result_key}-set_props", {_id: props})

        ctx = copy_context()

        def run():
            c = AttributeDict(**context)
            c.ignore_register_page = False
            c.updated_props = ProxySetProps(_set_props)
            context_value.set(c)
            errored = False
            try:
                if isinstance(user_callback_args, dict):
                    user_callback_output = fn(*maybe_progress, **user_callback_args)
                elif isinstance(user_callback_args, (list, tuple)):
                    user_callback_output = fn(*maybe_progress, *user_callback_args)
                else:
                    user_callback_output = fn(*maybe_progress, user_callback_args)
            except PreventUpdate:
                errored = True
                cache.set(result_key, {"_dash_no_update": "_dash_no_update"})
            except Exception as err:  # pylint: disable=broad-except
                errored = True
                cache.set(
                    result_key,
                    {
                        "long_callback_error": {
                            "msg": str(err),
                            "tb": traceback.format_exc(),
                        }
                    },
                )

            if not errored:
                cache.set(result_key, user_callback_output)

        ctx.run(run)

    return job_fn


class DiskcacheLongCallbackManager(DiskcacheManager):
    """Deprecated: use `from dash import DiskcacheManager` instead."""



File: dash/long_callback/managers/celery_manager.py

import json
import traceback
from contextvars import copy_context

from _plotly_utils.utils import PlotlyJSONEncoder

from dash._callback_context import context_value
from dash._utils import AttributeDict
from dash.exceptions import PreventUpdate
from dash.long_callback._proxy_set_props import ProxySetProps
from dash.long_callback.managers import BaseLongCallbackManager


class CeleryManager(BaseLongCallbackManager):
    """Manage background execution of callbacks with a celery queue."""

    def __init__(self, celery_app, cache_by=None, expire=None):
        """
        Long callback manager that runs callback logic on a celery task queue,
        and stores results using a celery result backend.

        :param celery_app:
            A celery.Celery application instance that must be configured with a
            result backend. See the celery documentation for information on
            configuration options.
        :param cache_by:
            A list of zero-argument functions.  When provided, caching is enabled and
            the return values of these functions are combined with the callback
            function's input arguments and source code to generate cache keys.
        :param expire:
            If provided, a cache entry will be removed when it has not been accessed
            for ``expire`` seconds.  If not provided, the lifetime of cache entries
            is determined by the default behavior of the celery result backend.
        """
        try:
            import celery  # pylint: disable=import-outside-toplevel,import-error
            from celery.backends.base import (  # pylint: disable=import-outside-toplevel,import-error
                DisabledBackend,
            )
        except ImportError as missing_imports:
            raise ImportError(
                """\
CeleryLongCallbackManager requires extra dependencies which can be installed doing

    $ pip install "dash[celery]"\n"""
            ) from missing_imports

        if not isinstance(celery_app, celery.Celery):
            raise ValueError("First argument must be a celery.Celery object")

        if isinstance(celery_app.backend, DisabledBackend):
            raise ValueError("Celery instance must be configured with a result backend")

        self.handle = celery_app
        self.expire = expire
        super().__init__(cache_by)

    def terminate_job(self, job):
        if job is None:
            return

        self.handle.control.terminate(job)

    def terminate_unhealthy_job(self, job):
        task = self.get_task(job)
        if task and task.status in ("FAILURE", "REVOKED"):
            return self.terminate_job(job)
        return False

    def job_running(self, job):
        future = self.get_task(job)
        return future and future.status in (
            "PENDING",
            "RECEIVED",
            "STARTED",
            "RETRY",
            "PROGRESS",
        )

    def make_job_fn(self, fn, progress, key=None):
        return _make_job_fn(fn, self.handle, progress, key)

    def get_task(self, job):
        if job:
            return self.handle.AsyncResult(job)

        return None

    def clear_cache_entry(self, key):
        self.handle.backend.delete(key)

    def call_job_fn(self, key, job_fn, args, context):
        task = job_fn.delay(key, self._make_progress_key(key), args, context)
        return task.task_id

    def get_progress(self, key):
        progress_key = self._make_progress_key(key)
        progress_data = self.handle.backend.get(progress_key)
        if progress_data:
            self.handle.backend.delete(progress_key)
            return json.loads(progress_data)

        return None

    def result_ready(self, key):
        return self.handle.backend.get(key) is not None

    def get_result(self, key, job):
        # Get result value
        result = self.handle.backend.get(key)
        if result is None:
            return self.UNDEFINED

        result = json.loads(result)

        # Clear result if not caching
        if self.cache_by is None:
            self.clear_cache_entry(key)
        else:
            if self.expire:
                # Set/update expiration time
                self.handle.backend.expire(key, self.expire)
        self.clear_cache_entry(self._make_progress_key(key))

        self.terminate_job(job)
        return result

    def get_updated_props(self, key):
        updated_props = self.handle.backend.get(self._make_set_props_key(key))
        if updated_props is None:
            return {}

        self.clear_cache_entry(key)

        return json.loads(updated_props)


def _make_job_fn(fn, celery_app, progress, key):
    cache = celery_app.backend

    @celery_app.task(name=f"long_callback_{key}")
    def job_fn(result_key, progress_key, user_callback_args, context=None):
        def _set_progress(progress_value):
            if not isinstance(progress_value, (list, tuple)):
                progress_value = [progress_value]

            cache.set(progress_key, json.dumps(progress_value, cls=PlotlyJSONEncoder))

        maybe_progress = [_set_progress] if progress else []

        def _set_props(_id, props):
            cache.set(
                f"{result_key}-set_props",
                json.dumps({_id: props}, cls=PlotlyJSONEncoder),
            )

        ctx = copy_context()

        def run():
            c = AttributeDict(**context)
            c.ignore_register_page = False
            c.updated_props = ProxySetProps(_set_props)
            context_value.set(c)
            errored = False
            try:
                if isinstance(user_callback_args, dict):
                    user_callback_output = fn(*maybe_progress, **user_callback_args)
                elif isinstance(user_callback_args, (list, tuple)):
                    user_callback_output = fn(*maybe_progress, *user_callback_args)
                else:
                    user_callback_output = fn(*maybe_progress, user_callback_args)
            except PreventUpdate:
                # Put NoUpdate dict directly to avoid circular imports.
                errored = True
                cache.set(
                    result_key,
                    json.dumps(
                        {"_dash_no_update": "_dash_no_update"}, cls=PlotlyJSONEncoder
                    ),
                )
            except Exception as err:  # pylint: disable=broad-except
                errored = True
                cache.set(
                    result_key,
                    json.dumps(
                        {
                            "long_callback_error": {
                                "msg": str(err),
                                "tb": traceback.format_exc(),
                            }
                        },
                    ),
                )

            if not errored:
                cache.set(
                    result_key, json.dumps(user_callback_output, cls=PlotlyJSONEncoder)
                )

        ctx.run(run)

    return job_fn


class CeleryLongCallbackManager(CeleryManager):
    """Deprecated: use `from dash import CeleryManager` instead."""



File: dash/nbextension/__init__.py




# Dash

[![CircleCI](https://img.shields.io/circleci/project/github/plotly/dash/master.svg)](https://circleci.com/gh/plotly/dash)
[![GitHub](https://img.shields.io/github/license/plotly/dash.svg?color=dark-green)](https://github.com/plotly/dash/blob/master/LICENSE)
[![PyPI](https://img.shields.io/pypi/v/dash.svg?color=dark-green)](https://pypi.org/project/dash/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/dash.svg?color=dark-green)](https://pypi.org/project/dash/)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/y/plotly/dash.svg?color=dark-green)](https://github.com/plotly/dash/graphs/contributors)

#### *Dash is the most downloaded, trusted Python framework for building ML & data science web apps*.

Built on top of [Plotly.js](https://github.com/plotly/plotly.js), [React](https://reactjs.org/) and [Flask](https://palletsprojects.com/p/flask/), Dash ties modern UI elements like dropdowns, sliders, and graphs directly to your analytical Python code. Read [our tutorial](https://dash.plotly.com/getting-started) (proudly crafted ❤️ with Dash itself).

- [Docs](https://dash.plotly.com/getting-started): Create your first Dash app in under 5 minutes

- [dash.gallery](https://dash.gallery): Dash app gallery with Python & R code

<div align="center">
  <a href="https://dash.plotly.com/project-maintenance">
    <img src="https://dash.plotly.com/assets/images/maintained-by-plotly.png" width="400px" alt="Maintained by Plotly">
  </a>
</div>


### Dash App Examples

| Dash App | Description |
|--- | :---: |
|![Sample Dash App](https://user-images.githubusercontent.com/1280389/30086128-9bb4a28e-9267-11e7-8fe4-bbac7d53f2b0.gif) | Here’s a simple example of a Dash App that ties a Dropdown to a Plotly Graph. As the user selects a value in the Dropdown, the application code dynamically exports data from Google Finance into a Pandas DataFrame. This app was written in just **43** lines of code ([view the source](https://gist.github.com/chriddyp/3d2454905d8f01886d651f207e2419f0)). |
|![Crossfiltering Dash App](https://user-images.githubusercontent.com/1280389/30086123-97c58bde-9267-11e7-98a0-7f626de5199a.gif)|Dash app code is declarative and reactive, which makes it easy to build complex apps that contain many interactive elements. Here’s an example with 5 inputs, 3 outputs, and cross filtering. This app was composed in just 160 lines of code, all of which were Python.|
|![Dash App with Mapbox map showing walmart store openings](https://user-images.githubusercontent.com/1280389/30086299-768509d0-9268-11e7-8e6b-626ac9ca512c.gif)| Dash uses [Plotly.js](https://github.com/plotly/plotly.js) for charting. About 50 chart types are supported, including maps. |
|![Financial report](https://user-images.githubusercontent.com/2678795/161153710-57952401-6e07-42d5-ba3e-bab6419998c7.gif)| Dash isn't just for dashboards. You have full control over the look and feel of your applications. Here's a Dash App that's styled to look like a PDF report. |

To learn more about Dash, read the [extensive announcement letter](https://medium.com/@plotlygraphs/introducing-dash-5ecf7191b503) or [jump in with the user guide](https://plotly.com/dash).

### Dash OSS & Dash Enterprise

With Dash Open Source, Dash apps run on your local laptop or workstation, but cannot be easily accessed by others in your organization.

Scale up with Dash Enterprise when your Dash app is ready for department or company-wide consumption. Or, launch your initiative with Dash Enterprise from the start to unlock developer productivity gains and hands-on acceleration from Plotly's team.

ML Ops Features: A one-stop shop for ML Ops: Horizontally scalable hosting, deployment, and authentication for your Dash apps. No IT or DevOps required.
- [**App manager**](https://plotly.com/dash/app-manager/) Deploy & manage Dash apps without needing IT or a DevOps team. App Manager gives you point & click control over all aspects of your Dash deployments.
- [**Kubernetes scaling**](https://plotly.com/dash/kubernetes/) Ensure high availability of Dash apps and scale horizontally with Dash Enterprise’s Kubernetes architecture. No IT or Helm required.
- [**No code auth**](https://plotly.com/dash/authentication/) Control Dash app access in a few clicks. Dash Enterprise supports LDAP, AD, PKI, Okta, SAML, OpenID Connect, OAuth, SSO, and simple email authentication.
- [**Job Queue**](https://plotly.com/dash/job-queue/) The Job Queue is the key to building scalable Dash apps. Move heavy computation from synchronous Dash callbacks to the Job Queue for asynchronous background processing.

Low-Code Features: Low-code Dash app capabilities that supercharge developer productivity.
- [**Design Kit**](https://plotly.com/dash/design-kit/) Design like a pro without writing a line of CSS. Easily arrange, style, brand, and customize your Dash apps.
- [**Snapshot Engine**](https://plotly.com/dash/snapshot-engine/) Save & share Dash app views as links or PDFs. Or, run a Python job through Dash and have Snapshot Engine email a report when the job is done.
- [**Dashboard Toolkit**](https://plotly.com/dash/toolkit/) Drag & drop layouts, chart editing, and crossfilter for your Dash apps.
- [**Embedding**](https://plotly.com/dash/embedding/) Natively embed Dash apps in an existing web application or website without the use of IFrames.

Enterprise AI Features: Everything that your data science team needs to rapidly deliver AI/ML research and business initiatives.
- [**AI App Marketplace**](https://plotly.com/dash/ai-and-ml-templates/) Dash Enterprise ships with dozens of Dash app templates for business problems where AI/ML is having the greatest impact.
- [**Big Data for Pything**](https://plotly.com/dash/big-data-for-python/) Connect to Python's most popular big data back ends: Dask, Databricks, NVIDIA RAPIDS, Snowflake, Postgres, Vaex, and more.
- [**GPU & Dask Acceleration**](https://plotly.com/dash/gpu-dask-acceleration/) Dash Enterprise puts Python’s most popular HPC stack for GPU and parallel CPU computing in the hands of business users.
- [**Data Science Workspaces**](https://plotly.com/dash/workspaces/) Be productive from Day 1. Write and execute Python, R, & Julia code from Dash Enterprise's onboard code editor.

See [https://plotly.com/contact-us/](https://plotly.com/contact-us/) to get in touch.

![Dash Enterprise](https://user-images.githubusercontent.com/2678795/161155614-21c54a22-f821-4dda-b910-ee27e27fb5f2.png)
