diff --git a/dash/_callback.py b/dash/_callback.py index ada060f955..ab4ffe921e 100644 --- a/dash/_callback.py +++ b/dash/_callback.py @@ -77,7 +77,7 @@ def callback( cache_ignore_triggered=True, on_error: Optional[Callable[[Exception], Any]] = None, **_kwargs, -): +) -> Callable[..., Any]: """ Normally used as a decorator, `@dash.callback` provides a server-side callback relating the values of one or more `Output` items to one or diff --git a/dash/dash.py b/dash/dash.py index e617901bd3..068cfdc162 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -1269,7 +1269,7 @@ def clientside_callback(self, clientside_function, *args, **kwargs): **kwargs, ) - def callback(self, *_args, **_kwargs): + def callback(self, *_args, **_kwargs) -> Callable[..., Any]: """ Normally used as a decorator, `@app.callback` provides a server-side callback relating the values of one or more `Output` items to one or diff --git a/dash/dependencies.py b/dash/dependencies.py index 819d134546..00186da1c8 100644 --- a/dash/dependencies.py +++ b/dash/dependencies.py @@ -1,3 +1,5 @@ +from typing import Union, Sequence + from dash.development.base_component import Component from ._validate import validate_callback @@ -5,8 +7,11 @@ from ._utils import stringify_id +ComponentIdType = Union[str, Component, dict] + + class _Wildcard: # pylint: disable=too-few-public-methods - def __init__(self, name): + def __init__(self, name: str): self._name = name def __str__(self): @@ -15,7 +20,7 @@ def __str__(self): def __repr__(self): return f"<{self}>" - def to_json(self): + def to_json(self) -> str: # used in serializing wildcards - arrays are not allowed as # id values, so make the wildcards look like length-1 arrays. return f'["{self._name}"]' @@ -27,7 +32,12 @@ def to_json(self): class DashDependency: # pylint: disable=too-few-public-methods - def __init__(self, component_id, component_property): + component_id: ComponentIdType + allow_duplicate: bool + component_property: str + allowed_wildcards: Sequence[_Wildcard] + + def __init__(self, component_id: ComponentIdType, component_property: str): if isinstance(component_id, Component): self.component_id = component_id._set_random_id() @@ -43,10 +53,10 @@ def __str__(self): def __repr__(self): return f"<{self.__class__.__name__} `{self}`>" - def component_id_str(self): + def component_id_str(self) -> str: return stringify_id(self.component_id) - def to_dict(self): + def to_dict(self) -> dict: return {"id": self.component_id_str(), "property": self.component_property} def __eq__(self, other): @@ -61,7 +71,7 @@ def __eq__(self, other): and self._id_matches(other) ) - def _id_matches(self, other): + def _id_matches(self, other) -> bool: my_id = self.component_id other_id = other.component_id self_dict = isinstance(my_id, dict) @@ -96,7 +106,7 @@ def _id_matches(self, other): def __hash__(self): return hash(str(self)) - def has_wildcard(self): + def has_wildcard(self) -> bool: """ Return true if id contains a wildcard (MATCH, ALL, or ALLSMALLER) """ @@ -112,7 +122,12 @@ class Output(DashDependency): # pylint: disable=too-few-public-methods allowed_wildcards = (MATCH, ALL) - def __init__(self, component_id, component_property, allow_duplicate=False): + def __init__( + self, + component_id: ComponentIdType, + component_property: str, + allow_duplicate: bool = False, + ): super().__init__(component_id, component_property) self.allow_duplicate = allow_duplicate @@ -130,7 +145,7 @@ class State(DashDependency): # pylint: disable=too-few-public-methods class ClientsideFunction: # pylint: disable=too-few-public-methods - def __init__(self, namespace=None, function_name=None): + def __init__(self, namespace: str, function_name: str): if namespace.startswith("_dashprivate_"): raise ValueError("Namespaces cannot start with '_dashprivate_'.") diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index c18db36780..02bb49061e 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -24,7 +24,7 @@ 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 +from dash.development.base_component import Component try: from dash.development.base_component import ComponentType # noqa: F401 except ImportError: @@ -80,7 +80,8 @@ def generate_class_string( _namespace = '{namespace}' _type = '{typename}' {shapes} - @_explicitize_args + _explicitize_dash_init = True + def __init__( self, {default_argtext} diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 39131a72d6..1e38311336 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -51,13 +51,21 @@ class ComponentMeta(abc.ABCMeta): # pylint: disable=arguments-differ def __new__(mcs, name, bases, attributes): - component = abc.ABCMeta.__new__(mcs, name, bases, attributes) module = attributes["__module__"].split(".")[0] + + if attributes.get("_explicitize_dash_init", False): + # We only want to patch the new generated component without + # the `@_explicitize_args` decorator for mypy support + # See issue: https://github.com/plotly/dash/issues/3226 + attributes["__init__"] = _explicitize_args(attributes["__init__"]) + + _component = abc.ABCMeta.__new__(mcs, name, bases, attributes) + if name == "Component" or module == "builtins": - # Don't do the base component + # Don't add to the registry the base component # and the components loaded dynamically by load_component # as it doesn't have the namespace. - return component + return _component _namespace = attributes.get("_namespace", module) ComponentRegistry.namespace_to_package[_namespace] = module @@ -66,7 +74,7 @@ def __new__(mcs, name, bases, attributes): "_children_props" ) - return component + return _component def is_number(s): @@ -435,6 +443,8 @@ def _validate_deprecation(self): ComponentType = typing.TypeVar("ComponentType", bound=Component) +ComponentTemplate = typing.TypeVar("ComponentTemplate") + # This wrapper adds an argument given to generated Component.__init__ # with the actual given parameters by the user as a list of string. diff --git a/requirements/ci.txt b/requirements/ci.txt index aa3cd94bfb..96495aa4f9 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -18,4 +18,4 @@ pyzmq==25.1.2 xlrd>=2.0.1 pytest-rerunfailures jupyterlab<4.0.0 -pyright==1.1.376;python_version>="3.7" +pyright==1.1.398;python_version>="3.7" diff --git a/tests/integration/test_typing.py b/tests/integration/test_typing.py index cf49c1577d..d6e7069698 100644 --- a/tests/integration/test_typing.py +++ b/tests/integration/test_typing.py @@ -223,7 +223,9 @@ def assert_pyright_output( "obj={}", { "expected_status": 1, - "expected_outputs": ['"dict[Any, Any]" is incompatible with "Obj"'], + "expected_outputs": [ + '"dict[Any, Any]" cannot be assigned to parameter "obj" of type "Obj | None"' + ], }, ), ( @@ -231,7 +233,7 @@ def assert_pyright_output( { "expected_status": 1, "expected_outputs": [ - '"dict[str, str | int]" is incompatible with "Obj"' + '"dict[str, str | int]" cannot be assigned to parameter "obj" of type "Obj | None"' ], }, ), diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index 72b8e40414..2388f13c11 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -3,7 +3,7 @@ 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 +from dash.development.base_component import Component try: from dash.development.base_component import ComponentType # noqa: F401 except ImportError: @@ -131,7 +131,8 @@ class Table(Component): } ) - @_explicitize_args + _explicitize_dash_init = True + def __init__( self, children: typing.Optional[typing.Union[str, int, float, ComponentType, typing.Sequence[typing.Union[str, int, float, ComponentType]]]] = None,