Skip to content

Improve prop typing generation #3220

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ignore_props = ['ignored_prop']
17 changes: 17 additions & 0 deletions @plotly/dash-generator-test-component-typescript/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,23 @@ describe('Test Typescript component metadata generation', () => {
);
expect(objectOfComponents).toBe("node");
}
);

test(
'union and literal values', () => {
const propType = R.path(
propPath('TypeScriptComponent', 'union_enum').concat(
'type'
),
metadata
);
expect(propType.name).toBe('union');
expect(propType.value.length).toBe(3);
expect(propType.value[0].name).toBe('number');
expect(propType.value[1].name).toBe('literal');
expect(propType.value[2].name).toBe('literal');
expect(propType.value[1].value).toBe('small');
}
)
});

Expand Down
2 changes: 2 additions & 0 deletions @plotly/dash-generator-test-component-typescript/src/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export type TypescriptComponentProps = {

object_of_string?: {[k: string]: string};
object_of_components?: {[k: string]: JSX.Element};
ignored_prop?: {ignore: {me: string}};
union_enum?: number | 'small' | 'large'
};

export type WrappedHTMLProps = {
Expand Down
45 changes: 29 additions & 16 deletions dash/development/_generate_prop_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
# Generate it instead with the provided metadata.json
# for them to be able to report invalid prop

import json
import os
import re

from dash.development._py_prop_typing import get_custom_ignore


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

Expand All @@ -25,58 +28,60 @@

prop_type_file_template = """// AUTOGENERATED FILE - DO NOT EDIT

var PropTypes = window.PropTypes;

var pt = window.PropTypes;
var pk = window['{package_name}'];

{components_prop_types}
"""

component_prop_types_template = (
"window['{package_name}'].{component_name}.propTypes = {prop_types}"
)
component_prop_types_template = "pk.{component_name}.propTypes = {prop_types};"


def generate_type(type_name):
def wrap(*_):
return f"PropTypes.{type_name}"
return f"pt.{type_name}"

return wrap


def generate_union(prop_info):
types = [generate_prop_type(t) for t in prop_info["value"]]
return f"PropTypes.oneOfType([{','.join(types)}])"
return f"pt.oneOfType([{','.join(types)}])"


def generate_shape(prop_info):
props = []
for key, value in prop_info["value"].items():
props.append(f"{key}:{generate_prop_type(value)}")
inner = "{" + ",".join(props) + "}"
return f"PropTypes.shape({inner})"
return f"pt.shape({inner})"


def generate_array_of(prop_info):
inner_type = generate_prop_type(prop_info["value"])
return f"PropTypes.arrayOf({inner_type})"
return f"pt.arrayOf({inner_type})"


def generate_any(*_):
return "PropTypes.any"
return "pt.any"


def generate_enum(prop_info):
values = str([v["value"] for v in prop_info["value"]])
return f"PropTypes.oneOf({values})"
return f"pt.oneOf({values})"


def generate_object_of(prop_info):
return f"PropTypes.objectOf({generate_prop_type(prop_info['value'])})"
return f"pt.objectOf({generate_prop_type(prop_info['value'])})"


def generate_tuple(*_):
# PropTypes don't have a tuple... just generate an array.
return "PropTypes.array"
return "pt.array"


def generate_literal(prop_info):
return f"pt.oneOf([{json.dumps(prop_info['value'])}])"


prop_types = {
Expand All @@ -97,6 +102,7 @@ def generate_tuple(*_):
"enum": generate_enum,
"objectOf": generate_object_of,
"tuple": generate_tuple,
"literal": generate_literal,
}


Expand All @@ -122,9 +128,12 @@ def check_init(namespace):
def generate_prop_types(
metadata,
package_name,
custom_typing_module,
):
patched = []

custom_ignore = get_custom_ignore(custom_typing_module)

for component_path, data in metadata.items():
filename = component_path.split("/")[-1]
extension = filename.split("/")[-1].split(".")[-1]
Expand All @@ -135,13 +144,17 @@ def generate_prop_types(

props = []
for prop_name, prop_data in data.get("props", {}).items():
props.append(f" {prop_name}:{generate_prop_type(prop_data['type'])}")
if prop_name in custom_ignore:
prop_type = "pt.any"
else:
prop_type = generate_prop_type(prop_data["type"])
props.append(f"{prop_name}:{prop_type}")

patched.append(
component_prop_types_template.format(
package_name=package_name,
component_name=component_name,
prop_types="{" + ",\n".join(props) + "}",
prop_types="{" + ",\n ".join(props) + "}",
)
)

Expand All @@ -151,7 +164,7 @@ def generate_prop_types(
) as f:
f.write(
prop_type_file_template.format(
components_prop_types="\n\n".join(patched)
package_name=package_name, components_prop_types="\n".join(patched)
)
)

Expand Down
22 changes: 17 additions & 5 deletions dash/development/_py_components_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ._all_keywords import python_keywords
from ._collect_nodes import collect_nodes, filter_base_nodes
from ._py_prop_typing import (
get_custom_ignore,
get_custom_props,
get_prop_typing,
shapes,
Expand Down Expand Up @@ -106,11 +107,13 @@ def __init__(
)
wildcard_prefixes = repr(parse_wildcards(props))
list_of_valid_keys = repr(list(map(str, filtered_props.keys())))
custom_ignore = get_custom_ignore(custom_typing_module)
docstring = create_docstring(
component_name=typename,
props=filtered_props,
description=description,
prop_reorder_exceptions=prop_reorder_exceptions,
ignored_props=custom_ignore,
).replace("\r\n", "\n")
required_args = required_props(filtered_props)
is_children_required = "children" in required_args
Expand Down Expand Up @@ -175,6 +178,7 @@ def __init__(
prop_key,
type_info,
custom_props=custom_props,
custom_ignore=custom_ignore,
)

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

custom_imp = get_custom_imports(custom_typing_module).get(typename)
custom_imp = get_custom_imports(custom_typing_module)
custom_imp = custom_imp.get(typename) or custom_imp.get("*")

if custom_imp:
imports += "\n".join(custom_imp)
imports += "\n\n"
Expand Down Expand Up @@ -334,7 +340,13 @@ def required_props(props):
return [prop_name for prop_name, prop in list(props.items()) if prop["required"]]


def create_docstring(component_name, props, description, prop_reorder_exceptions=None):
def create_docstring(
component_name,
props,
description,
prop_reorder_exceptions=None,
ignored_props=tuple(),
):
"""Create the Dash component docstring.
Parameters
----------
Expand Down Expand Up @@ -371,7 +383,7 @@ def create_docstring(component_name, props, description, prop_reorder_exceptions
indent_num=0,
is_flow_type="flowType" in prop and "type" not in prop,
)
for p, prop in filter_props(props).items()
for p, prop in filter_props(props, ignored_props).items()
)

return (
Expand Down Expand Up @@ -436,7 +448,7 @@ def reorder_props(props):
return OrderedDict(props1 + props2 + sorted(list(props.items())))


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

for arg_name, arg in list(filtered_props.items()):
if "type" not in arg and "flowType" not in arg:
if arg_name in ignored_props or ("type" not in arg and "flowType" not in arg):
filtered_props.pop(arg_name)
continue

Expand Down
40 changes: 19 additions & 21 deletions dash/development/_py_prop_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ def get_custom_props(module_name):
return _get_custom(module_name, "custom_props", {})


def get_custom_ignore(module_name):
return _get_custom(module_name, "ignore_props", ["style"])


def _clean_key(key):
k = ""
for ch in key:
Expand Down Expand Up @@ -136,48 +140,41 @@ def generate_enum(type_info, *_):
return f"Literal[{', '.join(values)}]"


def generate_literal(type_info, *_):
return f"Literal[{json.dumps(type_info['value'])}]"


def _get_custom_prop(custom_props, component_name, prop_name):
customs = custom_props.get(component_name) or custom_props.get("*", {})
return customs.get(prop_name)


def get_prop_typing(
type_name: str,
component_name: str,
prop_name: str,
type_info,
custom_props=None,
custom_ignore=None,
):
if prop_name == "id":
# Id is always the same either a string or a dict for pattern matching.
return "typing.Union[str, dict]"

if custom_props:
special = custom_props.get(component_name, {}).get(prop_name)
special = _get_custom_prop(custom_props, component_name, prop_name)
if special:
return special(type_info, component_name, prop_name)

if custom_ignore and prop_name in custom_ignore:
return "typing.Any"

prop_type = PROP_TYPING.get(type_name, generate_any)(
type_info, component_name, prop_name
)
return prop_type


def generate_plotly_figure(*_):
custom_imports["dash_core_components"]["Graph"].append(
"from plotly.graph_objects import Figure"
)
return "typing.Union[Figure, dict]"


def generate_datetime_prop(component, array=False):
if "import datetime" not in custom_imports["dash_core_components"][component]:
custom_imports["dash_core_components"][component].append("import datetime")

def generator(*_):
datetime_type = "typing.Union[str, datetime.datetime]"
if array:
datetime_type = f"typing.Sequence[{datetime_type}]"
return datetime_type

return generator


PROP_TYPING = {
"array": generate_type("typing.Sequence"),
"arrayOf": generate_array_of,
Expand All @@ -200,4 +197,5 @@ def generator(*_):
"enum": generate_enum,
"objectOf": generate_object_of,
"tuple": generate_tuple,
"literal": generate_literal,
}
6 changes: 5 additions & 1 deletion dash/development/component_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,11 @@ def generate_components(

components = generate_classes_files(project_shortname, metadata, *generator_methods)

generate_prop_types(metadata, project_shortname)
generate_prop_types(
metadata,
project_shortname,
custom_typing_module=custom_typing_module,
)

with open(
os.path.join(project_shortname, "metadata.json"), "w", encoding="utf-8"
Expand Down
8 changes: 6 additions & 2 deletions dash/extract-meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const BANNED_TYPES = [
'ChildNode',
'ParentNode',
];
const unionSupport = PRIMITIVES.concat('boolean', 'Element');
const unionSupport = PRIMITIVES.concat('boolean', 'Element', 'enum');

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

Expand Down Expand Up @@ -261,12 +261,16 @@ function gatherComponents(sources, components = {}) {
typeName = 'object';
}
}
if (t.value) {
// A literal value
return true;
}
return (
unionSupport.includes(typeName) ||
isArray(checker.typeToString(t))
);
})
.map(t => getPropType(t, propObj, parentType));
.map(t => t.value ? {name: 'literal', value: t.value} : getPropType(t, propObj, parentType));

if (!value.length) {
name = 'any';
Expand Down