Skip to content

Commit c150a49

Browse files
committed
Add custom prop typing for component libs
1 parent 2731a04 commit c150a49

File tree

5 files changed

+108
-33
lines changed

5 files changed

+108
-33
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# This file is automatically loaded on build time to generate types.
2+
3+
def generate_plotly_figure(*_):
4+
return "typing.Union[Figure, dict]"
5+
6+
7+
def generate_datetime_prop(array=False):
8+
9+
def generator(*_):
10+
datetime_type = "typing.Union[str, datetime.datetime]"
11+
if array:
12+
datetime_type = f"typing.Sequence[{datetime_type}]"
13+
return datetime_type
14+
15+
return generator
16+
17+
18+
custom_imports = {
19+
"Graph": ["from plotly.graph_objects import Figure"],
20+
"DatePickerRange": ["import datetime"],
21+
"DatePickerSingle": ["import datetime"],
22+
}
23+
24+
custom_props = {
25+
"Graph": {"figure": generate_plotly_figure},
26+
"DatePickerRange": {
27+
"start_date": generate_datetime_prop(),
28+
"end_date": generate_datetime_prop(),
29+
"min_date_allowed": generate_datetime_prop(),
30+
"max_date_allowed": generate_datetime_prop(),
31+
"disabled_days": generate_datetime_prop(True),
32+
},
33+
"DatePickerSingle": {
34+
"date": generate_datetime_prop(),
35+
"min_date_allowed": generate_datetime_prop(),
36+
"max_date_allowed": generate_datetime_prop(),
37+
"disabled_days": generate_datetime_prop(True),
38+
"initial_visible_month": generate_datetime_prop(),
39+
},
40+
}

dash/dash.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import base64
1717
import traceback
1818
from urllib.parse import urlparse
19-
from typing import Any, Callable, Dict, Optional, Union, List, Sequence
19+
from typing import Any, Callable, Dict, Optional, Union, Sequence
2020

2121
import flask
2222

dash/development/_py_components_generation.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
from dash.exceptions import NonExistentEventException
1111
from ._all_keywords import python_keywords
1212
from ._collect_nodes import collect_nodes, filter_base_nodes
13-
from ._py_prop_typing import get_prop_typing, shapes, custom_imports
13+
from ._py_prop_typing import (
14+
get_custom_props,
15+
get_prop_typing,
16+
shapes,
17+
get_custom_imports,
18+
)
1419
from .base_component import Component, ComponentType
1520

