diff --git a/@plotly/dash-generator-test-component-typescript/src/components/RequiredChildrenComponent.tsx b/@plotly/dash-generator-test-component-typescript/src/components/RequiredChildrenComponent.tsx new file mode 100644 index 0000000000..5842303ace --- /dev/null +++ b/@plotly/dash-generator-test-component-typescript/src/components/RequiredChildrenComponent.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { RequiredChildrenComponentProps } from "../props"; + + +const RequiredChildrenComponent = (props: RequiredChildrenComponentProps) => { + const {children} = props; + return ( +
+ {children} +
+ ) +} + +export default RequiredChildrenComponent; diff --git a/@plotly/dash-generator-test-component-typescript/src/index.ts b/@plotly/dash-generator-test-component-typescript/src/index.ts index db623b462d..d4dacf00c3 100644 --- a/@plotly/dash-generator-test-component-typescript/src/index.ts +++ b/@plotly/dash-generator-test-component-typescript/src/index.ts @@ -6,6 +6,7 @@ import WrappedHTML from './components/WrappedHTML'; import FCComponent from './components/FCComponent'; import EmptyComponent from './components/EmptyComponent'; import MixedComponent from './components/MixedComponent'; +import RequiredChildrenComponent from './components/RequiredChildrenComponent'; export { TypeScriptComponent, @@ -16,4 +17,5 @@ export { FCComponent, EmptyComponent, MixedComponent, + RequiredChildrenComponent, }; diff --git a/@plotly/dash-generator-test-component-typescript/src/props.ts b/@plotly/dash-generator-test-component-typescript/src/props.ts index 86d84af5f6..3912f9ddf1 100644 --- a/@plotly/dash-generator-test-component-typescript/src/props.ts +++ b/@plotly/dash-generator-test-component-typescript/src/props.ts @@ -48,3 +48,7 @@ export type WrappedHTMLProps = { children?: React.ReactNode; id?: string; } & Pick, 'autoFocus'> + +export type RequiredChildrenComponentProps = { + children: React.ReactNode; +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 80f8cffca7..ad18b27287 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Fixed +- [#2218](https://github.com/plotly/dash/pull/2218) Fix bug [#1348](https://github.com/plotly/dash/issues/1348) Validate children prop (required or not). - [#2223](https://github.com/plotly/dash/pull/2223) Exclude hidden folders when building `dash.page_registry`. - [#2182](https://github.com/plotly/dash/pull/2182) Fix [#2172](https://github.com/plotly/dash/issues/2172) Make it so that when using pages, if `suppress_callback_exceptions=True` the `validation_layout` is not set. - [#2152](https://github.com/plotly/dash/pull/2152) Fix bug [#2128](https://github.com/plotly/dash/issues/2128) preventing rendering of multiple components inside a dictionary. diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index 48b8aac5ae..04981a69dd 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -1,7 +1,7 @@ from collections import OrderedDict import copy import os -from textwrap import fill +from textwrap import fill, dedent from dash.development.base_component import _explicitize_args from dash.exceptions import NonExistentEventException @@ -65,11 +65,8 @@ def __init__(self, {default_argtext}): _explicit_args = kwargs.pop('_explicit_args') _locals = locals() _locals.update(kwargs) # For wildcard attrs and excess named props - args = {{k: _locals[k] for k in _explicit_args if k != 'children'}} - for k in {required_props}: - if k not in args: - raise TypeError( - 'Required argument `' + k + '` was not specified.') + args = {args} + {required_validation} super({typename}, self).__init__({argtext}) ''' @@ -87,18 +84,40 @@ def __init__(self, {default_argtext}): description=description, prop_reorder_exceptions=prop_reorder_exceptions, ).replace("\r\n", "\n") + required_args = required_props(filtered_props) + is_children_required = 'children' in required_args + required_args = [arg for arg in required_args if arg != "children"] prohibit_events(props) # pylint: disable=unused-variable prop_keys = list(props.keys()) - if "children" in props: + if "children" in props and "children" in list_of_valid_keys: prop_keys.remove("children") default_argtext = "children=None, " + args = "{k: _locals[k] for k in _explicit_args if k != 'children'}" argtext = "children=children, **args" else: default_argtext = "" + args = "{k: _locals[k] for k in _explicit_args}" argtext = "**args" + + if len(required_args) == 0: + required_validation = "" + else: + required_validation = f""" + for k in {required_args}: + if k not in args: + raise TypeError( + 'Required argument `' + k + '` was not specified.') + """ + + if is_children_required: + required_validation += """ + if 'children' not in _explicit_args: + raise TypeError('Required argument children was not specified.') + """ + default_arglist = [ ( f"{p:s}=Component.REQUIRED" @@ -121,20 +140,23 @@ def __init__(self, {default_argtext}): ) default_argtext += ", ".join(default_arglist + ["**kwargs"]) - required_args = required_props(filtered_props) nodes = collect_nodes({k: v for k, v in props.items() if k != "children"}) - return c.format( - typename=typename, - namespace=namespace, - filtered_props=filtered_props, - list_of_valid_wildcard_attr_prefixes=wildcard_prefixes, - list_of_valid_keys=list_of_valid_keys, - docstring=docstring, - default_argtext=default_argtext, - argtext=argtext, - required_props=required_args, - children_props=nodes, - base_nodes=filter_base_nodes(nodes) + ["children"], + + return dedent( + c.format( + typename=typename, + namespace=namespace, + filtered_props=filtered_props, + list_of_valid_wildcard_attr_prefixes=wildcard_prefixes, + list_of_valid_keys=list_of_valid_keys, + docstring=docstring, + default_argtext=default_argtext, + args=args, + argtext=argtext, + required_validation=required_validation, + children_props=nodes, + base_nodes=filter_base_nodes(nodes) + ["children"], + ) ) diff --git a/tests/integration/test_generation.py b/tests/integration/test_generation.py index c5c5f738f9..25f5178e7e 100644 --- a/tests/integration/test_generation.py +++ b/tests/integration/test_generation.py @@ -9,6 +9,7 @@ TypeScriptComponent, TypeScriptClassComponent, StandardComponent, + RequiredChildrenComponent, ) from dash_test_components import StyledComponent from dash.html import Button, Div @@ -99,3 +100,10 @@ def test_gene003_max_props(): with pytest.raises(TypeError): MyNestedComponent(valuey="nor this") + + +def test_gene004_required_children_prop(): + with pytest.raises(TypeError): + RequiredChildrenComponent() + + RequiredChildrenComponent(children='worked') diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index 42f8b3fb78..8f1dac0a37 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -100,8 +100,5 @@ def __init__(self, children=None, optionalArray=Component.UNDEFINED, optionalBoo _locals = locals() _locals.update(kwargs) # For wildcard attrs and excess named props args = {k: _locals[k] for k in _explicit_args if k != 'children'} - for k in []: - if k not in args: - raise TypeError( - 'Required argument `' + k + '` was not specified.') + super(Table, self).__init__(children=children, **args) diff --git a/tests/unit/development/test_base_component.py b/tests/unit/development/test_base_component.py index 49d3d969ff..6b7bb96757 100644 --- a/tests/unit/development/test_base_component.py +++ b/tests/unit/development/test_base_component.py @@ -534,3 +534,8 @@ def update(v): @app.callback(Output(output1, "children"), Input(input1, "value")) def update2(v): return f"Input 1 {v}" + + +def test_debc030_invalid_children_args(): + with pytest.raises(TypeError): + dcc.Input(children='invalid children')