diff --git a/CHANGELOG.md b/CHANGELOG.md index a815905269..83d1e1e17d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). clientside JavaScript callbacks via inline strings. - [#1020](https://github.com/plotly/dash/pull/1020) Allow `visit_and_snapshot` API in `dash.testing.browser` to stay on the page so you can run other checks. +### Changed +- [#1026](https://github.com/plotly/dash/pull/1026) Better error message when you forget to wrap multiple `children` in an array, and they get passed to other props. + ### Fixed - [#1018](https://github.com/plotly/dash/pull/1006) Fix the `dash.testing` **stop** API with process application runner in Python2. Use `kill()` instead of `communicate()` to avoid hanging. - [#1027](https://github.com/plotly/dash/pull/1027) Fix bug with renderer callback lock never resolving with non-rendered async component using the asyncDecorator diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 070c4adc90..3318a76d86 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -94,6 +94,14 @@ def __init__(self, **kwargs): ", ".join(sorted(self._prop_names)) ) ) + + if k != "children" and isinstance(v, Component): + raise TypeError( + "Component detected as a prop other than `children`\n" + + "Did you forget to wrap multiple `children` in an array?\n" + + "Prop {} has value {}\n".format(k, repr(v)) + ) + setattr(self, k, v) def to_plotly_json(self): diff --git a/tests/integration/devtools/test_devtools_error_handling.py b/tests/integration/devtools/test_devtools_error_handling.py index e62fe31077..167ae4ee5b 100644 --- a/tests/integration/devtools/test_devtools_error_handling.py +++ b/tests/integration/devtools/test_devtools_error_handling.py @@ -186,6 +186,8 @@ def update_output(n_clicks): dev_tools_hot_reload=False, ) + dash_duo.wait_for_element('.js-plotly-plot .main-svg') + dash_duo.find_element("#button").click() dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "1") dash_duo.percy_snapshot("devtools - validation exception - closed") diff --git a/tests/unit/development/test_base_component.py b/tests/unit/development/test_base_component.py index a2984b65d4..6da20789e3 100644 --- a/tests/unit/development/test_base_component.py +++ b/tests/unit/development/test_base_component.py @@ -4,6 +4,7 @@ import pytest from dash.development.base_component import Component +import dash_html_components as html Component._prop_names = ("id", "a", "children", "style") Component._type = "TestComponent" @@ -36,23 +37,23 @@ def nested_tree(): return c, c1, c2, c3, c4, c5 -def test_init(): +def test_debc001_init(): Component(a=3) -def test_get_item_with_children(): +def test_debc002_get_item_with_children(): c1 = Component(id="1") c2 = Component(children=[c1]) assert c2["1"] == c1 -def test_get_item_with_children_as_component_instead_of_list(): +def test_debc003_get_item_with_children_as_component_instead_of_list(): c1 = Component(id="1") c2 = Component(id="2", children=c1) assert c2["1"] == c1 -def test_get_item_with_nested_children_one_branch(): +def test_debc004_get_item_with_nested_children_one_branch(): c1 = Component(id="1") c2 = Component(id="2", children=[c1]) c3 = Component(children=[c2]) @@ -61,7 +62,7 @@ def test_get_item_with_nested_children_one_branch(): assert c3["1"] == c1 -def test_get_item_with_nested_children_two_branches(): +def test_debc005_get_item_with_nested_children_two_branches(): c1 = Component(id="1") c2 = Component(id="2", children=[c1]) c3 = Component(id="3") @@ -75,7 +76,7 @@ def test_get_item_with_nested_children_two_branches(): assert c5["3"] == c3 -def test_get_item_with_nested_children_with_mixed_strings_and_without_lists(): +def test_debc006_get_item_with_full_tree(): c, c1, c2, c3, c4, c5 = nested_tree() keys = [k for k in c] @@ -90,7 +91,7 @@ def test_get_item_with_nested_children_with_mixed_strings_and_without_lists(): c["x"] -def test_len_with_nested_children_with_mixed_strings_and_without_lists(): +def test_debc007_len_with_full_tree(): c = nested_tree()[0] assert ( len(c) == 5 + 5 + 1 @@ -98,7 +99,7 @@ def test_len_with_nested_children_with_mixed_strings_and_without_lists(): components, 2 strings + 2 numbers + none in c2, and 1 string in c1" -def test_set_item_with_nested_children_with_mixed_strings_and_without_lists(): +def test_debc008_set_item_anywhere_in_tree(): keys = ["0.0", "0.1", "0.1.x", "0.1.x.x", "0.1.x.x.0"] c = nested_tree()[0] @@ -110,7 +111,7 @@ def test_set_item_with_nested_children_with_mixed_strings_and_without_lists(): assert c[new_id] == new_component -def test_del_item_with_nested_children_with_mixed_strings_and_without_lists(): +def test_debc009_del_item_full_tree(): c = nested_tree()[0] keys = reversed([k for k in c]) for key in keys: @@ -120,13 +121,13 @@ def test_del_item_with_nested_children_with_mixed_strings_and_without_lists(): c[key] -def test_traverse_with_nested_children_with_mixed_strings_and_without_lists(): +def test_debc010_traverse_full_tree(): c, c1, c2, c3, c4, c5 = nested_tree() elements = [i for i in c._traverse()] assert elements == c.children + [c3] + [c2] + c2.children -def test_traverse_with_tuples(): +def test_debc011_traverse_with_tuples(): c, c1, c2, c3, c4, c5 = nested_tree() c2.children = tuple(c2.children) c.children = tuple(c.children) @@ -134,7 +135,7 @@ def test_traverse_with_tuples(): assert elements == list(c.children) + [c3] + [c2] + list(c2.children) -def test_to_plotly_json_with_nested_children_with_mixed_strings_and_without_lists(): +def test_debc012_to_plotly_json_full_tree(): c = nested_tree()[0] Component._namespace Component._type @@ -194,7 +195,7 @@ def test_to_plotly_json_with_nested_children_with_mixed_strings_and_without_list assert res == expected -def test_get_item_raises_key_if_id_doesnt_exist(): +def test_debc013_get_item_raises_key_if_id_doesnt_exist(): c = Component() with pytest.raises(KeyError): c["1"] @@ -212,7 +213,7 @@ def test_get_item_raises_key_if_id_doesnt_exist(): c3["0"] -def test_set_item(): +def test_debc014_set_item(): c1a = Component(id="1", children="Hello world") c2 = Component(id="2", children=c1a) assert c2["1"] == c1a @@ -222,7 +223,7 @@ def test_set_item(): assert c2["1"] == c1b -def test_set_item_with_children_as_list(): +def test_debc015_set_item_with_children_as_list(): c1 = Component(id="1") c2 = Component(id="2", children=[c1]) assert c2["1"] == c1 @@ -231,7 +232,7 @@ def test_set_item_with_children_as_list(): assert c2["3"] == c3 -def test_set_item_with_nested_children(): +def test_debc016_set_item_with_nested_children(): c1 = Component(id="1") c2 = Component(id="2", children=[c1]) c3 = Component(id="3") @@ -256,14 +257,14 @@ def test_set_item_with_nested_children(): c5["1"] -def test_set_item_raises_key_error(): +def test_debc017_set_item_raises_key_error(): c1 = Component(id="1") c2 = Component(id="2", children=[c1]) with pytest.raises(KeyError): c2["3"] = Component(id="3") -def test_del_item_from_list(): +def test_debc018_del_item_from_list(): c1 = Component(id="1") c2 = Component(id="2") c3 = Component(id="3", children=[c1, c2]) @@ -280,7 +281,7 @@ def test_del_item_from_list(): assert c3.children == [] -def test_del_item_from_class(): +def test_debc019_del_item_from_class(): c1 = Component(id="1") c2 = Component(id="2", children=c1) assert c2["1"] == c1 @@ -291,7 +292,7 @@ def test_del_item_from_class(): assert c2.children is None -def test_to_plotly_json_without_children(): +def test_debc020_to_plotly_json_without_children(): c = Component(id="a") c._prop_names = ("id",) c._type = "MyComponent" @@ -303,7 +304,7 @@ def test_to_plotly_json_without_children(): } -def test_to_plotly_json_with_null_arguments(): +def test_debc021_to_plotly_json_with_null_arguments(): c = Component(id="a") c._prop_names = ("id", "style") c._type = "MyComponent" @@ -325,7 +326,7 @@ def test_to_plotly_json_with_null_arguments(): } -def test_to_plotly_json_with_children(): +def test_debc022_to_plotly_json_with_children(): c = Component(id="a", children="Hello World") c._prop_names = ("id", "children") c._type = "MyComponent" @@ -341,7 +342,7 @@ def test_to_plotly_json_with_children(): } -def test_to_plotly_json_with_wildcards(): +def test_debc023_to_plotly_json_with_wildcards(): c = Component( id="a", **{"aria-expanded": "true", "data-toggle": "toggled", "data-none": None} ) @@ -360,7 +361,7 @@ def test_to_plotly_json_with_wildcards(): } -def test_len(): +def test_debc024_len(): assert len(Component()) == 0 assert len(Component(children="Hello World")) == 1 assert len(Component(children=Component())) == 1 @@ -368,7 +369,7 @@ def test_len(): assert len(Component(children=[Component(children=Component()), Component()])) == 3 -def test_iter(): +def test_debc025_iter(): # The mixin methods from MutableMapping were cute but probably never # used - at least not by us. Test that they're gone @@ -418,3 +419,19 @@ def test_iter(): assert k in keys, "iteration produces key " + k assert len(keys) == len(keys2), "iteration produces no extra keys" + + +def test_debc026_component_not_children(): + children = [Component(id='a'), html.Div(id='b'), 'c', 1] + for i in range(len(children)): + # cycle through each component in each position + children = children[1:] + [children[0]] + + # use html.Div because only real components accept positional args + html.Div(children) + # the first arg is children, and a single component works there + html.Div(children[0], id='x') + + with pytest.raises(TypeError): + # If you forget the `[]` around children you get this: + html.Div(children[0], children[1], children[2], children[3])