diff --git a/@plotly/dash-generator-test-component-typescript/base/py.typed b/@plotly/dash-generator-test-component-typescript/base/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/MANIFEST.in b/MANIFEST.in index ec222d3c57..457204bf7d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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 diff --git a/dash/__init__.py b/dash/__init__.py index 8f740e35ba..7fcac0f0ed 100644 --- a/dash/__init__.py +++ b/dash/__init__.py @@ -53,3 +53,37 @@ def _jupyter_nbextension_paths(): "require": "dash/main", } ] + + +__all__ = [ + "Input", + "Output", + "State", + "ClientsideFunction", + "MATCH", + "ALLSMALLER", + "ALL", + "development", + "exceptions", + "dcc", + "html", + "dash_table", + "__version__", + "callback_context", + "set_props", + "callback", + "get_app", + "get_asset_url", + "get_relative_path", + "strip_relative_path", + "CeleryManager", + "DiskcacheManager", + "register_page", + "page_registry", + "Dash", + "no_update", + "page_container", + "Patch", + "jupyter_dash", + "ctx", +] diff --git a/dash/_callback.py b/dash/_callback.py index 071c209dec..0e901ea8cc 100644 --- a/dash/_callback.py +++ b/dash/_callback.py @@ -1,7 +1,7 @@ import collections import hashlib from functools import wraps -from typing import Callable, Optional, Any +from typing import Callable, Optional, Any, List, Tuple import flask @@ -9,6 +9,7 @@ handle_callback_args, handle_grouped_callback_args, Output, + Input, ) from .development.base_component import ComponentRegistry from .exceptions import ( @@ -62,14 +63,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, + cancel: Optional[List[Input]] = None, + manager: Optional[BaseLongCallbackManager] = None, + cache_args_to_ignore: Optional[list] = None, on_error: Optional[Callable[[Exception], Any]] = None, **_kwargs, ): @@ -156,7 +157,7 @@ def callback( callback_list = _kwargs.pop("callback_list", GLOBAL_CALLBACK_LIST) if background: - long_spec = { + long_spec: Any = { "interval": interval, } diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index f2f8443ed5..6fa88bb396 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -1,16 +1,34 @@ 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 +import_string = """# AUTO GENERATED FILE - DO NOT EDIT -# pylint: disable=unused-argument,too-many-locals +import typing # noqa: F401 +import numbers # noqa: F401 +from typing_extensions import TypedDict, NotRequired, Literal # noqa: F401 +from dash.development.base_component import Component, _explicitize_args +try: + from dash.development.base_component import ComponentType # noqa: F401 +except ImportError: + ComponentType = typing.TypeVar("ComponentType", bound=Component) + + +""" + + +# pylint: disable=unused-argument,too-many-locals,too-many-branches def generate_class_string( typename, props, @@ -54,8 +72,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} + ): self._prop_names = {list_of_valid_keys} self._valid_wildcard_attributes =\ {list_of_valid_wildcard_attr_prefixes} @@ -94,7 +116,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 +142,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 +179,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 +196,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()), ) ) @@ -179,20 +220,22 @@ def generate_class_file( Returns ------- """ - import_string = ( - "# AUTO GENERATED FILE - DO NOT EDIT\n\n" - + "from dash.development.base_component import " - + "Component, _explicitize_args\n\n\n" - ) + imports = import_string class_string = generate_class_string( typename, props, description, namespace, prop_reorder_exceptions, max_props ) + + custom_imp = custom_imports[namespace][typename] + if custom_imp: + imports += "\n".join(custom_imp) + imports += "\n\n" + file_name = f"{typename:s}.py" file_path = os.path.join(namespace, file_name) with open(file_path, "w", encoding="utf-8") as f: - f.write(import_string) + f.write(imports) f.write(class_string) print(f"Generated {file_name}") @@ -242,7 +285,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] diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py new file mode 100644 index 0000000000..bd38ec054c --- /dev/null +++ b/dash/development/_py_prop_typing.py @@ -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 }" + ), + " ", + ) + + 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.Sequence[{typed}]" + + +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.Sequence"), + "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, +} diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 1b5d844517..39131a72d6 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -2,6 +2,7 @@ import collections import inspect import sys +import typing import uuid import random import warnings @@ -88,6 +89,12 @@ def _check_if_has_indexable_children(item): class Component(metaclass=ComponentMeta): _children_props = [] _base_nodes = ["children"] + _namespace: str + _type: str + _prop_names: typing.List[str] + + _valid_wildcard_attributes: typing.List[str] + available_wildcard_properties: typing.List[str] class _UNDEFINED: def __repr__(self): @@ -111,7 +118,6 @@ def __init__(self, **kwargs): self._validate_deprecation() import dash # pylint: disable=import-outside-toplevel, cyclic-import - # pylint: disable=super-init-not-called for k, v in list(kwargs.items()): # pylint: disable=no-member k_in_propnames = k in self._prop_names @@ -427,13 +433,15 @@ def _validate_deprecation(self): warnings.warn(DeprecationWarning(textwrap.dedent(deprecation_message))) +ComponentType = typing.TypeVar("ComponentType", bound=Component) + + +# This wrapper adds an argument given to generated Component.__init__ +# with the actual given parameters by the user as a list of string. +# This is then checked in the generated init to check if required +# props were provided. def _explicitize_args(func): - # Python 2 - if hasattr(func, "func_code"): - varnames = func.func_code.co_varnames - # Python 3 - else: - varnames = func.__code__.co_varnames + varnames = func.__code__.co_varnames def wrapper(*args, **kwargs): if "_explicit_args" in kwargs: @@ -445,11 +453,8 @@ def wrapper(*args, **kwargs): kwargs["_explicit_args"].remove("self") return func(*args, **kwargs) - # If Python 3, we can set the function signature to be correct - if hasattr(inspect, "signature"): - # pylint: disable=no-member - new_sig = inspect.signature(wrapper).replace( - parameters=inspect.signature(func).parameters.values() - ) - wrapper.__signature__ = new_sig + new_sig = inspect.signature(wrapper).replace( + parameters=list(inspect.signature(func).parameters.values()) + ) + wrapper.__signature__ = new_sig return wrapper diff --git a/dash/py.typed b/dash/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dash/types.py b/dash/types.py index c954f654bb..9a39adb43e 100644 --- a/dash/types.py +++ b/dash/types.py @@ -1,7 +1,7 @@ from typing_extensions import TypedDict, NotRequired -class RendererHooks(TypedDict): +class RendererHooks(TypedDict): # pylint: disable=too-many-ancestors layout_pre: NotRequired[str] layout_post: NotRequired[str] request_pre: NotRequired[str] diff --git a/requirements/ci.txt b/requirements/ci.txt index a7dd6507ff..b7f501bfb2 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -19,3 +19,4 @@ pyzmq==25.1.2 xlrd>=2.0.1 pytest-rerunfailures jupyterlab<4.0.0 +pyright==1.1.376;python_version>="3.7" diff --git a/requirements/install.txt b/requirements/install.txt index 139752ea65..94e78db8da 100644 --- a/requirements/install.txt +++ b/requirements/install.txt @@ -10,3 +10,4 @@ requests retrying nest-asyncio setuptools +stringcase>=1.2.0 diff --git a/tests/integration/test_typing.py b/tests/integration/test_typing.py new file mode 100644 index 0000000000..cf49c1577d --- /dev/null +++ b/tests/integration/test_typing.py @@ -0,0 +1,251 @@ +import os +import shlex +import subprocess +import sys + +import pytest + +component_template = """ +from dash_generator_test_component_typescript.TypeScriptComponent import TypeScriptComponent + +t = TypeScriptComponent({0}) +""" + + +def run_pyright(codefile: str): + + cmd = shlex.split( + f"pyright {codefile}", + posix=sys.platform != "win32", + comments=True, + ) + + env = os.environ.copy() + + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env, + ) + out, err = proc.communicate() + return out.decode(), err.decode(), proc.poll() + + +def assert_pyright_output( + codefile: str, expected_outputs=tuple(), expected_errors=tuple(), expected_status=0 +): + output, error, status = run_pyright(codefile) + assert ( + status == expected_status + ), f"Status: {status}\nOutput: {output}\nError: {error}" + for ex_out in expected_outputs: + assert ex_out in output, f"Invalid output:\n {output}" + for ex_err in expected_errors: + assert ex_err in error + + +@pytest.mark.parametrize( + "arguments, assertions", + [ + ( + "a_string=4", + { + "expected_status": 1, + "expected_outputs": [ + 'Argument of type "Literal[4]" cannot be assigned to parameter "a_string" of type "str | None"' + ], + }, + ), + ( + "a_string='FooBar'", + { + "expected_status": 0, + }, + ), + ( + "a_number=''", + { + "expected_status": 1, + "expected_outputs": [ + 'Argument of type "Literal[\'\']" cannot be assigned to parameter "a_number" ' + 'of type "int | float | Number | None"' + ], + }, + ), + ( + "a_number=0", + { + "expected_status": 0, + }, + ), + ( + "a_number=2.2", + { + "expected_status": 0, + }, + ), + ( + "a_bool=4", + { + "expected_status": 1, + }, + ), + ( + "a_bool=True", + { + "expected_status": 0, + }, + ), + ( + "array_string={}", + { + "expected_status": 1, + "expected_outputs": [ + 'Argument of type "dict[Any, Any]" cannot be assigned to parameter "array_string" ' + 'of type "Sequence[str] | None"' + ], + }, + ), + ( + "array_string=[]", + { + "expected_status": 0, + }, + ), + ( + "array_string=[1,2,4]", + { + "expected_status": 1, + }, + ), + ( + "array_number=[1,2]", + { + "expected_status": 0, + }, + ), + ( + "array_number=['not','a', 'number']", + { + "expected_status": 1, + }, + ), + ( + "array_obj=[{'a': 'b'}]", + { + "expected_status": 0, + }, + ), + ( + "array_obj=[1]", + { + "expected_status": 1, + }, + ), + ( + "array_obj=[1, {}]", + { + "expected_status": 1, + }, + ), + ( + "union='Union'", + { + "expected_status": 0, + }, + ), + ( + "union=1", + { + "expected_status": 0, + }, + ), + ( + "union=0.42", + { + "expected_status": 0, + }, + ), + ( + "union=[]", + { + "expected_status": 1, + }, + ), + ( + "element=[]", + { + "expected_status": 0, + }, + ), + ( + "element=[TypeScriptComponent()]", + { + "expected_status": 0, + }, + ), + ( + "element=TypeScriptComponent()", + { + "expected_status": 0, + }, + ), + ( + "element=set()", + { + "expected_status": 1, + }, + ), + ( + "a_tuple=(1,2)", + { + "expected_status": 1, + "expected_outputs": [ + 'Argument of type "tuple[Literal[1], Literal[2]]" cannot be assigned ' + 'to parameter "a_tuple" of type "Tuple[int | float | Number, str] | None"' + ], + }, + ), + ( + "a_tuple=(1, 'tuple')", + { + "expected_status": 0, + }, + ), + ( + "obj=set()", + { + "expected_status": 1, + }, + ), + ( + "obj={}", + { + "expected_status": 1, + "expected_outputs": ['"dict[Any, Any]" is incompatible with "Obj"'], + }, + ), + ( + "obj={'value': 'a', 'label': 1}", + { + "expected_status": 1, + "expected_outputs": [ + '"dict[str, str | int]" is incompatible with "Obj"' + ], + }, + ), + ( + "obj={'value': 'a', 'label': 'lab'}", + { + "expected_status": 0, + }, + ), + ], +) +def test_component_typing(arguments, assertions, tmp_path): + codefile = os.path.join(tmp_path, "code.py") + with open(codefile, "w") as f: + f.write(component_template.format(arguments)) + + assert_pyright_output(codefile, **assertions) diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index c5e3a0436a..78ea54959e 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -1,6 +1,13 @@ # AUTO GENERATED FILE - DO NOT EDIT +import typing # noqa: F401 +import numbers # noqa: F401 +from typing_extensions import TypedDict, NotRequired, Literal # noqa: F401 from dash.development.base_component import Component, _explicitize_args +try: + from dash.development.base_component import ComponentType # noqa: F401 +except ImportError: + ComponentType = typing.TypeVar("ComponentType", bound=Component) class Table(Component): @@ -90,8 +97,66 @@ class Table(Component): _base_nodes = ['optionalNode', 'optionalElement', 'children'] _namespace = 'TableComponents' _type = 'Table' + OptionalObjectWithExactAndNestedDescriptionFigure = TypedDict( + "OptionalObjectWithExactAndNestedDescriptionFigure", + { + "data": NotRequired[typing.Sequence[dict]], + "layout": NotRequired[dict] + } + ) + + OptionalObjectWithExactAndNestedDescription = TypedDict( + "OptionalObjectWithExactAndNestedDescription", + { + "color": NotRequired[str], + "fontSize": NotRequired[typing.Union[int, float, numbers.Number]], + "figure": NotRequired["OptionalObjectWithExactAndNestedDescriptionFigure"] + } + ) + + OptionalObjectWithShapeAndNestedDescriptionFigure = TypedDict( + "OptionalObjectWithShapeAndNestedDescriptionFigure", + { + "data": NotRequired[typing.Sequence[dict]], + "layout": NotRequired[dict] + } + ) + + OptionalObjectWithShapeAndNestedDescription = TypedDict( + "OptionalObjectWithShapeAndNestedDescription", + { + "color": NotRequired[str], + "fontSize": NotRequired[typing.Union[int, float, numbers.Number]], + "figure": NotRequired["OptionalObjectWithShapeAndNestedDescriptionFigure"] + } + ) + @_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: typing.Optional[typing.Union[str, int, float, ComponentType, typing.Sequence[typing.Union[str, int, float, ComponentType]]]] = None, + optionalArray: typing.Optional[typing.Sequence] = None, + optionalBool: typing.Optional[bool] = None, + optionalFunc: typing.Optional[typing.Any] = None, + optionalNumber: typing.Optional[typing.Union[int, float, numbers.Number]] = None, + optionalObject: typing.Optional[dict] = None, + optionalString: typing.Optional[str] = None, + optionalSymbol: typing.Optional[typing.Any] = None, + optionalNode: typing.Optional[typing.Union[str, int, float, ComponentType, typing.Sequence[typing.Union[str, int, float, ComponentType]]]] = None, + optionalElement: typing.Optional[ComponentType] = None, + optionalMessage: typing.Optional[typing.Any] = None, + optionalEnum: typing.Optional[Literal["News", "Photos"]] = None, + optionalUnion: typing.Optional[typing.Union[str, typing.Union[int, float, numbers.Number], typing.Any]] = None, + optionalArrayOf: typing.Optional[typing.Sequence[typing.Union[int, float, numbers.Number]]] = None, + optionalObjectOf: typing.Optional[typing.Dict[typing.Union[str, float, int], typing.Union[int, float, numbers.Number]]] = None, + optionalObjectWithExactAndNestedDescription: typing.Optional["OptionalObjectWithExactAndNestedDescription"] = None, + optionalObjectWithShapeAndNestedDescription: typing.Optional["OptionalObjectWithShapeAndNestedDescription"] = None, + optionalAny: typing.Optional[typing.Any] = None, + customProp: typing.Optional[typing.Any] = None, + customArrayProp: typing.Optional[typing.Sequence[typing.Any]] = None, + id: typing.Optional[str] = None, + **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'] diff --git a/tests/unit/development/test_generate_class.py b/tests/unit/development/test_generate_class.py index 2c7c0de494..2a07fee7ca 100644 --- a/tests/unit/development/test_generate_class.py +++ b/tests/unit/development/test_generate_class.py @@ -128,6 +128,14 @@ def test_attrs_match_forbidden_props(component_class): # props are not added as attrs unless explicitly provided # except for children, which is always set if it's a prop at all. expected_attrs = set(reserved_words + ["children"]) - {"_.*"} + expected_attrs.update( + { + "OptionalObjectWithExactAndNestedDescription", + "OptionalObjectWithExactAndNestedDescriptionFigure", + "OptionalObjectWithShapeAndNestedDescription", + "OptionalObjectWithShapeAndNestedDescriptionFigure", + } + ) c = component_class() base_attrs = set(dir(c)) extra_attrs = set(a for a in base_attrs if a[0] != "_") diff --git a/tests/unit/development/test_generate_class_file.py b/tests/unit/development/test_generate_class_file.py index b3ba34109b..f79121bf01 100644 --- a/tests/unit/development/test_generate_class_file.py +++ b/tests/unit/development/test_generate_class_file.py @@ -8,16 +8,10 @@ from dash.development._py_components_generation import ( generate_class_string, generate_class_file, + import_string, ) from . import _dir, has_trailing_space -# Import string not included in generated class string -import_string = ( - "# AUTO GENERATED FILE - DO NOT EDIT\n\n" - + "from dash.development.base_component import" - + " Component, _explicitize_args\n\n\n" -) - @pytest.fixture def make_component_dir(load_test_metadata_json):