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
|