Skip to content

Custom typing generator #3152

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

Merged
merged 3 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ This project adheres to [Semantic Versioning](https://semver.org/).

## [3.0.0-rc2] - UNRELEASED

## Added

- [#3152](https://github.com/plotly/dash/pull/3152) Custom Python prop typing for component library.
- Added `-t`, `--custom-typing-module` argument to `dash-generate-components` CLI, default to `dash_prop_typing` and can contains definitions in variables:
- `custom_imports: dict[ComponentName, list[str]]` import statement to be copied at the top of the component class definition.
- `custom_props: dict[ComponentName, dict[PropName, function]]` for custom props. The function signature is: `def generate_type(type_info, component_name, prop_name) -> str`

## Fixed

- [#3142](https://github.com/plotly/dash/pull/3142) Fix typing generation for id and dates props.
Expand Down
40 changes: 40 additions & 0 deletions components/dash-core-components/dash_prop_typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# This file is automatically loaded on build time to generate types.

def generate_plotly_figure(*_):
return "typing.Union[Figure, dict]"


def generate_datetime_prop(array=False):

def generator(*_):
datetime_type = "typing.Union[str, datetime.datetime]"
if array:
datetime_type = f"typing.Sequence[{datetime_type}]"
return datetime_type

return generator


custom_imports = {
"Graph": ["from plotly.graph_objects import Figure"],
"DatePickerRange": ["import datetime"],
"DatePickerSingle": ["import datetime"],
}

custom_props = {
"Graph": {"figure": generate_plotly_figure},
"DatePickerRange": {
"start_date": generate_datetime_prop(),
"end_date": generate_datetime_prop(),
"min_date_allowed": generate_datetime_prop(),
"max_date_allowed": generate_datetime_prop(),
"disabled_days": generate_datetime_prop(True),
},
"DatePickerSingle": {
"date": generate_datetime_prop(),
"min_date_allowed": generate_datetime_prop(),
"max_date_allowed": generate_datetime_prop(),
"disabled_days": generate_datetime_prop(True),
"initial_visible_month": generate_datetime_prop(),
},
}
10 changes: 5 additions & 5 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import base64
import traceback
from urllib.parse import urlparse
from typing import Any, Callable, Dict, Optional, Union, List
from typing import Any, Callable, Dict, Optional, Union, Sequence

import flask

Expand Down Expand Up @@ -397,14 +397,14 @@ def __init__( # pylint: disable=too-many-statements
routes_pathname_prefix: Optional[str] = None,
serve_locally: bool = True,
compress: Optional[bool] = None,
meta_tags: Optional[List[Dict[str, Any]]] = None,
meta_tags: Optional[Sequence[Dict[str, Any]]] = None,
index_string: str = _default_index,
external_scripts: Optional[List[Union[str, Dict[str, Any]]]] = None,
external_stylesheets: Optional[List[Union[str, Dict[str, Any]]]] = None,
external_scripts: Optional[Sequence[Union[str, Dict[str, Any]]]] = None,
external_stylesheets: Optional[Sequence[Union[str, Dict[str, Any]]]] = None,
suppress_callback_exceptions: Optional[bool] = None,
prevent_initial_callbacks: bool = False,
show_undo_redo: bool = False,
extra_hot_reload_paths: Optional[List[str]] = None,
extra_hot_reload_paths: Optional[Sequence[str]] = None,
plugins: Optional[list] = None,
title: str = "Dash",
update_title: str = "Updating...",
Expand Down
28 changes: 24 additions & 4 deletions dash/development/_py_components_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
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, shapes, custom_imports
from ._py_prop_typing import (
get_custom_props,
get_prop_typing,
shapes,
get_custom_imports,
)
from .base_component import Component, ComponentType

import_string = """# AUTO GENERATED FILE - DO NOT EDIT
Expand All @@ -36,6 +41,7 @@ def generate_class_string(
namespace,
prop_reorder_exceptions=None,
max_props=None,
custom_typing_module=None,
):
"""Dynamically generate class strings to have nicely formatted docstrings,
keyword arguments, and repr.
Expand Down Expand Up @@ -162,7 +168,14 @@ def __init__(

type_name = type_info.get("name")

typed = get_prop_typing(type_name, typename, prop_key, type_info, namespace)
custom_props = get_custom_props(custom_typing_module)
typed = get_prop_typing(
type_name,
typename,
prop_key,
type_info,
custom_props=custom_props,
)

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

Expand Down Expand Up @@ -208,6 +221,7 @@ def generate_class_file(
namespace,
prop_reorder_exceptions=None,
max_props=None,
custom_typing_module="dash_prop_typing",
):
"""Generate a Python class file (.py) given a class string.
Parameters
Expand All @@ -223,10 +237,16 @@ def generate_class_file(
imports = import_string

class_string = generate_class_string(
typename, props, description, namespace, prop_reorder_exceptions, max_props
typename,
props,
description,
namespace,
prop_reorder_exceptions,
max_props,
custom_typing_module,
)

custom_imp = custom_imports[namespace][typename]
custom_imp = get_custom_imports(custom_typing_module).get(typename)
if custom_imp:
imports += "\n".join(custom_imp)
imports += "\n\n"
Expand Down
53 changes: 26 additions & 27 deletions dash/development/_py_prop_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json
import string
import textwrap
import importlib

import stringcase

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


def _get_custom(module_name, prop, default):
if not module_name:
return default
try:
module = importlib.import_module(module_name)
return getattr(module, prop, default)
except ImportError:
return default


def get_custom_imports(module_name):
return _get_custom(module_name, "custom_imports", {})


def get_custom_props(module_name):
return _get_custom(module_name, "custom_props", {})


def _clean_key(key):
k = ""
for ch in key:
Expand Down Expand Up @@ -118,17 +137,18 @@ def generate_enum(type_info, *_):


def get_prop_typing(
type_name: str, component_name: str, prop_name: str, type_info, namespace=None
type_name: str,
component_name: str,
prop_name: str,
type_info,
custom_props=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 namespace:
# Only check the namespace once
special = (
special_cases.get(namespace, {}).get(component_name, {}).get(prop_name)
)
if custom_props:
special = custom_props.get(component_name, {}).get(prop_name)
if special:
return special(type_info, component_name, prop_name)

Expand Down Expand Up @@ -158,27 +178,6 @@ def generator(*_):
return generator


special_cases = {
"dash_core_components": {
"Graph": {"figure": generate_plotly_figure},
"DatePickerRange": {
"start_date": generate_datetime_prop("DatePickerRange"),
"end_date": generate_datetime_prop("DatePickerRange"),
"min_date_allowed": generate_datetime_prop("DatePickerRange"),
"max_date_allowed": generate_datetime_prop("DatePickerRange"),
"disabled_days": generate_datetime_prop("DatePickerRange", True),
},
"DatePickerSingle": {
"date": generate_datetime_prop("DatePickerSingle"),
"min_date_allowed": generate_datetime_prop("DatePickerSingle"),
"max_date_allowed": generate_datetime_prop("DatePickerSingle"),
"disabled_days": generate_datetime_prop("DatePickerSingle", True),
"initial_visible_month": generate_datetime_prop("DatePickerSingle"),
},
}
}


PROP_TYPING = {
"array": generate_type("typing.Sequence"),
"arrayOf": generate_array_of,
Expand Down
18 changes: 17 additions & 1 deletion dash/development/component_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def generate_components(
metadata=None,
keep_prop_order=None,
max_props=None,
custom_typing_module=None,
):

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

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

py_generator_kwargs = {}
py_generator_kwargs = {
"custom_typing_module": custom_typing_module,
}
if keep_prop_order is not None:
keep_prop_order = [
component.strip(" ") for component in keep_prop_order.split(",")
Expand Down Expand Up @@ -239,10 +242,22 @@ def component_build_arg_parser():
"but you may also want to reduce further for improved readability at the "
"expense of auto-completion for the later props. Use 0 to include all props.",
)
parser.add_argument(
"-t",
"--custom-typing-module",
type=str,
default="dash_prop_typing",
help=" Module containing custom typing definition for components."
"Can contains two variables:\n"
" - custom_imports: dict[ComponentName, list[str]].\n"
" - custom_props: dict[ComponentName, dict[PropName, function]].\n",
)
return parser


def cli():
# Add current path for loading modules.
sys.path.insert(0, ".")
args = component_build_arg_parser().parse_args()
generate_components(
args.components_source,
Expand All @@ -256,6 +271,7 @@ def cli():
jlprefix=args.jl_prefix,
keep_prop_order=args.keep_prop_order,
max_props=args.max_props,
custom_typing_module=args.custom_typing_module,
)


Expand Down