-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Add typing to component init #2276
Changes from 52 commits
5323742
8626b3d
c9bb168
91cc0d6
6bbafdc
c620f58
147798b
a5250e5
6d50f15
dee40cf
24a8e90
1f4aed9
88ca3f3
c18f0fd
544dcab
91a2a1d
ed360e1
b3061a4
a4ea22e
8029f15
b0ed806
ae345e0
d7136e3
a3dc517
a4f2c4a
12c5319
8e0dfea
453834b
13ff296
279950c
e6843fd
6dc0fc1
709d1e3
e6c2356
7bc56b9
9eee253
2adb37f
f228185
7b0da8a
fb8decb
197180c
8734e1b
d5b0ebe
4e583e3
150c2a5
96b931a
4045a61
46d05be
85c26b7
40bd21a
2de448e
648a1c6
8d185c8
ef6b054
6b30d08
dd5cb44
26a83e5
18fad2d
95d8b0d
00da064
d13b343
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,20 @@ | ||
from collections import OrderedDict | ||
import copy | ||
import numbers | ||
import os | ||
import typing | ||
from textwrap import fill, dedent | ||
|
||
from typing_extensions import TypedDict, NotRequired, Literal | ||
from dash.development.base_component import _explicitize_args | ||
from dash.exceptions import NonExistentEventException | ||
from ._all_keywords import python_keywords | ||
from ._collect_nodes import collect_nodes, filter_base_nodes | ||
from .base_component import Component | ||
from ._py_prop_typing import get_prop_typing, shapes, custom_imports | ||
from .base_component import Component, ComponentType | ||
|
||
|
||
# pylint: disable=unused-argument,too-many-locals | ||
# pylint: disable=unused-argument,too-many-locals,too-many-branches | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. :-) |
||
def generate_class_string( | ||
T4rk1n marked this conversation as resolved.
Show resolved
Hide resolved
|
||
typename, | ||
props, | ||
|
@@ -54,8 +58,12 @@ def generate_class_string( | |
_base_nodes = {base_nodes} | ||
_namespace = '{namespace}' | ||
_type = '{typename}' | ||
{shapes} | ||
@_explicitize_args | ||
def __init__(self, {default_argtext}): | ||
def __init__( | ||
self, | ||
{default_argtext} | ||
): | ||
gvwilson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self._prop_names = {list_of_valid_keys} | ||
self._valid_wildcard_attributes =\ | ||
{list_of_valid_wildcard_attr_prefixes} | ||
|
@@ -94,7 +102,9 @@ def __init__(self, {default_argtext}): | |
prop_keys = list(props.keys()) | ||
if "children" in props and "children" in list_of_valid_keys: | ||
prop_keys.remove("children") | ||
default_argtext = "children=None, " | ||
# TODO For dash 3.0, remove the Optional and = None for proper typing. | ||
# Also add the other required props after children. | ||
default_argtext = f"children: typing.Optional[{get_prop_typing('node', '', '', {})}] = None,\n " | ||
args = "{k: _locals[k] for k in _explicit_args if k != 'children'}" | ||
argtext = "children=children, **args" | ||
else: | ||
|
@@ -118,15 +128,31 @@ def __init__(self, {default_argtext}): | |
raise TypeError('Required argument children was not specified.') | ||
""" | ||
|
||
default_arglist = [ | ||
( | ||
f"{p:s}=Component.REQUIRED" | ||
if props[p]["required"] | ||
else f"{p:s}=Component.UNDEFINED" | ||
) | ||
for p in prop_keys | ||
if not p.endswith("-*") and p not in python_keywords and p != "setProps" | ||
] | ||
default_arglist = [] | ||
|
||
for prop_key in prop_keys: | ||
prop = props[prop_key] | ||
if ( | ||
prop_key.endswith("-*") | ||
or prop_key in python_keywords | ||
or prop_key == "setProps" | ||
): | ||
continue | ||
|
||
type_info = prop.get("type") | ||
|
||
if not type_info: | ||
print(f"Invalid prop type for typing: {prop_key}") | ||
default_arglist.append(f"{prop_key} = None") | ||
continue | ||
|
||
type_name = type_info.get("name") | ||
|
||
typed = get_prop_typing(type_name, typename, prop_key, type_info, namespace) | ||
|
||
arg_value = f"{prop_key}: typing.Optional[{typed}] = None" | ||
|
||
default_arglist.append(arg_value) | ||
|
||
if max_props: | ||
final_max_props = max_props - (1 if "children" in props else 0) | ||
|
@@ -139,7 +165,7 @@ def __init__(self, {default_argtext}): | |
"they may still be used as keyword arguments." | ||
) | ||
|
||
default_argtext += ", ".join(default_arglist + ["**kwargs"]) | ||
default_argtext += ",\n ".join(default_arglist + ["**kwargs"]) | ||
nodes = collect_nodes({k: v for k, v in props.items() if k != "children"}) | ||
|
||
return dedent( | ||
|
@@ -156,6 +182,7 @@ def __init__(self, {default_argtext}): | |
required_validation=required_validation, | ||
children_props=nodes, | ||
base_nodes=filter_base_nodes(nodes) + ["children"], | ||
shapes="\n".join(shapes.get(typename, {}).values()), | ||
) | ||
) | ||
|
||
|
@@ -181,13 +208,23 @@ def generate_class_file( | |
""" | ||
import_string = ( | ||
"# AUTO GENERATED FILE - DO NOT EDIT\n\n" | ||
+ "from dash.development.base_component import " | ||
+ "Component, _explicitize_args\n\n\n" | ||
"import typing # noqa: F401\n" | ||
"import numbers # noqa: F401\n" | ||
"from typing_extensions import TypedDict, NotRequired, Literal # noqa: F401\n" | ||
"from dash.development.base_component import ComponentType # noqa: F401\n" | ||
"from dash.development.base_component import " | ||
"Component, _explicitize_args\n\n\n" | ||
) | ||
|
||
class_string = generate_class_string( | ||
typename, props, description, namespace, prop_reorder_exceptions, max_props | ||
) | ||
|
||
custom_imp = custom_imports[namespace][typename] | ||
if custom_imp: | ||
import_string += "\n".join(custom_imp) | ||
import_string += "\n\n" | ||
|
||
file_name = f"{typename:s}.py" | ||
|
||
file_path = os.path.join(namespace, file_name) | ||
|
@@ -242,7 +279,16 @@ def generate_class( | |
string = generate_class_string( | ||
typename, props, description, namespace, prop_reorder_exceptions | ||
) | ||
scope = {"Component": Component, "_explicitize_args": _explicitize_args} | ||
scope = { | ||
"Component": Component, | ||
"ComponentType": ComponentType, | ||
"_explicitize_args": _explicitize_args, | ||
"typing": typing, | ||
"numbers": numbers, | ||
"TypedDict": TypedDict, | ||
"NotRequired": NotRequired, | ||
"Literal": Literal, | ||
} | ||
# pylint: disable=exec-used | ||
exec(string, scope) | ||
result = scope[typename] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import collections | ||
import json | ||
import string | ||
import textwrap | ||
|
||
import stringcase | ||
|
||
|
||
shapes = {} | ||
shape_template = """{name} = TypedDict( | ||
"{name}", | ||
{values} | ||
) | ||
""" | ||
custom_imports = collections.defaultdict(lambda: collections.defaultdict(list)) | ||
|
||
|
||
def _clean_key(key): | ||
k = "" | ||
for ch in key: | ||
if ch not in string.ascii_letters + "_": | ||
k += "_" | ||
else: | ||
k += ch | ||
return k | ||
|
||
|
||
def generate_any(*_): | ||
return "typing.Any" | ||
|
||
|
||
def generate_shape(type_info, component_name: str, prop_name: str): | ||
props = [] | ||
name = stringcase.pascalcase(prop_name) | ||
|
||
for prop_key, prop_type in type_info["value"].items(): | ||
typed = get_prop_typing( | ||
prop_type["name"], component_name, f"{prop_name}_{prop_key}", prop_type | ||
) | ||
if not prop_type.get("required"): | ||
props.append(f' "{prop_key}": NotRequired[{typed}]') | ||
else: | ||
props.append(f' "{prop_key}": {typed}') | ||
|
||
shapes.setdefault(component_name, {}) | ||
shapes[component_name][name] = textwrap.indent( | ||
shape_template.format( | ||
name=name, values=" {\n" + ",\n".join(props) + "\n }" | ||
), | ||
" ", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need to indent an empty string at this point? |
||
) | ||
|
||
return f'"{name}"' | ||
|
||
|
||
def generate_union(type_info, component_name: str, prop_name: str): | ||
types = [] | ||
for union in type_info["value"]: | ||
u_type = get_prop_typing(union["name"], component_name, prop_name, union) | ||
if u_type not in types: | ||
types.append(u_type) | ||
return f"typing.Union[{', '.join(types)}]" | ||
|
||
|
||
def generate_tuple( | ||
type_info, | ||
component_name: str, | ||
prop_name: str, | ||
): | ||
els = type_info.get("elements") | ||
elements = ", ".join( | ||
get_prop_typing(x.get("name"), component_name, prop_name, x) for x in els | ||
) | ||
return f"typing.Tuple[{elements}]" | ||
|
||
|
||
def generate_array_of( | ||
type_info, | ||
component_name: str, | ||
prop_name: str, | ||
): | ||
typed = get_prop_typing( | ||
type_info["value"]["name"], component_name, prop_name, type_info["value"] | ||
) | ||
return f"typing.Union[typing.Sequence[{typed}], typing.Tuple]" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, I changed from List but didn't remove the Tuple. |
||
|
||
|
||
def generate_object_of(type_info, component_name: str, prop_name: str): | ||
typed = get_prop_typing( | ||
type_info["value"]["name"], component_name, prop_name, type_info["value"] | ||
) | ||
return f"typing.Dict[typing.Union[str, float, int], {typed}]" | ||
|
||
|
||
def generate_type(typename): | ||
def type_handler(*_): | ||
return typename | ||
|
||
return type_handler | ||
|
||
|
||
def _get_literal_value(value): | ||
if value is None: | ||
return "None" | ||
|
||
if isinstance(value, bool): | ||
return str(value) | ||
|
||
return json.dumps(value) | ||
|
||
|
||
def generate_enum(type_info, *_): | ||
values = [ | ||
_get_literal_value(json.loads(v["value"].replace("'", '"'))) | ||
for v in type_info["value"] | ||
if v | ||
] | ||
return f"Literal[{', '.join(values)}]" | ||
|
||
|
||
def get_prop_typing( | ||
type_name: str, component_name: str, prop_name: str, type_info, namespace=None | ||
): | ||
if namespace: | ||
# Only check the namespace once | ||
special = ( | ||
special_cases.get(namespace, {}).get(component_name, {}).get(prop_name) | ||
) | ||
if special: | ||
return special(type_info, component_name, prop_name) | ||
|
||
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]" | ||
|
||
|
||
special_cases = {"dash_core_components": {"Graph": {"figure": generate_plotly_figure}}} | ||
|
||
|
||
PROP_TYPING = { | ||
"array": generate_type("typing.Union[typing.Sequence, typing.Tuple]"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, doesn't |
||
"arrayOf": generate_array_of, | ||
"object": generate_type("dict"), | ||
"shape": generate_shape, | ||
"exact": generate_shape, | ||
"string": generate_type("str"), | ||
"bool": generate_type("bool"), | ||
"number": generate_type("typing.Union[int, float, numbers.Number]"), | ||
"node": generate_type( | ||
"typing.Union[str, int, float, ComponentType," | ||
" typing.Sequence[typing.Union" | ||
"[str, int, float, ComponentType]]]" | ||
), | ||
"func": generate_any, | ||
"element": generate_type("ComponentType"), | ||
"union": generate_union, | ||
"any": generate_any, | ||
"custom": generate_any, | ||
"enum": generate_enum, | ||
"objectOf": generate_object_of, | ||
"tuple": generate_tuple, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
took me a moment to parse this :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's given as
[(Output('id', 'prop), "on", "off")]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can just stop at a higher level
Optional[List]
, and be les concrete, leave room for future modifications.