diff --git a/@plotly/dash-generator-test-component-typescript/dash_prop_typing.py b/@plotly/dash-generator-test-component-typescript/dash_prop_typing.py new file mode 100644 index 0000000000..db903c1482 --- /dev/null +++ b/@plotly/dash-generator-test-component-typescript/dash_prop_typing.py @@ -0,0 +1 @@ +ignore_props = ['ignored_prop'] diff --git a/@plotly/dash-generator-test-component-typescript/generator.test.ts b/@plotly/dash-generator-test-component-typescript/generator.test.ts index b8c390a574..266f343176 100644 --- a/@plotly/dash-generator-test-component-typescript/generator.test.ts +++ b/@plotly/dash-generator-test-component-typescript/generator.test.ts @@ -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'); + } ) }); diff --git a/@plotly/dash-generator-test-component-typescript/src/props.ts b/@plotly/dash-generator-test-component-typescript/src/props.ts index bbdc6b4773..56e16fa6b9 100644 --- a/@plotly/dash-generator-test-component-typescript/src/props.ts +++ b/@plotly/dash-generator-test-component-typescript/src/props.ts @@ -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 = { diff --git a/dash/development/_generate_prop_types.py b/dash/development/_generate_prop_types.py index 7990fdc0c0..92477ea0c5 100644 --- a/dash/development/_generate_prop_types.py +++ b/dash/development/_generate_prop_types.py @@ -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") @@ -25,27 +28,25 @@ 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): @@ -53,30 +54,34 @@ def generate_shape(prop_info): 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 = { @@ -97,6 +102,7 @@ def generate_tuple(*_): "enum": generate_enum, "objectOf": generate_object_of, "tuple": generate_tuple, + "literal": generate_literal, } @@ -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] @@ -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) + "}", ) ) @@ -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) ) ) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index 26cd796bb2..c18db36780 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -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, @@ -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 @@ -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" @@ -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" @@ -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 ---------- @@ -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 ( @@ -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'} @@ -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 diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index 7c93d555a1..fcd4c58961 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -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: @@ -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, @@ -200,4 +197,5 @@ def generator(*_): "enum": generate_enum, "objectOf": generate_object_of, "tuple": generate_tuple, + "literal": generate_literal, } diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index c7f407aeb8..250fe9c0be 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -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" diff --git a/dash/extract-meta.js b/dash/extract-meta.js index 7a2acaef51..3427ef81d3 100755 --- a/dash/extract-meta.js +++ b/dash/extract-meta.js @@ -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('|')})\\[\\]`); @@ -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';