Skip to content

Commit 07be9e2

Browse files
authored
Merge pull request #2029 from plotly/limit-component-args
Limit generated components to 250 explicit args by default
2 parents 3860484 + b43044d commit 07be9e2

File tree

8 files changed

+88
-29
lines changed

8 files changed

+88
-29
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ npm-debug*
7373
dash_generator_test_component_standard/
7474
dash_generator_test_component_nested/
7575
dash_test_components/
76+
dash_generator_test_component_typescript/
7677
inst/
7778
man/
7879
R/

@plotly/dash-generator-test-component-standard/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"scripts": {
1111
"build:js": "webpack --mode production",
1212
"setup": "python setup.py sdist",
13-
"build:py_and_r": "dash-generate-components ./src/components dash_generator_test_component_standard && cp base/** dash_generator_test_component_standard/ && dash-generate-components ./src/components dash_generator_test_component_standard --r-prefix 'dgtc_standard'",
13+
"build:py_and_r": "dash-generate-components ./src/components dash_generator_test_component_standard && cp base/** dash_generator_test_component_standard/ && dash-generate-components ./src/components dash_generator_test_component_standard --r-prefix 'dgtc_standard' --max-props 2",
1414
"build": "run-s build:js build:py_and_r setup"
1515
},
1616
"author": "Chris Parmer <[email protected]>",

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ This project adheres to [Semantic Versioning](https://semver.org/).
1818

1919
### Fixed
2020

21+
- [#2029](https://github.com/plotly/dash/pull/2029) Restrict the number of props listed explicitly in generated component constructors - default is 250. This prevents exceeding the Python 3.6 limit of 255 arguments. The omitted props are still in the docstring and can still be provided the same as before, they just won't appear in the signature so autocompletion may be affected.
22+
2123
- [#1968](https://github.com/plotly/dash/pull/1968) Fix bug [#1877](https://github.com/plotly/dash/issues/1877), code which uses `merge_duplicate_headers` and `style_header_conditional` to highlight columns, it incorrectly highlights header cells.
2224

2325
- [#2015](https://github.com/plotly/dash/pull/2015) Fix bug [#1854](https://github.com/plotly/dash/issues/1854) in which the combination of row_selectable="single or multi" and filter_action="native" caused the JS error.

dash/development/_py_components_generation.py

+36-16
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99
from .base_component import Component
1010

1111

12-
# pylint: disable=unused-argument
12+
# pylint: disable=unused-argument,too-many-locals
1313
def generate_class_string(
14-
typename, props, description, namespace, prop_reorder_exceptions=None
14+
typename,
15+
props,
16+
description,
17+
namespace,
18+
prop_reorder_exceptions=None,
19+
max_props=None,
1520
):
1621
"""Dynamically generate class strings to have nicely formatted docstrings,
1722
keyword arguments, and repr.
@@ -56,7 +61,7 @@ def __init__(self, {default_argtext}):
5661
{list_of_valid_wildcard_attr_prefixes}
5762
_explicit_args = kwargs.pop('_explicit_args')
5863
_locals = locals()
59-
_locals.update(kwargs) # For wildcard attrs
64+
_locals.update(kwargs) # For wildcard attrs and excess named props
6065
args = {{k: _locals[k] for k in _explicit_args if k != 'children'}}
6166
for k in {required_props}:
6267
if k not in args:
@@ -91,18 +96,28 @@ def __init__(self, {default_argtext}):
9196
else:
9297
default_argtext = ""
9398
argtext = "**args"
94-
default_argtext += ", ".join(
95-
[
96-
(
97-
f"{p:s}=Component.REQUIRED"
98-
if props[p]["required"]
99-
else f"{p:s}=Component.UNDEFINED"
99+
default_arglist = [
100+
(
101+
f"{p:s}=Component.REQUIRED"
102+
if props[p]["required"]
103+
else f"{p:s}=Component.UNDEFINED"
104+
)
105+
for p in prop_keys
106+
if not p.endswith("-*") and p not in python_keywords and p != "setProps"
107+
]
108+
109+
if max_props:
110+
final_max_props = max_props - (1 if "children" in props else 0)
111+
if len(default_arglist) > final_max_props:
112+
default_arglist = default_arglist[:final_max_props]
113+
docstring += (
114+
"\n\n"
115+
"Note: due to the large number of props for this component,\n"
116+
"not all of them appear in the constructor signature, but\n"
117+
"they may still be used as keyword arguments."
100118
)
101-
for p in prop_keys
102-
if not p.endswith("-*") and p not in python_keywords and p != "setProps"
103-
]
104-
+ ["**kwargs"]
105-
)
119+
120+
default_argtext += ", ".join(default_arglist + ["**kwargs"])
106121
required_args = required_props(filtered_props)
107122
return c.format(
108123
typename=typename,
@@ -118,7 +133,12 @@ def __init__(self, {default_argtext}):
118133

119134

120135
def generate_class_file(
121-
typename, props, description, namespace, prop_reorder_exceptions=None
136+
typename,
137+
props,
138+
description,
139+
namespace,
140+
prop_reorder_exceptions=None,
141+
max_props=None,
122142
):
123143
"""Generate a Python class file (.py) given a class string.
124144
Parameters
@@ -138,7 +158,7 @@ def generate_class_file(
138158
)
139159

140160
class_string = generate_class_string(
141-
typename, props, description, namespace, prop_reorder_exceptions
161+
typename, props, description, namespace, prop_reorder_exceptions, max_props
142162
)
143163
file_name = f"{typename:s}.py"
144164

dash/development/base_component.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def __init__(self, **kwargs):
138138
+ " detected a Component for a prop other than `children`\n"
139139
+ f"Prop {k} has value {v!r}\n\n"
140140
+ "Did you forget to wrap multiple `children` in an array?\n"
141-
+ "For example, it must be html.Div([\"a\", \"b\", \"c\"]) not html.Div(\"a\", \"b\", \"c\")\n"
141+
+ 'For example, it must be html.Div(["a", "b", "c"]) not html.Div("a", "b", "c")\n'
142142
)
143143

144144
if k == "id":

dash/development/component_generator.py

+24-10
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class _CombinedFormatter(
3535
pass
3636

3737

38-
# pylint: disable=too-many-locals, too-many-arguments, too-many-branches
38+
# pylint: disable=too-many-locals, too-many-arguments, too-many-branches, too-many-statements
3939
def generate_components(
4040
components_source,
4141
project_shortname,
@@ -48,6 +48,7 @@ def generate_components(
4848
jlprefix=None,
4949
metadata=None,
5050
keep_prop_order=None,
51+
max_props=None,
5152
):
5253

5354
project_shortname = project_shortname.replace("-", "_").rstrip("/\\")
@@ -97,7 +98,17 @@ def generate_components(
9798

9899
metadata = safe_json_loads(out.decode("utf-8"))
99100

100-
generator_methods = [generate_class_file]
101+
py_generator_kwargs = {}
102+
if keep_prop_order is not None:
103+
keep_prop_order = [
104+
component.strip(" ") for component in keep_prop_order.split(",")
105+
]
106+
py_generator_kwargs["prop_reorder_exceptions"] = keep_prop_order
107+
108+
if max_props:
109+
py_generator_kwargs["max_props"] = max_props
110+
111+
generator_methods = [functools.partial(generate_class_file, **py_generator_kwargs)]
101112

102113
if rprefix is not None or jlprefix is not None:
103114
with open("package.json", "r") as f:
@@ -122,14 +133,6 @@ def generate_components(
122133
functools.partial(generate_struct_file, prefix=jlprefix)
123134
)
124135

125-
if keep_prop_order is not None:
126-
keep_prop_order = [
127-
component.strip(" ") for component in keep_prop_order.split(",")
128-
]
129-
generator_methods[0] = functools.partial(
130-
generate_class_file, prop_reorder_exceptions=keep_prop_order
131-
)
132-
133136
components = generate_classes_files(project_shortname, metadata, *generator_methods)
134137

135138
with open(os.path.join(project_shortname, "metadata.json"), "w") as f:
@@ -221,6 +224,16 @@ def component_build_arg_parser():
221224
"props. Pass the 'ALL' keyword to have every component retain "
222225
"its original prop order.",
223226
)
227+
parser.add_argument(
228+
"--max-props",
229+
type=int,
230+
default=250,
231+
help="Specify the max number of props to list in the component signature. "
232+
"More props will still be shown in the docstring, and will still work when "
233+
"provided as kwargs to the component. Python <3.7 only supports 255 args, "
234+
"but you may also want to reduce further for improved readability at the "
235+
"expense of auto-completion for the later props. Use 0 to include all props.",
236+
)
224237
return parser
225238

226239

@@ -237,6 +250,7 @@ def cli():
237250
rsuggests=args.r_suggests,
238251
jlprefix=args.jl_prefix,
239252
keep_prop_order=args.keep_prop_order,
253+
max_props=args.max_props,
240254
)
241255

242256

tests/integration/test_generation.py

+22
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import pytest
2+
13
from dash import Dash, Input, Output
24
from dash.exceptions import PreventUpdate
35

@@ -19,6 +21,9 @@ def test_gene001_simple_callback(dash_duo):
1921

2022
app.layout = Div(
2123
[
24+
# Note: `value` is not part of the explicit function signature
25+
# for MyStandardComponent, due to --max-props 2 in its build command
26+
# but this verifies that it still works.
2227
MyStandardComponent(id="standard", value="Standard"),
2328
MyNestedComponent(id="nested", value="Nested"),
2429
TypeScriptComponent(id="typescript", required_string="TypeScript"),
@@ -72,3 +77,20 @@ def update_container(n_clicks):
7277
)
7378

7479
dash_duo.percy_snapshot(name="gene002-arbitrary-resource")
80+
81+
82+
def test_gene003_max_props():
83+
limited_props_doc = "large number of props for this component"
84+
# dash_generator_test_component_standard has max_props set to 2, so
85+
# MyStandardComponent gets the restricted signature and note about it
86+
# in its docstring.
87+
# dash_generator_test_component_nested and MyNestedComponent do not.
88+
assert limited_props_doc in MyStandardComponent.__doc__
89+
assert limited_props_doc not in MyNestedComponent.__doc__
90+
91+
# Verify that it still works to check for invalid props in both cases
92+
with pytest.raises(TypeError):
93+
MyStandardComponent(valuex="nope")
94+
95+
with pytest.raises(TypeError):
96+
MyNestedComponent(valuey="nor this")

tests/unit/development/metadata_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def __init__(self, children=None, optionalArray=Component.UNDEFINED, optionalBoo
9696
self.available_wildcard_properties = ['data-', 'aria-']
9797
_explicit_args = kwargs.pop('_explicit_args')
9898
_locals = locals()
99-
_locals.update(kwargs) # For wildcard attrs
99+
_locals.update(kwargs) # For wildcard attrs and excess named props
100100
args = {k: _locals[k] for k in _explicit_args if k != 'children'}
101101
for k in []:
102102
if k not in args:

0 commit comments

Comments
 (0)