Skip to content

Commit

Permalink
Add typing to component init.
Browse files Browse the repository at this point in the history
  • Loading branch information
T4rk1n committed Oct 20, 2022
1 parent a9eb343 commit d029d79
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 18 deletions.
60 changes: 44 additions & 16 deletions dash/development/_py_components_generation.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
from collections import OrderedDict
import copy
import os
import typing
from textwrap import fill, dedent

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 ._py_prop_typing import get_prop_typing
from .base_component import Component


# pylint: disable=unused-argument,too-many-locals
# pylint: disable=unused-argument,too-many-locals,too-many-branches
def generate_class_string(
typename,
props,
Expand Down Expand Up @@ -55,7 +57,10 @@ def generate_class_string(
_namespace = '{namespace}'
_type = '{typename}'
@_explicitize_args
def __init__(self, {default_argtext}):
def __init__(
self,
{default_argtext}
):
self._prop_names = {list_of_valid_keys}
self._valid_wildcard_attributes =\
{list_of_valid_wildcard_attr_prefixes}
Expand Down Expand Up @@ -94,7 +99,7 @@ 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, "
default_argtext = "children=None,"
args = "{k: _locals[k] for k in _explicit_args if k != 'children'}"
argtext = "children=children, **args"
else:
Expand All @@ -118,15 +123,33 @@ 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
required = prop.get("required")
type_info = prop.get("type")

if not type_info:
print(f"Invalid prop type: {prop_key}")
continue

type_name = type_info.get("name")

typed = get_prop_typing(type_name, type_info)

if required:
arg_value = f"{prop_key}: {typed} = Component.REQUIRED"
else:
arg_value = f"{prop_key}: {typed} = Component.UNDEFINED"

default_arglist.append(arg_value)

if max_props:
final_max_props = max_props - (1 if "children" in props else 0)
Expand All @@ -139,7 +162,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(
Expand Down Expand Up @@ -181,8 +204,9 @@ 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"
"from dash.development.base_component import "
"Component, _explicitize_args\n\n\n"
)

class_string = generate_class_string(
Expand Down Expand Up @@ -242,7 +266,11 @@ def generate_class(
string = generate_class_string(
typename, props, description, namespace, prop_reorder_exceptions
)
scope = {"Component": Component, "_explicitize_args": _explicitize_args}
scope = {
"Component": Component,
"_explicitize_args": _explicitize_args,
"typing": typing,
}
# pylint: disable=exec-used
exec(string, scope)
result = scope[typename]
Expand Down
76 changes: 76 additions & 0 deletions dash/development/_py_prop_typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
def generate_any(_):
return "typing.Any"


def generate_shape(t):
props = []

for prop in t["value"].values():
prop_type = PROP_TYPING.get(prop["name"], generate_any)(prop)
if prop_type not in props:
props.append(prop_type)

if len(props) == 0:
return "typing.Any"

return f"typing.Dict[str, typing.Union[{', '.join(props)}]]"


def generate_union(t):
types = []
for union in t["value"]:
u_type = PROP_TYPING.get(union["name"], generate_any)(union)
if u_type not in types:
types.append(u_type)
return f"typing.Union[{', '.join(types)}]"


def generate_tuple(type_info):
els = type_info.get("elements")
elements = ", ".join(get_prop_typing(x.get("name"), x) for x in els)
return f"typing.Tuple[{elements}]"


def generate_array_of(t):
typed = PROP_TYPING.get(t["value"]["name"], generate_any)(t["value"])
return f"typing.List[{typed}]"


def generate_object_of(t):
typed = PROP_TYPING.get(t["value"]["name"], generate_any)(t["value"])
return f"typing.Dict[str, {typed}]"


def generate_type(typename):
def type_handler(_):
return typename

return type_handler


def get_prop_typing(type_name: str, type_info):
return PROP_TYPING.get(type_name, generate_any)(type_info)


PROP_TYPING = {
"array": generate_type("typing.List"),
"arrayOf": generate_array_of,
"object": generate_type("typing.Dict"),
"shape": generate_shape,
"string": generate_type("str"),
"bool": generate_type("bool"),
"number": generate_type("typing.Union[float, int]"),
"node": generate_type(
"typing.Union[str, int, float, Component,"
" typing.List[typing.Union"
"[str, int, float, Component]]]"
),
"func": generate_any,
"element": generate_type("Component"),
"union": generate_union,
"any": generate_any,
"custom": generate_any,
"enum": generate_any,
"objectOf": generate_object_of,
"tuple": generate_tuple,
}
26 changes: 25 additions & 1 deletion tests/unit/development/metadata_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# AUTO GENERATED FILE - DO NOT EDIT

import typing # noqa: F401
from dash.development.base_component import Component, _explicitize_args


Expand Down Expand Up @@ -91,7 +92,30 @@ class Table(Component):
_namespace = 'TableComponents'
_type = 'Table'
@_explicitize_args
def __init__(self, children=None, optionalArray=Component.UNDEFINED, optionalBool=Component.UNDEFINED, optionalFunc=Component.UNDEFINED, optionalNumber=Component.UNDEFINED, optionalObject=Component.UNDEFINED, optionalString=Component.UNDEFINED, optionalSymbol=Component.UNDEFINED, optionalNode=Component.UNDEFINED, optionalElement=Component.UNDEFINED, optionalMessage=Component.UNDEFINED, optionalEnum=Component.UNDEFINED, optionalUnion=Component.UNDEFINED, optionalArrayOf=Component.UNDEFINED, optionalObjectOf=Component.UNDEFINED, optionalObjectWithExactAndNestedDescription=Component.UNDEFINED, optionalObjectWithShapeAndNestedDescription=Component.UNDEFINED, optionalAny=Component.UNDEFINED, customProp=Component.UNDEFINED, customArrayProp=Component.UNDEFINED, id=Component.UNDEFINED, **kwargs):
def __init__(
self,
children=None,optionalArray: typing.List = Component.UNDEFINED,
optionalBool: bool = Component.UNDEFINED,
optionalFunc: typing.Any = Component.UNDEFINED,
optionalNumber: typing.Union[float, int] = Component.UNDEFINED,
optionalObject: typing.Dict = Component.UNDEFINED,
optionalString: str = Component.UNDEFINED,
optionalSymbol: typing.Any = Component.UNDEFINED,
optionalNode: typing.Union[str, int, float, Component, typing.List[typing.Union[str, int, float, Component]]] = Component.UNDEFINED,
optionalElement: Component = Component.UNDEFINED,
optionalMessage: typing.Any = Component.UNDEFINED,
optionalEnum: typing.Any = Component.UNDEFINED,
optionalUnion: typing.Union[str, typing.Union[float, int], typing.Any] = Component.UNDEFINED,
optionalArrayOf: typing.List[typing.Union[float, int]] = Component.UNDEFINED,
optionalObjectOf: typing.Dict[str, typing.Union[float, int]] = Component.UNDEFINED,
optionalObjectWithExactAndNestedDescription: typing.Any = Component.UNDEFINED,
optionalObjectWithShapeAndNestedDescription: typing.Dict[str, typing.Union[str, typing.Union[float, int], typing.Dict[str, typing.Union[typing.List[typing.Dict], typing.Dict]]]] = Component.UNDEFINED,
optionalAny: typing.Any = Component.UNDEFINED,
customProp: typing.Any = Component.UNDEFINED,
customArrayProp: typing.List[typing.Any] = Component.UNDEFINED,
id: str = Component.UNDEFINED,
**kwargs
):
self._prop_names = ['children', 'id', 'aria-*', 'customArrayProp', 'customProp', 'data-*', 'in', 'optionalAny', 'optionalArray', 'optionalArrayOf', 'optionalBool', 'optionalElement', 'optionalEnum', 'optionalNode', 'optionalNumber', 'optionalObject', 'optionalObjectOf', 'optionalObjectWithExactAndNestedDescription', 'optionalObjectWithShapeAndNestedDescription', 'optionalString', 'optionalUnion']
self._valid_wildcard_attributes = ['data-', 'aria-']
self.available_properties = ['children', 'id', 'aria-*', 'customArrayProp', 'customProp', 'data-*', 'in', 'optionalAny', 'optionalArray', 'optionalArrayOf', 'optionalBool', 'optionalElement', 'optionalEnum', 'optionalNode', 'optionalNumber', 'optionalObject', 'optionalObjectOf', 'optionalObjectWithExactAndNestedDescription', 'optionalObjectWithShapeAndNestedDescription', 'optionalString', 'optionalUnion']
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/development/test_generate_class_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

# Import string not included in generated class string
import_string = (
"# AUTO GENERATED FILE - DO NOT EDIT\n\n"
"# AUTO GENERATED FILE - DO NOT EDIT\n\nimport typing # noqa: F401\n"
+ "from dash.development.base_component import"
+ " Component, _explicitize_args\n\n\n"
)
Expand Down

0 comments on commit d029d79

Please sign in to comment.