Skip to content
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

Merged
merged 61 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
5323742
Add typing to component init.
T4rk1n Oct 19, 2022
8626b3d
Add test_typing
T4rk1n Oct 25, 2022
c9bb168
Merge branch 'dev' into prop-typing
T4rk1n Oct 26, 2022
91cc0d6
Add py.typed
T4rk1n Oct 26, 2022
6bbafdc
Lock pyright for py>3.7
T4rk1n Oct 26, 2022
c620f58
Merge branch 'dev' into prop-typing
T4rk1n Oct 27, 2022
147798b
Skip pyright test on py 3.6
T4rk1n Oct 27, 2022
a5250e5
Merge branch 'dev' into prop-typing
T4rk1n Oct 28, 2022
6d50f15
Add numbers as dict key.
T4rk1n Oct 31, 2022
dee40cf
Change number to numbers.Number
T4rk1n Oct 31, 2022
24a8e90
Fix test_generate_class_file
T4rk1n Oct 31, 2022
1f4aed9
Generate enums
T4rk1n Nov 25, 2022
88ca3f3
Generate typed dicts for shapes.
T4rk1n Nov 25, 2022
c18f0fd
Add stringcase & typing_extensions to requires.
T4rk1n Nov 29, 2022
544dcab
Fix whitespace around extra types & runtime classes.
T4rk1n Nov 29, 2022
91a2a1d
Fix metadata test
T4rk1n Nov 29, 2022
ed360e1
Lock version of typing_extensions for 3.6
T4rk1n Nov 29, 2022
b3061a4
Fix keyword in enum & shapes, NoneType in enum + add enum suffix.
T4rk1n Nov 29, 2022
a4ea22e
Fix metadata test
T4rk1n Nov 29, 2022
8029f15
Fix test_attrs_match_forbidden_props
T4rk1n Nov 29, 2022
b0ed806
Add back base numbers.
T4rk1n Nov 30, 2022
ae345e0
Fix tests
T4rk1n Nov 30, 2022
d7136e3
Add pyright test cases for shapes
T4rk1n Nov 30, 2022
a3dc517
Merge branch 'dev' into prop-typing
T4rk1n Dec 1, 2022
a4f2c4a
Merge branch 'dev' into prop-typing
T4rk1n Dec 6, 2022
12c5319
Merge branch 'dev' into prop-typing
T4rk1n Apr 20, 2023
8e0dfea
Fix enum duplicate union type.
T4rk1n Apr 27, 2023
453834b
Add tuple to untyped array typing union.
T4rk1n Apr 27, 2023
13ff296
Add basic tuple as union of arrayOf props typing.
T4rk1n Apr 28, 2023
279950c
Fix typing tests.
T4rk1n May 1, 2023
e6843fd
Replace enum generation with Literal
T4rk1n May 1, 2023
6dc0fc1
Allow all keys in TypedDicts and postpone annotations declared on the…
T4rk1n May 1, 2023
709d1e3
Merge branch 'dev' into prop-typing
T4rk1n May 1, 2023
e6c2356
Relax version of typing_extensions>=4.1.1
T4rk1n May 2, 2023
7bc56b9
Fix test_rdrh003_refresh_jwt
T4rk1n May 2, 2023
9eee253
Move stringcase requirement to requires-install
T4rk1n May 2, 2023
2adb37f
Merge branch 'dev' into prop-typing
T4rk1n Aug 6, 2024
f228185
Fix lint on 12
T4rk1n Aug 8, 2024
7b0da8a
Remove py2 code from explicitize_args
T4rk1n Aug 8, 2024
fb8decb
Update browser-tools
T4rk1n Aug 8, 2024
197180c
Support plotly.Figure
T4rk1n Aug 8, 2024
8734e1b
Use ComponentType
T4rk1n Aug 9, 2024
d5b0ebe
Add Optional to every prop
T4rk1n Aug 12, 2024
4e583e3
Fix list[str] > List[str]
T4rk1n Aug 12, 2024
150c2a5
Fix test_typing
T4rk1n Aug 16, 2024
96b931a
Update pyright
T4rk1n Aug 16, 2024
4045a61
Update pyright
T4rk1n Aug 16, 2024
46d05be
Fix custom imports.
T4rk1n Aug 19, 2024
85c26b7
Include py.typed in manifest
T4rk1n Aug 20, 2024
40bd21a
Add typing to callback function
T4rk1n Aug 20, 2024
2de448e
Merge branch 'dev' into prop-typing
T4rk1n Aug 20, 2024
648a1c6
build
T4rk1n Aug 20, 2024
8d185c8
Update tests/integration/test_typing.py
T4rk1n Aug 26, 2024
ef6b054
Remove tuple from list typing
T4rk1n Aug 26, 2024
6b30d08
Deprecate runtime component loader
T4rk1n Aug 26, 2024
dd5cb44
Merge branch 'prop-typing' of github.com:plotly/dash into prop-typing
T4rk1n Aug 26, 2024
26a83e5
Backward compatible ComponentType.
T4rk1n Aug 26, 2024
18fad2d
Remove component loader deprecation from pr.
T4rk1n Sep 9, 2024
95d8b0d
Merge branch 'dev' into prop-typing
T4rk1n Sep 9, 2024
00da064
Merge branch 'dash-3.0' into prop-typing
T4rk1n Nov 8, 2024
d13b343
Fix type assertion
T4rk1n Nov 8, 2024
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
Empty file.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ include dash/dash-renderer/build/*.js
include dash/dash-renderer/build/*.map
include dash/labextension/dist/dash-jupyterlab.tgz
include dash/labextension/package.json
include dash/py.typed
21 changes: 11 additions & 10 deletions dash/_callback.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import collections
import hashlib
from functools import wraps
from typing import Callable, Optional, Any
from typing import Callable, Optional, Any, List, Tuple

import flask

from .dependencies import (
handle_callback_args,
handle_grouped_callback_args,
Output,
Input,
)
from .exceptions import (
InvalidCallbackReturnValue,
Expand Down Expand Up @@ -60,14 +61,14 @@ def is_no_update(obj):
# pylint: disable=too-many-locals
def callback(
*_args,
background=False,
interval=1000,
progress=None,
progress_default=None,
running=None,
cancel=None,
manager=None,
cache_args_to_ignore=None,
background: bool = False,
interval: int = 1000,
progress: Optional[Output] = None,
progress_default: Any = None,
running: Optional[List[Tuple[Output, Any, Any]]] = None,
Copy link
Contributor

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 :-)

Copy link
Contributor Author

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")]

Copy link
Contributor

@eff-kay eff-kay Aug 26, 2024

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.

cancel: Optional[List[Input]] = None,
manager: Optional[BaseLongCallbackManager] = None,
cache_args_to_ignore: Optional[list] = None,
on_error: Optional[Callable[[Exception], Any]] = None,
**_kwargs,
):
Expand Down Expand Up @@ -154,7 +155,7 @@ def callback(
callback_list = _kwargs.pop("callback_list", GLOBAL_CALLBACK_LIST)

if background:
long_spec = {
long_spec: Any = {
"interval": interval,
}

Expand Down
80 changes: 63 additions & 17 deletions dash/development/_py_components_generation.py
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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,
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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(
Expand All @@ -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()),
)
)

Expand All @@ -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)
Expand Down Expand Up @@ -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]
Expand Down
170 changes: 170 additions & 0 deletions dash/development/_py_prop_typing.py
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 }"
),
" ",
Copy link
Contributor

Choose a reason for hiding this comment

The 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]"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the Union here? Sequence includes tuples, I believe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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]"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, doesn't Sequence include Tuple already?

"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,
}
Loading