Skip to content

Commit 3dfa941

Browse files
rmarren1chriddyp
authored andcommitted
Support for wildcard attributes, implement data-* attribute (#237)
* data-* and aria-* attribute support, with serialization * edit callback validator for * attrs and a integration test * Tests for data-* aria-* docgen, repr * fixed linting issues and date-time issue with testing * updated changelog.md and version.py
1 parent cc66c5e commit 3dfa941

11 files changed

+261
-22
lines changed

Diff for: CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.21.1 - 2018-04-10
2+
## Added
3+
- `aria-*` and `data-*` attributes are now supported in all dash html components. (#40)
4+
- These new keywords can be added using a dictionary expansion, e.g. `html.Div(id="my-div", **{"data-toggle": "toggled", "aria-toggled": "true"})`
5+
16
## 0.21.0 - 2018-02-21
27
## Added
38
- #207 Dash now supports React components that use [Flow](https://flow.org/en/docs/react/).

Diff for: dash/dash.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,9 @@ def _validate_callback(self, output, inputs, state, events):
408408

409409
if (hasattr(arg, 'component_property') and
410410
arg.component_property not in
411-
component.available_properties):
411+
component.available_properties and not
412+
any(arg.component_property.startswith(w) for w in
413+
component.available_wildcard_properties)):
412414
raise exceptions.NonExistantPropException('''
413415
Attempting to assign a callback with
414416
the property "{}" but the component

Diff for: dash/development/base_component.py

+64-16
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ def __init__(self, **kwargs):
2323
# pylint: disable=super-init-not-called
2424
for k, v in list(kwargs.items()):
2525
# pylint: disable=no-member
26-
if k not in self._prop_names:
26+
k_in_propnames = k in self._prop_names
27+
k_in_wildcards = any([k.startswith(w)
28+
for w in
29+
self._valid_wildcard_attributes])
30+
if not k_in_propnames and not k_in_wildcards:
2731
raise TypeError(
2832
'Unexpected keyword argument `{}`'.format(k) +
2933
'\nAllowed arguments: {}'.format(
@@ -34,10 +38,21 @@ def __init__(self, **kwargs):
3438
setattr(self, k, v)
3539

3640
def to_plotly_json(self):
41+
# Add normal properties
42+
props = {
43+
p: getattr(self, p)
44+
for p in self._prop_names # pylint: disable=no-member
45+
if hasattr(self, p)
46+
}
47+
# Add the wildcard properties data-* and aria-*
48+
props.update({
49+
k: getattr(self, k)
50+
for k in self.__dict__
51+
if any(k.startswith(w) for w in
52+
self._valid_wildcard_attributes) # pylint:disable=no-member
53+
})
3754
as_json = {
38-
'props': {p: getattr(self, p)
39-
for p in self._prop_names # pylint: disable=no-member
40-
if hasattr(self, p)},
55+
'props': props,
4156
'type': self._type, # pylint: disable=no-member
4257
'namespace': self._namespace # pylint: disable=no-member
4358
}
@@ -225,8 +240,12 @@ def __init__(self, {default_argtext}):
225240
self._prop_names = {list_of_valid_keys}
226241
self._type = '{typename}'
227242
self._namespace = '{namespace}'
243+
self._valid_wildcard_attributes =\
244+
{list_of_valid_wildcard_attr_prefixes}
228245
self.available_events = {events}
229246
self.available_properties = {list_of_valid_keys}
247+
self.available_wildcard_properties =\
248+
{list_of_valid_wildcard_attr_prefixes}
230249
231250
for k in {required_args}:
232251
if k not in kwargs:
@@ -236,15 +255,23 @@ def __init__(self, {default_argtext}):
236255
super({typename}, self).__init__({argtext})
237256
238257
def __repr__(self):
239-
if(any(getattr(self, c, None) is not None for c in self._prop_names
240-
if c is not self._prop_names[0])):
241-
242-
return (
243-
'{typename}(' +
244-
', '.join([c+'='+repr(getattr(self, c, None))
245-
for c in self._prop_names
246-
if getattr(self, c, None) is not None])+')')
247-
258+
if(any(getattr(self, c, None) is not None
259+
for c in self._prop_names
260+
if c is not self._prop_names[0])
261+
or any(getattr(self, c, None) is not None
262+
for c in self.__dict__.keys()
263+
if any(c.startswith(wc_attr)
264+
for wc_attr in self._valid_wildcard_attributes))):
265+
props_string = ', '.join([c+'='+repr(getattr(self, c, None))
266+
for c in self._prop_names
267+
if getattr(self, c, None) is not None])
268+
wilds_string = ', '.join([c+'='+repr(getattr(self, c, None))
269+
for c in self.__dict__.keys()
270+
if any([c.startswith(wc_attr)
271+
for wc_attr in
272+
self._valid_wildcard_attributes])])
273+
return ('{typename}(' + props_string +
274+
(', ' + wilds_string if wilds_string != '' else '') + ')')
248275
else:
249276
return (
250277
'{typename}(' +
@@ -253,6 +280,8 @@ def __repr__(self):
253280

254281
filtered_props = reorder_props(filter_props(props))
255282
# pylint: disable=unused-variable
283+
list_of_valid_wildcard_attr_prefixes = repr(parse_wildcards(props))
284+
# pylint: disable=unused-variable
256285
list_of_valid_keys = repr(list(filtered_props.keys()))
257286
# pylint: disable=unused-variable
258287
docstring = create_docstring(
@@ -273,11 +302,9 @@ def __repr__(self):
273302

274303
required_args = required_props(props)
275304

276-
d = c.format(**locals())
277-
278305
scope = {'Component': Component}
279306
# pylint: disable=exec-used
280-
exec(d, scope)
307+
exec(c.format(**locals()), scope)
281308
result = scope[typename]
282309
return result
283310

@@ -366,6 +393,27 @@ def parse_events(props):
366393
return events
367394

368395

396+
def parse_wildcards(props):
397+
"""
398+
Pull out the wildcard attributes from the Component props
399+
400+
Parameters
401+
----------
402+
props: dict
403+
Dictionary with {propName: propMetadata} structure
404+
405+
Returns
406+
-------
407+
list
408+
List of Dash valid wildcard prefixes
409+
"""
410+
list_of_valid_wildcard_attr_prefixes = []
411+
for wildcard_attr in ["data-*", "aria-*"]:
412+
if wildcard_attr in props.keys():
413+
list_of_valid_wildcard_attr_prefixes.append(wildcard_attr[:-1])
414+
return list_of_valid_wildcard_attr_prefixes
415+
416+
369417
def reorder_props(props):
370418
"""
371419
If "children" is in props, then move it to the

Diff for: dash/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.21.0'
1+
__version__ = '0.21.1'

Diff for: dev-requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
dash_core_components>=0.4.0
2-
dash_html_components>=0.5.0
2+
dash_html_components>=0.11.0rc1
33
dash_flow_example==0.0.3
44
dash_renderer
55
percy

Diff for: requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ appnope==0.1.0
22
backports.shutil-get-terminal-size==1.0.0
33
click==6.7
44
dash-core-components==0.3.3
5-
dash-html-components==0.4.0
5+
dash-html-components==0.11.0rc1
66
dash-renderer==0.2.9
77
dash.ly==0.14.0
88
decorator==4.0.11

Diff for: tests/development/metadata_test.json

+14
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,20 @@
207207
"required": false,
208208
"description": ""
209209
},
210+
"data-*": {
211+
"type": {
212+
"name": "string"
213+
},
214+
"required": false,
215+
"description": ""
216+
},
217+
"aria-*": {
218+
"type": {
219+
"name": "string"
220+
},
221+
"required": false,
222+
"description": ""
223+
},
210224
"id": {
211225
"type": {
212226
"name": "string"

Diff for: tests/development/test_base_component.py

+36
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
Component._prop_names = ('id', 'a', 'children', 'style', )
1818
Component._type = 'TestComponent'
1919
Component._namespace = 'test_namespace'
20+
Component._valid_wildcard_attributes = ['data-', 'aria-']
2021

2122

2223
def nested_tree():
@@ -411,6 +412,25 @@ def to_dict(id, children):
411412
)
412413
"""
413414

415+
def test_to_plotly_json_with_wildcards(self):
416+
c = Component(id='a', **{'aria-expanded': 'true',
417+
'data-toggle': 'toggled',
418+
'data-none': None})
419+
c._prop_names = ('id',)
420+
c._type = 'MyComponent'
421+
c._namespace = 'basic'
422+
self.assertEqual(
423+
c.to_plotly_json(),
424+
{'namespace': 'basic',
425+
'props': {
426+
'aria-expanded': 'true',
427+
'data-toggle': 'toggled',
428+
'data-none': None,
429+
'id': 'a',
430+
},
431+
'type': 'MyComponent'}
432+
)
433+
414434
def test_len(self):
415435
self.assertEqual(len(Component()), 0)
416436
self.assertEqual(len(Component(children='Hello World')), 1)
@@ -580,6 +600,16 @@ def test_repr_nested_arguments(self):
580600
"Table(Table(children=Table(id='1'), id='2'))"
581601
)
582602

603+
def test_repr_with_wildcards(self):
604+
c = self.ComponentClass(id='1', **{"data-one": "one",
605+
"aria-two": "two"})
606+
data_first = "Table(id='1', data-one='one', aria-two='two')"
607+
aria_first = "Table(id='1', aria-two='two', data-one='one')"
608+
repr_string = repr(c)
609+
if not (repr_string == data_first or repr_string == aria_first):
610+
raise Exception("%s\nDoes not equal\n%s\nor\n%s" %
611+
(repr_string, data_first, aria_first))
612+
583613
def test_docstring(self):
584614
assert_docstring(self.assertEqual, self.ComponentClass.__doc__)
585615

@@ -674,6 +704,10 @@ def setUp(self):
674704

675705
['customArrayProp', 'list'],
676706

707+
['data-*', 'string'],
708+
709+
['aria-*', 'string'],
710+
677711
['id', 'string'],
678712

679713
['dashEvents', "a value equal to: 'restyle', 'relayout', 'click'"]
@@ -749,6 +783,8 @@ def assert_docstring(assertEqual, docstring):
749783

750784
"- customProp (optional)",
751785
"- customArrayProp (list; optional)",
786+
'- data-* (string; optional)',
787+
'- aria-* (string; optional)',
752788
'- id (string; optional)',
753789
'',
754790
"Available events: 'restyle', 'relayout', 'click'",

Diff for: tests/development/test_component_loader.py

+17
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,20 @@
2828
"description": "Children",
2929
"required": false
3030
},
31+
"data-*": {
32+
"type": {
33+
"name": "string"
34+
},
35+
"description": "Wildcard data",
36+
"required": false
37+
},
38+
"aria-*": {
39+
"type": {
40+
"name": "string"
41+
},
42+
"description": "Wildcard aria",
43+
"required": false
44+
},
3145
"bar": {
3246
"type": {
3347
"name": "custom"
@@ -113,6 +127,9 @@ def test_loadcomponents(self):
113127
'foo': 'Hello World',
114128
'bar': 'Lah Lah',
115129
'baz': 'Lemons',
130+
'data-foo': 'Blah',
131+
'aria-bar': 'Seven',
132+
'baz': 'Lemons',
116133
'children': 'Child'
117134
}
118135
AKwargs = {

0 commit comments

Comments
 (0)