482 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			482 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import abc
 | 
						|
import collections
 | 
						|
import inspect
 | 
						|
import sys
 | 
						|
import typing
 | 
						|
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):
 | 
						|
        module = attributes["__module__"].split(".")[0]
 | 
						|
 | 
						|
        if attributes.get("_explicitize_dash_init", False):
 | 
						|
            # We only want to patch the new generated component without
 | 
						|
            # the `@_explicitize_args` decorator for mypy support
 | 
						|
            # See issue: https://github.com/plotly/dash/issues/3226
 | 
						|
            # Only for component that were generated by 3.0.3
 | 
						|
            # Better to setattr on the component afterwards to ensure
 | 
						|
            # backward compatibility.
 | 
						|
            attributes["__init__"] = _explicitize_args(attributes["__init__"])
 | 
						|
 | 
						|
        _component = abc.ABCMeta.__new__(mcs, name, bases, attributes)
 | 
						|
 | 
						|
        if name == "Component" or module == "builtins":
 | 
						|
            # Don't add to the registry 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"]
 | 
						|
    _namespace: str
 | 
						|
    _type: str
 | 
						|
    _prop_names: typing.List[str]
 | 
						|
 | 
						|
    _valid_wildcard_attributes: typing.List[str]
 | 
						|
    available_wildcard_properties: typing.List[str]
 | 
						|
 | 
						|
    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
 | 
						|
 | 
						|
        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:  # type: ignore[reportAttributeAccessIssue]
 | 
						|
                    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):  # type: ignore[reportOptionalIterable]
 | 
						|
                # 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  # type: ignore[reportOptionalSubscript]
 | 
						|
                        return
 | 
						|
                    if operation == "delete":
 | 
						|
                        del self.children[i]  # type: ignore[reportOptionalSubscript]
 | 
						|
                        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):  # type: ignore[reportOptionalIterable]
 | 
						|
                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  # type: ignore[reportAttributeAccessIssue]
 | 
						|
 | 
						|
    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:  # type: ignore[reportOptionalIterable]
 | 
						|
                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)))
 | 
						|
 | 
						|
 | 
						|
# Renderable node type.
 | 
						|
ComponentType = typing.Union[
 | 
						|
    str,
 | 
						|
    int,
 | 
						|
    float,
 | 
						|
    Component,
 | 
						|
    None,
 | 
						|
    typing.Sequence[typing.Union[str, int, float, Component, None]],
 | 
						|
]
 | 
						|
 | 
						|
ComponentTemplate = typing.TypeVar("ComponentTemplate")
 | 
						|
 | 
						|
 | 
						|
# This wrapper adds an argument given to generated Component.__init__
 | 
						|
# with the actual given parameters by the user as a list of string.
 | 
						|
# This is then checked in the generated init to check if required
 | 
						|
# props were provided.
 | 
						|
def _explicitize_args(func):
 | 
						|
    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)
 | 
						|
 | 
						|
    new_sig = inspect.signature(wrapper).replace(
 | 
						|
        parameters=list(inspect.signature(func).parameters.values())
 | 
						|
    )
 | 
						|
    wrapper.__signature__ = new_sig  # type: ignore[reportFunctionMemberAccess]
 | 
						|
    return wrapper
 |