Skip to content

Commit 52b698a

Browse files
authored
Merge pull request #3220 from plotly/improve-generation
Improve prop typing generation
2 parents e0a2e16 + ec75aca commit 52b698a

File tree

8 files changed

+96
-45
lines changed

8 files changed

+96
-45
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ignore_props = ['ignored_prop']

@plotly/dash-generator-test-component-typescript/generator.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,23 @@ describe('Test Typescript component metadata generation', () => {
271271
);
272272
expect(objectOfComponents).toBe("node");
273273
}
274+
);
275+
276+
test(
277+
'union and literal values', () => {
278+
const propType = R.path(
279+
propPath('TypeScriptComponent', 'union_enum').concat(
280+
'type'
281+
),
282+
metadata
283+
);
284+
expect(propType.name).toBe('union');
285+
expect(propType.value.length).toBe(3);
286+
expect(propType.value[0].name).toBe('number');
287+
expect(propType.value[1].name).toBe('literal');
288+
expect(propType.value[2].name).toBe('literal');
289+
expect(propType.value[1].value).toBe('small');
290+
}
274291
)
275292
});
276293

@plotly/dash-generator-test-component-typescript/src/props.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export type TypescriptComponentProps = {
4747

4848
object_of_string?: {[k: string]: string};
4949
object_of_components?: {[k: string]: JSX.Element};
50+
ignored_prop?: {ignore: {me: string}};
51+
union_enum?: number | 'small' | 'large'
5052
};
5153

