aboutsummaryrefslogtreecommitdiff
path: root/venv/lib/python3.8/site-packages/dash/development/base_component.py
diff options
context:
space:
mode:
Diffstat (limited to 'venv/lib/python3.8/site-packages/dash/development/base_component.py')
-rw-r--r--venv/lib/python3.8/site-packages/dash/development/base_component.py481
1 files changed, 481 insertions, 0 deletions
diff --git a/venv/lib/python3.8/site-packages/dash/development/base_component.py b/venv/lib/python3.8/site-packages/dash/development/base_component.py
new file mode 100644
index 0000000..975acfd
--- /dev/null
+++ b/venv/lib/python3.8/site-packages/dash/development/base_component.py
@@ -0,0 +1,481 @@
+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