aboutsummaryrefslogtreecommitdiff
path: root/venv/lib/python3.8/site-packages/dash/development/base_component.py
blob: 975acfd5372bd9f87156276bd080819210c17c59 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
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