5254
export type WrappedHTMLProps = {

dash/development/_generate_prop_types.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
# Generate it instead with the provided metadata.json
33
# for them to be able to report invalid prop
44

5+
import json
56
import os
67
import re
78

9+
from dash.development._py_prop_typing import get_custom_ignore
10+
811

912
init_check_re = re.compile("proptypes.js")
1013

@@ -25,58 +28,60 @@
2528

2629
prop_type_file_template = """// AUTOGENERATED FILE - DO NOT EDIT
2730
28-
var PropTypes = window.PropTypes;
29-
31+
var pt = window.PropTypes;
32+
var pk = window['{package_name}'];
3033
3134
{components_prop_types}
3235
"""
3336

34-
component_prop_types_template = (
35-
"window['{package_name}'].{component_name}.propTypes = {prop_types}"
36-
)
37+
component_prop_types_template = "pk.{component_name}.propTypes = {prop_types};"
3738

3839

3940
def generate_type(type_name):
4041
def wrap(*_):
41-
return f"PropTypes.{type_name}"
42+
return f"pt.{type_name}"
4243

4344
return wrap
4445

4546

4647
def generate_union(prop_info):
4748
types = [generate_prop_type(t) for t in prop_info["value"]]
48-
return f"PropTypes.oneOfType([{','.join(types)}])"
49+
return f"pt.oneOfType([{','.join(types)}])"
4950

5051

5152
def generate_shape(prop_info):
5253
props = []
5354
for key, value in prop_info["value"].items():
5455
props.append(f"{key}:{generate_prop_type(value)}")
5556
inner = "{" + ",".join(props) + "}"
56-
return f"PropTypes.shape({inner})"
57+
return f"pt.shape({inner})"
5758

5859

5960
def generate_array_of(prop_info):
6061
inner_type = generate_prop_type(prop_info["value"])
61-
return f"PropTypes.arrayOf({inner_type})"
62+
return f"pt.arrayOf({inner_type})"
6263

6364

6465
def generate_any(*_):
65-
return "PropTypes.any"
66+
return "pt.any"
6667

6768

6869
def generate_enum(prop_info):
6970
values = str([v["value"] for v in prop_info["value"]])
70-
return f"PropTypes.oneOf({values})"
71+
return f"pt.oneOf({values})"
7172

7273

7374
def generate_object_of(prop_info):
74-
return f"PropTypes.objectOf({generate_prop_type(prop_info['value'])})"
75+
return f"pt.objectOf({generate_prop_type(prop_info['value'])})"
7576

7677

7778
def generate_tuple(*_):
7879
# PropTypes don't have a tuple... just generate an array.
79-
return "PropTypes.array"
80+
return "pt.array"
81+
82+
83+
def generate_literal(prop_info):
84+
return f"pt.oneOf([{json.dumps(prop_info['value'])}])"
8085

8186

8287
prop_types = {
@@ -97,6 +102,7 @@ def generate_tuple(*_):
97102
"enum": generate_enum,
98103
"objectOf": generate_object_of,
99104
"tuple": generate_tuple,
105+
"literal": generate_literal,
100106
}
101107

102108

@@ -122,9 +128,12 @@ def check_init(namespace):
122128
def generate_prop_types(
123129
metadata,
124130
package_name,
131+
custom_typing_module,
125132
):
126133
patched = []
127134

135+
custom_ignore = get_custom_ignore(custom_typing_module)
136+
128137
for component_path, data in metadata.items():
129138
filename = component_path.split("/")[-1]
130139
extension = filename.split("/")[-1].split(".")[-1]
@@ -135,13 +144,17 @@ def generate_prop_types(
135144

136145
props = []
137146
for prop_name, prop_data in data.get("props", {}).items():
138-
props.append(f" {prop_name}:{generate_prop_type(prop_data['type'])}")
147+
if prop_name in custom_ignore:
148+
prop_type = "pt.any"
149+
else:
150+
prop_type = generate_prop_type(prop_data["type"])
151+
props.append(f"{prop_name}:{prop_type}")
139152

140153
patched.append(
141154
component_prop_types_template.format(
142155
package_name=package_name,
143156
component_name=component_name,
144-
prop_types="{" + ",\n".join(props) + "}",
157+
prop_types="{" + ",\n ".join(props) + "}",
145158
)
146159
)
147160

@@ -151,7 +164,7 @@ def generate_prop_types(
151164
) as f:
152165
f.write(
153166
prop_type_file_template.format(
154-
components_prop_types="\n\n".join(patched)
167+
package_name=package_name, components_prop_types="\n".join(patched)
155168
)
156169
)
157170

dash/development/_py_components_generation.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from ._all_keywords import python_keywords
1212
from ._collect_nodes import collect_nodes, filter_base_nodes
1313
from ._py_prop_typing import (
14+
get_custom_ignore,
1415
get_custom_props,
1516
get_prop_typing,
1617
shapes,
@@ -106,11 +107,13 @@ def __init__(
106107
)
107108
wildcard_prefixes = repr(parse_wildcards(props))
108109
list_of_valid_keys = repr(list(map(str, filtered_props.keys())))
110+
custom_ignore = get_custom_ignore(custom_typing_module)
109111
docstring = create_docstring(
110112
component_name=typename,
111113
props=filtered_props,
112114
description=description,
113115
prop_reorder_exceptions=prop_reorder_exceptions,
116+
ignored_props=custom_ignore,
114117
).replace("\r\n", "\n")
115118
required_args = required_props(filtered_props)
116119
is_children_required = "children" in required_args
@@ -175,6 +178,7 @@ def __init__(
175178
prop_key,
176179
type_info,
177180
custom_props=custom_props,
181+
custom_ignore=custom_ignore,
178182
)
179183

180184
arg_value = f"{prop_key}: typing.Optional[{typed}] = None"
@@ -246,7 +250,9 @@ def generate_class_file(
246250
custom_typing_module,
247251
)
248252

249-
custom_imp = get_custom_imports(custom_typing_module).get(typename)
253+
custom_imp = get_custom_imports(custom_typing_module)
254+
custom_imp = custom_imp.get(typename) or custom_imp.get("*")
255+
250256
if custom_imp:
251257
imports += "\n".join(custom_imp)
252258
imports += "\n\n"
@@ -334,7 +340,13 @@ def required_props(props):
334340
return [prop_name for prop_name, prop in list(props.items()) if prop["required"]]
335341

336342

337-
def create_docstring(component_name, props, description, prop_reorder_exceptions=None):
343+
def create_docstring(
344+
component_name,
345+
props,
346+
description,
347+
prop_reorder_exceptions=None,
348+
ignored_props=tuple(),
349+
):
338350
"""Create the Dash component docstring.
339351
Parameters
340352
----------
@@ -371,7 +383,7 @@ def create_docstring(component_name, props, description, prop_reorder_exceptions
371383
indent_num=0,
372384
is_flow_type="flowType" in prop and "type" not in prop,
373385
)
374-
for p, prop in filter_props(props).items()
386+
for p, prop in filter_props(props, ignored_props).items()
375387
)
376388

377389
return (
@@ -436,7 +448,7 @@ def reorder_props(props):
436448
return OrderedDict(props1 + props2 + sorted(list(props.items())))
437449

438450

439-
def filter_props(props):
451+
def filter_props(props, ignored_props=tuple()):
440452
"""Filter props from the Component arguments to exclude:
441453
- Those without a "type" or a "flowType" field
442454
- Those with arg.type.name in {'func', 'symbol', 'instanceOf'}
@@ -481,7 +493,7 @@ def filter_props(props):
481493
filtered_props = copy.deepcopy(props)
482494

483495
for arg_name, arg in list(filtered_props.items()):
484-
if "type" not in arg and "flowType" not in arg:
496+
if arg_name in ignored_props or ("type" not in arg and "flowType" not in arg):
485497
filtered_props.pop(arg_name)
486498
continue
487499

dash/development/_py_prop_typing.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ def get_custom_props(module_name):
3434
return _get_custom(module_name, "custom_props", {})
3535

3636

37+
def get_custom_ignore(module_name):
38+
return _get_custom(module_name, "ignore_props", ["style"])
39+
40+
3741
def _clean_key(key):
3842
k = ""
3943
for ch in key:
@@ -136,48 +140,41 @@ def generate_enum(type_info, *_):
136140
return f"Literal[{', '.join(values)}]"
137141

138142

143+
def generate_literal(type_info, *_):
144+
return f"Literal[{json.dumps(type_info['value'])}]"
145+
146+
147+
def _get_custom_prop(custom_props, component_name, prop_name):
148+
customs = custom_props.get(component_name) or custom_props.get("*", {})
149+
return customs.get(prop_name)
150+
151+
139152
def get_prop_typing(
140153
type_name: str,
141154
component_name: str,
142155
prop_name: str,
143156
type_info,
144157
custom_props=None,
158+
custom_ignore=None,
145159
):
146160
if prop_name == "id":
147161
# Id is always the same either a string or a dict for pattern matching.
148162
return "typing.Union[str, dict]"
149163

150164
if custom_props:
151-
special = custom_props.get(component_name, {}).get(prop_name)
165+
special = _get_custom_prop(custom_props, component_name, prop_name)
152166
if special:
153167
return special(type_info, component_name, prop_name)
154168

169+
if custom_ignore and prop_name in custom_ignore:
170+
return "typing.Any"
171+
155172
prop_type = PROP_TYPING.get(type_name, generate_any)(
156173
type_info, component_name, prop_name
157174
)
158175
return prop_type
159176

160177

161-
def generate_plotly_figure(*_):
162-
custom_imports["dash_core_components"]["Graph"].append(
163-
"from plotly.graph_objects import Figure"
164-
)
165-
return "typing.Union[Figure, dict]"
166-
167-
168-
def generate_datetime_prop(component, array=False):
169-
if "import datetime" not in custom_imports["dash_core_components"][component]:
170-
custom_imports["dash_core_components"][component].append("import datetime")
171-
172-
def generator(*_):
173-
datetime_type = "typing.Union[str, datetime.datetime]"
174-
if array:
175-
datetime_type = f"typing.Sequence[{datetime_type}]"
176-
return datetime_type
177-
178-
return generator
179-
180-
181178
PROP_TYPING = {
182179
"array": generate_type("typing.Sequence"),
183180
"arrayOf": generate_array_of,
@@ -200,4 +197,5 @@ def generator(*_):
200197
"enum": generate_enum,
201198
"objectOf": generate_object_of,
202199
"tuple": generate_tuple,
200+
"literal": generate_literal,
203201
}

dash/development/component_generator.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ def generate_components(
139139

140140
components = generate_classes_files(project_shortname, metadata, *generator_methods)
141141

142-
generate_prop_types(metadata, project_shortname)
142+
generate_prop_types(
143+
metadata,
144+
project_shortname,
145+
custom_typing_module=custom_typing_module,
146+
)
143147

144148
with open(
145149
os.path.join(project_shortname, "metadata.json"), "w", encoding="utf-8"

dash/extract-meta.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ const BANNED_TYPES = [
6767
'ChildNode',
6868
'ParentNode',
6969
];
70-
const unionSupport = PRIMITIVES.concat('boolean', 'Element');
70+
const unionSupport = PRIMITIVES.concat('boolean', 'Element', 'enum');
7171

7272
const reArray = new RegExp(`(${unionSupport.join('|')})\\[\\]`);
7373

@@ -261,12 +261,16 @@ function gatherComponents(sources, components = {}) {
261261
typeName = 'object';
262262
}
263263
}
264+
if (t.value) {
265+
// A literal value
266+
return true;
267+
}
264268
return (
265269
unionSupport.includes(typeName) ||
266270
isArray(checker.typeToString(t))
267271
);
268272
})
269-
.map(t => getPropType(t, propObj, parentType));
273+
.map(t => t.value ? {name: 'literal', value: t.value} : getPropType(t, propObj, parentType));
270274

271275
if (!value.length) {
272276
name = 'any';

0 commit comments

Comments
 (0)