Skip to content

Commit af9745c

Browse files
committed
Limit generated components to 250 explicit args by default
configurable to any other number Keep this to 253 or below for Py3.6 support
1 parent 3860484 commit af9745c

File tree

6 files changed

+83
-27
lines changed

6 files changed

+83
-27
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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]>",

dash/development/_py_components_generation.py

Lines changed: 35 additions & 16 deletions
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,27 @@ 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\nNote: due to the large number of props for this component,"
115+
"\nnot all of them appear in the constructor signature, but"
116+
"\nthey may still be used as keyword arguments."
100117
)
101-
for p in prop_keys
102-
if not p.endswith("-*") and p not in python_keywords and p != "setProps"
103-
]
104-
+ ["**kwargs"]
105-
)
118+
119+
default_argtext += ", ".join(default_arglist + ["**kwargs"])
106120
required_args = required_props(filtered_props)
107121
return c.format(
108122
typename=typename,
@@ -118,7 +132,12 @@ def __init__(self, {default_argtext}):
118132

119133

120134
def generate_class_file(
121-
typename, props, description, namespace, prop_reorder_exceptions=None
135+
typename,
136+
props,
137+
description,
138+
namespace,
139+
prop_reorder_exceptions=None,
140+
max_props=None,
122141
):
123142
"""Generate a Python class file (.py) given a class string.
124143
Parameters
@@ -138,7 +157,7 @@ def generate_class_file(
138157
)
139158

140159
class_string = generate_class_string(
141-
typename, props, description, namespace, prop_reorder_exceptions
160+
typename, props, description, namespace, prop_reorder_exceptions, max_props
142161
)
143162
file_name = f"{typename:s}.py"
144163

dash/development/base_component.py

Lines changed: 1 addition & 1 deletion
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

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 22 additions & 0 deletions
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")

0 commit comments

Comments
 (0)