1621
import_string = """# AUTO GENERATED FILE - DO NOT EDIT
@@ -36,6 +41,7 @@ def generate_class_string(
3641
namespace,
3742
prop_reorder_exceptions=None,
3843
max_props=None,
44+
custom_typing_module=None,
3945
):
4046
"""Dynamically generate class strings to have nicely formatted docstrings,
4147
keyword arguments, and repr.
@@ -162,7 +168,14 @@ def __init__(
162168

163169
type_name = type_info.get("name")
164170

165-
typed = get_prop_typing(type_name, typename, prop_key, type_info, namespace)
171+
custom_props = get_custom_props(custom_typing_module)
172+
typed = get_prop_typing(
173+
type_name,
174+
typename,
175+
prop_key,
176+
type_info,
177+
custom_props=custom_props,
178+
)
166179

167180
arg_value = f"{prop_key}: typing.Optional[{typed}] = None"
168181

@@ -208,6 +221,7 @@ def generate_class_file(
208221
namespace,
209222
prop_reorder_exceptions=None,
210223
max_props=None,
224+
custom_typing_module="dash_prop_typing",
211225
):
212226
"""Generate a Python class file (.py) given a class string.
213227
Parameters
@@ -223,10 +237,16 @@ def generate_class_file(
223237
imports = import_string
224238

225239
class_string = generate_class_string(
226-
typename, props, description, namespace, prop_reorder_exceptions, max_props
240+
typename,
241+
props,
242+
description,
243+
namespace,
244+
prop_reorder_exceptions,
245+
max_props,
246+
custom_typing_module,
227247
)
228248

229-
custom_imp = custom_imports[namespace][typename]
249+
custom_imp = get_custom_imports(custom_typing_module).get(typename)
230250
if custom_imp:
231251
imports += "\n".join(custom_imp)
232252
imports += "\n\n"

dash/development/_py_prop_typing.py

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import string
44
import textwrap
5+
import importlib
56

67
import stringcase
78

@@ -15,6 +16,24 @@
1516
custom_imports = collections.defaultdict(lambda: collections.defaultdict(list))
1617

1718

19+
def _get_custom(module_name, prop, default):
20+
if not module_name:
21+
return default
22+
try:
23+
module = importlib.import_module(module_name)
24+
return getattr(module, prop, default)
25+
except ImportError:
26+
return default
27+
28+
29+
def get_custom_imports(module_name):
30+
return _get_custom(module_name, "custom_imports", {})
31+
32+
33+
def get_custom_props(module_name):
34+
return _get_custom(module_name, "custom_props", {})
35+
36+
1837
def _clean_key(key):
1938
k = ""
2039
for ch in key:
@@ -118,17 +137,18 @@ def generate_enum(type_info, *_):
118137

119138

120139
def get_prop_typing(
121-
type_name: str, component_name: str, prop_name: str, type_info, namespace=None
140+
type_name: str,
141+
component_name: str,
142+
prop_name: str,
143+
type_info,
144+
custom_props=None,
122145
):
123146
if prop_name == "id":
124147
# Id is always the same either a string or a dict for pattern matching.
125148
return "typing.Union[str, dict]"
126149

127-
if namespace:
128-
# Only check the namespace once
129-
special = (
130-
special_cases.get(namespace, {}).get(component_name, {}).get(prop_name)
131-
)
150+
if custom_props:
151+
special = custom_props.get(component_name, {}).get(prop_name)
132152
if special:
133153
return special(type_info, component_name, prop_name)
134154

@@ -158,27 +178,6 @@ def generator(*_):
158178
return generator
159179

160180

161-
special_cases = {
162-
"dash_core_components": {
163-
"Graph": {"figure": generate_plotly_figure},
164-
"DatePickerRange": {
165-
"start_date": generate_datetime_prop("DatePickerRange"),
166-
"end_date": generate_datetime_prop("DatePickerRange"),
167-
"min_date_allowed": generate_datetime_prop("DatePickerRange"),
168-
"max_date_allowed": generate_datetime_prop("DatePickerRange"),
169-
"disabled_days": generate_datetime_prop("DatePickerRange", True),
170-
},
171-
"DatePickerSingle": {
172-
"date": generate_datetime_prop("DatePickerSingle"),
173-
"min_date_allowed": generate_datetime_prop("DatePickerSingle"),
174-
"max_date_allowed": generate_datetime_prop("DatePickerSingle"),
175-
"disabled_days": generate_datetime_prop("DatePickerSingle", True),
176-
"initial_visible_month": generate_datetime_prop("DatePickerSingle"),
177-
},
178-
}
179-
}
180-
181-
182181
PROP_TYPING = {
183182
"array": generate_type("typing.Sequence"),
184183
"arrayOf": generate_array_of,

dash/development/component_generator.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def generate_components(
5050
metadata=None,
5151
keep_prop_order=None,
5252
max_props=None,
53+
custom_typing_module=None,
5354
):
5455

5556
project_shortname = project_shortname.replace("-", "_").rstrip("/\\")
@@ -99,7 +100,9 @@ def generate_components(
99100

100101
metadata = safe_json_loads(out.decode("utf-8"))
101102

102-
py_generator_kwargs = {}
103+
py_generator_kwargs = {
104+
"custom_typing_module": custom_typing_module,
105+
}
103106
if keep_prop_order is not None:
104107
keep_prop_order = [
105108
component.strip(" ") for component in keep_prop_order.split(",")
@@ -239,10 +242,22 @@ def component_build_arg_parser():
239242
"but you may also want to reduce further for improved readability at the "
240243
"expense of auto-completion for the later props. Use 0 to include all props.",
241244
)
245+
parser.add_argument(
246+
"-t",
247+
"--custom-typing-module",
248+
type=str,
249+
default="dash_prop_typing",
250+
help=" Module containing custom typing definition for components."
251+
"Can contains two variables:\n"
252+
" - custom_imports: dict[ComponentName, list[str]].\n"
253+
" - custom_props: dict[ComponentName, dict[PropName, function]].\n",
254+
)
242255
return parser
243256

244257

245258
def cli():
259+
# Add current path for loading modules.
260+
sys.path.insert(0, ".")
246261
args = component_build_arg_parser().parse_args()
247262
generate_components(
248263
args.components_source,
@@ -256,6 +271,7 @@ def cli():
256271
jlprefix=args.jl_prefix,
257272
keep_prop_order=args.keep_prop_order,
258273
max_props=args.max_props,
274+
custom_typing_module=args.custom_typing_module,
259275
)
260276

261277

0 commit comments

Comments
 (0)