import abc import collections import inspect import sys import uuid import random from .._utils import patch_collections_abc, stringify_id, OrderedSet MutableSequence = patch_collections_abc("MutableSequence") rd = random.Random(0) # 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) @classmethod def get_resources(cls, resource_name): resources = [] for module_name in cls.registry: 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 ComponentRegistry.registry.add(module) ComponentRegistry.children_props[attributes.get("_namespace", module)][ 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): 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 _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