From 53237426582253803b0c91be61a89bb9522377b1 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 19 Oct 2022 13:57:41 -0400 Subject: [PATCH 01/49] Add typing to component init. --- dash/development/_py_components_generation.py | 60 +++++++++++---- dash/development/_py_prop_typing.py | 77 +++++++++++++++++++ tests/unit/development/metadata_test.py | 27 ++++++- .../development/test_generate_class_file.py | 2 +- 4 files changed, 148 insertions(+), 18 deletions(-) create mode 100644 dash/development/_py_prop_typing.py diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index 6e7c49ecdf..145eca2ce1 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -1,16 +1,18 @@ from collections import OrderedDict import copy import os +import typing from textwrap import fill, dedent 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 ._py_prop_typing import get_prop_typing from .base_component import Component -# pylint: disable=unused-argument,too-many-locals +# pylint: disable=unused-argument,too-many-locals,too-many-branches def generate_class_string( typename, props, @@ -55,7 +57,10 @@ def generate_class_string( _namespace = '{namespace}' _type = '{typename}' @_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 +99,7 @@ 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, " + default_argtext = f"children: {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 +123,33 @@ 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 + required = prop.get("required") + type_info = prop.get("type") + + default_value = "Component.REQUIRED" if required else "Component.UNDEFINED" + + if not type_info: + print(f"Invalid prop type for typing: {prop_key}") + default_arglist.append(f"{prop_key} = {default_value}") + continue + + type_name = type_info.get("name") + + typed = get_prop_typing(type_name, type_info) + + arg_value = f"{prop_key}: {typed} = {default_value}" + + default_arglist.append(arg_value) if max_props: final_max_props = max_props - (1 if "children" in props else 0) @@ -139,7 +162,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( @@ -181,8 +204,9 @@ 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" + "from dash.development.base_component import " + "Component, _explicitize_args\n\n\n" ) class_string = generate_class_string( @@ -242,7 +266,11 @@ def generate_class( string = generate_class_string( typename, props, description, namespace, prop_reorder_exceptions ) - scope = {"Component": Component, "_explicitize_args": _explicitize_args} + scope = { + "Component": Component, + "_explicitize_args": _explicitize_args, + "typing": typing, + } # 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..4dc10bcfd0 --- /dev/null +++ b/dash/development/_py_prop_typing.py @@ -0,0 +1,77 @@ +def generate_any(_): + return "typing.Any" + + +def generate_shape(t): + props = [] + + for prop in t["value"].values(): + prop_type = PROP_TYPING.get(prop["name"], generate_any)(prop) + if prop_type not in props: + props.append(prop_type) + + if len(props) == 0: + return "typing.Any" + + return f"typing.Dict[str, typing.Union[{', '.join(props)}]]" + + +def generate_union(t): + types = [] + for union in t["value"]: + u_type = PROP_TYPING.get(union["name"], generate_any)(union) + if u_type not in types: + types.append(u_type) + return f"typing.Union[{', '.join(types)}]" + + +def generate_tuple(type_info): + els = type_info.get("elements") + elements = ", ".join(get_prop_typing(x.get("name"), x) for x in els) + return f"typing.Tuple[{elements}]" + + +def generate_array_of(t): + typed = PROP_TYPING.get(t["value"]["name"], generate_any)(t["value"]) + return f"typing.List[{typed}]" + + +def generate_object_of(t): + typed = PROP_TYPING.get(t["value"]["name"], generate_any)(t["value"]) + return f"typing.Dict[str, {typed}]" + + +def generate_type(typename): + def type_handler(_): + return typename + + return type_handler + + +def get_prop_typing(type_name: str, type_info): + return PROP_TYPING.get(type_name, generate_any)(type_info) + + +PROP_TYPING = { + "array": generate_type("typing.List"), + "arrayOf": generate_array_of, + "object": generate_type("typing.Dict"), + "shape": generate_shape, + "exact": generate_shape, + "string": generate_type("str"), + "bool": generate_type("bool"), + "number": generate_type("typing.Union[float, int]"), + "node": generate_type( + "typing.Union[str, int, float, Component," + " typing.List[typing.Union" + "[str, int, float, Component]]]" + ), + "func": generate_any, + "element": generate_type("Component"), + "union": generate_union, + "any": generate_any, + "custom": generate_any, + "enum": generate_any, + "objectOf": generate_object_of, + "tuple": generate_tuple, +} diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index 8f1dac0a37..4eadcf5b13 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -1,5 +1,6 @@ # AUTO GENERATED FILE - DO NOT EDIT +import typing # noqa: F401 from dash.development.base_component import Component, _explicitize_args @@ -91,7 +92,31 @@ class Table(Component): _namespace = 'TableComponents' _type = 'Table' @_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.Union[str, int, float, Component, typing.List[typing.Union[str, int, float, Component]]] = None, + optionalArray: typing.List = Component.UNDEFINED, + optionalBool: bool = Component.UNDEFINED, + optionalFunc: typing.Any = Component.UNDEFINED, + optionalNumber: typing.Union[float, int] = Component.UNDEFINED, + optionalObject: typing.Dict = Component.UNDEFINED, + optionalString: str = Component.UNDEFINED, + optionalSymbol: typing.Any = Component.UNDEFINED, + optionalNode: typing.Union[str, int, float, Component, typing.List[typing.Union[str, int, float, Component]]] = Component.UNDEFINED, + optionalElement: Component = Component.UNDEFINED, + optionalMessage: typing.Any = Component.UNDEFINED, + optionalEnum: typing.Any = Component.UNDEFINED, + optionalUnion: typing.Union[str, typing.Union[float, int], typing.Any] = Component.UNDEFINED, + optionalArrayOf: typing.List[typing.Union[float, int]] = Component.UNDEFINED, + optionalObjectOf: typing.Dict[str, typing.Union[float, int]] = Component.UNDEFINED, + optionalObjectWithExactAndNestedDescription: typing.Dict[str, typing.Union[str, typing.Union[float, int], typing.Dict[str, typing.Union[typing.List[typing.Dict], typing.Dict]]]] = Component.UNDEFINED, + optionalObjectWithShapeAndNestedDescription: typing.Dict[str, typing.Union[str, typing.Union[float, int], typing.Dict[str, typing.Union[typing.List[typing.Dict], typing.Dict]]]] = Component.UNDEFINED, + optionalAny: typing.Any = Component.UNDEFINED, + customProp: typing.Any = Component.UNDEFINED, + customArrayProp: typing.List[typing.Any] = Component.UNDEFINED, + id: str = Component.UNDEFINED, + **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_file.py b/tests/unit/development/test_generate_class_file.py index b3ba34109b..c3d496b43a 100644 --- a/tests/unit/development/test_generate_class_file.py +++ b/tests/unit/development/test_generate_class_file.py @@ -13,7 +13,7 @@ # Import string not included in generated class string import_string = ( - "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + "# AUTO GENERATED FILE - DO NOT EDIT\n\nimport typing # noqa: F401\n" + "from dash.development.base_component import" + " Component, _explicitize_args\n\n\n" ) From 8626b3d2bcc749da877df88af06ce9d8d10823f4 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 25 Oct 2022 17:41:34 -0400 Subject: [PATCH 02/49] Add test_typing --- dash/development/base_component.py | 4 + requires-ci.txt | 1 + tests/integration/test_typing.py | 221 +++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 tests/integration/test_typing.py diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 99373e2df0..1746f55fed 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 @@ -71,6 +72,9 @@ class Component(metaclass=ComponentMeta): _children_props = [] _base_nodes = ["children"] + _valid_wildcard_attributes: typing.List[str] + available_wildcard_properties: typing.List[str] + class _UNDEFINED: def __repr__(self): return "undefined" diff --git a/requires-ci.txt b/requires-ci.txt index cc95315651..a14d8ab3df 100644 --- a/requires-ci.txt +++ b/requires-ci.txt @@ -24,3 +24,4 @@ pytest-sugar==0.9.4 xlrd>=2.0.1;python_version>="3.8" xlrd<2;python_version<"3.8" pytest-rerunfailures +pyright==1.1.276 diff --git a/tests/integration/test_typing.py b/tests/integration/test_typing.py new file mode 100644 index 0000000000..e582a6ab85 --- /dev/null +++ b/tests/integration/test_typing.py @@ -0,0 +1,221 @@ +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): + + 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, 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 + 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"' + ], + }, + ), + ( + "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 "float | int"' + ], + }, + ), + ( + "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 "List[str]"' + ], + }, + ), + ( + "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=[{}]", + { + "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[float | int, str]"' + ], + }, + ), + ( + "a_tuple=(1, 'tuple')", + { + "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) From 91cc0d60a7cc9587847cd7221773b3717e97fdb3 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 26 Oct 2022 13:17:10 -0400 Subject: [PATCH 03/49] Add py.typed --- @plotly/dash-generator-test-component-typescript/base/py.typed | 0 dash/py.typed | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 @plotly/dash-generator-test-component-typescript/base/py.typed create mode 100644 dash/py.typed 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/dash/py.typed b/dash/py.typed new file mode 100644 index 0000000000..e69de29bb2 From 6bbafdc7fdb01a03f1387bd7a5c1ff585108b94a Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 26 Oct 2022 13:20:16 -0400 Subject: [PATCH 04/49] Lock pyright for py>3.7 --- requires-ci.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requires-ci.txt b/requires-ci.txt index 1cdadbb8ba..f8cdbaaf48 100644 --- a/requires-ci.txt +++ b/requires-ci.txt @@ -26,4 +26,4 @@ py==1.11.0 xlrd>=2.0.1;python_version>="3.8" xlrd<2;python_version<"3.8" pytest-rerunfailures -pyright==1.1.276 +pyright==1.1.276;python_version>="3.7" From 147798bf2920a3ee5e16836b0e2cd65a9afdadde Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 27 Oct 2022 10:40:35 -0400 Subject: [PATCH 05/49] Skip pyright test on py 3.6 --- tests/integration/test_typing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/test_typing.py b/tests/integration/test_typing.py index e582a6ab85..1a7257ac7b 100644 --- a/tests/integration/test_typing.py +++ b/tests/integration/test_typing.py @@ -45,6 +45,7 @@ def assert_pyright_output( assert ex_err in error +@pytest.mark.skipif(sys.version_info < (3, 7), reason="pyright not available on 3.6") @pytest.mark.parametrize( "arguments, assertions", [ From 6d50f1506d8198d7db90b8e956ab2403e502415b Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 31 Oct 2022 10:28:27 -0400 Subject: [PATCH 06/49] Add numbers as dict key. --- dash/development/_py_prop_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index 4dc10bcfd0..10375c9fd7 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -38,7 +38,7 @@ def generate_array_of(t): def generate_object_of(t): typed = PROP_TYPING.get(t["value"]["name"], generate_any)(t["value"]) - return f"typing.Dict[str, {typed}]" + return f"typing.Dict[typing.Union[str, float, int], {typed}]" def generate_type(typename): From dee40cfe9ef45ae684d86b9c8362dc38a0eeb4bd Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 31 Oct 2022 11:56:55 -0400 Subject: [PATCH 07/49] Change number to numbers.Number --- dash/development/_py_components_generation.py | 1 + dash/development/_py_prop_typing.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index 145eca2ce1..4a4f5148f2 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -205,6 +205,7 @@ def generate_class_file( import_string = ( "# AUTO GENERATED FILE - DO NOT EDIT\n\n" "import typing # noqa: F401\n" + "import numbers # noqa: F401\n" "from dash.development.base_component import " "Component, _explicitize_args\n\n\n" ) diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index 10375c9fd7..81983cf436 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -60,7 +60,7 @@ def get_prop_typing(type_name: str, type_info): "exact": generate_shape, "string": generate_type("str"), "bool": generate_type("bool"), - "number": generate_type("typing.Union[float, int]"), + "number": generate_type("numbers.Number"), "node": generate_type( "typing.Union[str, int, float, Component," " typing.List[typing.Union" From 24a8e9089fa887067defe461c1daa7098ac8d4b1 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 31 Oct 2022 12:08:41 -0400 Subject: [PATCH 08/49] Fix test_generate_class_file --- dash/development/_py_components_generation.py | 2 ++ tests/unit/development/metadata_test.py | 13 +++++++------ tests/unit/development/test_generate_class_file.py | 4 +++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index 4a4f5148f2..8a01f61aa9 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -1,5 +1,6 @@ from collections import OrderedDict import copy +import numbers import os import typing from textwrap import fill, dedent @@ -271,6 +272,7 @@ def generate_class( "Component": Component, "_explicitize_args": _explicitize_args, "typing": typing, + "numbers": numbers, } # pylint: disable=exec-used exec(string, scope) diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index 4eadcf5b13..e85a8c8ab0 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -1,6 +1,7 @@ # AUTO GENERATED FILE - DO NOT EDIT import typing # noqa: F401 +import numbers # noqa: F401 from dash.development.base_component import Component, _explicitize_args @@ -98,7 +99,7 @@ def __init__( optionalArray: typing.List = Component.UNDEFINED, optionalBool: bool = Component.UNDEFINED, optionalFunc: typing.Any = Component.UNDEFINED, - optionalNumber: typing.Union[float, int] = Component.UNDEFINED, + optionalNumber: numbers.Number = Component.UNDEFINED, optionalObject: typing.Dict = Component.UNDEFINED, optionalString: str = Component.UNDEFINED, optionalSymbol: typing.Any = Component.UNDEFINED, @@ -106,11 +107,11 @@ def __init__( optionalElement: Component = Component.UNDEFINED, optionalMessage: typing.Any = Component.UNDEFINED, optionalEnum: typing.Any = Component.UNDEFINED, - optionalUnion: typing.Union[str, typing.Union[float, int], typing.Any] = Component.UNDEFINED, - optionalArrayOf: typing.List[typing.Union[float, int]] = Component.UNDEFINED, - optionalObjectOf: typing.Dict[str, typing.Union[float, int]] = Component.UNDEFINED, - optionalObjectWithExactAndNestedDescription: typing.Dict[str, typing.Union[str, typing.Union[float, int], typing.Dict[str, typing.Union[typing.List[typing.Dict], typing.Dict]]]] = Component.UNDEFINED, - optionalObjectWithShapeAndNestedDescription: typing.Dict[str, typing.Union[str, typing.Union[float, int], typing.Dict[str, typing.Union[typing.List[typing.Dict], typing.Dict]]]] = Component.UNDEFINED, + optionalUnion: typing.Union[str, numbers.Number, typing.Any] = Component.UNDEFINED, + optionalArrayOf: typing.List[numbers.Number] = Component.UNDEFINED, + optionalObjectOf: typing.Dict[typing.Union[str, float, int], numbers.Number] = Component.UNDEFINED, + optionalObjectWithExactAndNestedDescription: typing.Dict[str, typing.Union[str, numbers.Number, typing.Dict[str, typing.Union[typing.List[typing.Dict], typing.Dict]]]] = Component.UNDEFINED, + optionalObjectWithShapeAndNestedDescription: typing.Dict[str, typing.Union[str, numbers.Number, typing.Dict[str, typing.Union[typing.List[typing.Dict], typing.Dict]]]] = Component.UNDEFINED, optionalAny: typing.Any = Component.UNDEFINED, customProp: typing.Any = Component.UNDEFINED, customArrayProp: typing.List[typing.Any] = Component.UNDEFINED, diff --git a/tests/unit/development/test_generate_class_file.py b/tests/unit/development/test_generate_class_file.py index c3d496b43a..dc5c7f2fe5 100644 --- a/tests/unit/development/test_generate_class_file.py +++ b/tests/unit/development/test_generate_class_file.py @@ -13,7 +13,9 @@ # Import string not included in generated class string import_string = ( - "# AUTO GENERATED FILE - DO NOT EDIT\n\nimport typing # noqa: F401\n" + "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + "import typing # noqa: F401\n" + "import numbers # noqa: F401\n" + "from dash.development.base_component import" + " Component, _explicitize_args\n\n\n" ) From 1f4aed9b99acdd030d5116489f3471351911a371 Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 25 Nov 2022 15:17:44 -0500 Subject: [PATCH 09/49] Generate enums --- dash/development/_py_components_generation.py | 12 ++- dash/development/_py_prop_typing.py | 87 +++++++++++++++---- 2 files changed, 79 insertions(+), 20 deletions(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index 8a01f61aa9..4670376820 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -9,7 +9,7 @@ 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 +from ._py_prop_typing import get_prop_typing, enums from .base_component import Component @@ -57,6 +57,7 @@ def generate_class_string( _base_nodes = {base_nodes} _namespace = '{namespace}' _type = '{typename}' +{enums} @_explicitize_args def __init__( self, @@ -100,7 +101,9 @@ def __init__( prop_keys = list(props.keys()) if "children" in props and "children" in list_of_valid_keys: prop_keys.remove("children") - default_argtext = f"children: {get_prop_typing('node', {})} = None,\n " + default_argtext = ( + f"children: {get_prop_typing('node', '', '', {})} = None,\n " + ) args = "{k: _locals[k] for k in _explicit_args if k != 'children'}" argtext = "children=children, **args" else: @@ -146,7 +149,7 @@ def __init__( type_name = type_info.get("name") - typed = get_prop_typing(type_name, type_info) + typed = get_prop_typing(type_name, typename, prop_key, type_info) arg_value = f"{prop_key}: {typed} = {default_value}" @@ -180,6 +183,7 @@ def __init__( required_validation=required_validation, children_props=nodes, base_nodes=filter_base_nodes(nodes) + ["children"], + enums="\n".join(f" {k} = {k}" for k in enums.get(typename, {}).keys()), ) ) @@ -207,6 +211,7 @@ def generate_class_file( "# AUTO GENERATED FILE - DO NOT EDIT\n\n" "import typing # noqa: F401\n" "import numbers # noqa: F401\n" + "import enum # noqa: F401\n" "from dash.development.base_component import " "Component, _explicitize_args\n\n\n" ) @@ -219,6 +224,7 @@ def generate_class_file( file_path = os.path.join(namespace, file_name) with open(file_path, "w", encoding="utf-8") as f: f.write(import_string) + f.write("\n\n".join(enums.get(typename, {}).values())) f.write(class_string) print(f"Generated {file_name}") diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index 81983cf436..32af1dfcaf 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -1,12 +1,28 @@ -def generate_any(_): +import json + +import stringcase + +enums = {} +enum_template = """ +class {name}(enum.Enum): +{values} + + def to_plotly_json(self): + return self.value + +""" +enum_value_template = " {name} = {value}" + + +def generate_any(*_): return "typing.Any" -def generate_shape(t): +def generate_shape(type_info, component_name: str, prop_name: str): props = [] - for prop in t["value"].values(): - prop_type = PROP_TYPING.get(prop["name"], generate_any)(prop) + for prop in type_info["value"].values(): + prop_type = get_prop_typing(prop["name"], component_name, prop_name, prop) if prop_type not in props: props.append(prop_type) @@ -16,40 +32,77 @@ def generate_shape(t): return f"typing.Dict[str, typing.Union[{', '.join(props)}]]" -def generate_union(t): +def generate_union(type_info, component_name: str, prop_name: str): types = [] - for union in t["value"]: - u_type = PROP_TYPING.get(union["name"], generate_any)(union) + 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): +def generate_tuple( + type_info, + component_name: str, + prop_name: str, +): els = type_info.get("elements") - elements = ", ".join(get_prop_typing(x.get("name"), x) for x in els) + 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(t): - typed = PROP_TYPING.get(t["value"]["name"], generate_any)(t["value"]) +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.List[{typed}]" -def generate_object_of(t): - typed = PROP_TYPING.get(t["value"]["name"], generate_any)(t["value"]) +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(_): + def type_handler(*_): return typename return type_handler -def get_prop_typing(type_name: str, type_info): - return PROP_TYPING.get(type_name, generate_any)(type_info) +def generate_enum(type_info, component_name: str, prop_name: str): + name = stringcase.pascalcase(prop_name) + + values = [json.loads(v["value"].replace("'", '"')) for v in type_info["value"] if v] + value_type = type(values[0]).__name__ + + enums.setdefault(component_name, {}) + enums[component_name][name] = enum_template.format( + name=name, + values="\n".join( + enum_value_template.format( + name=str(x), + value=json.dumps(x), + ) + for x in values + ), + ) + + return f"typing.Union[{value_type}, {name}]" + + +def get_prop_typing(type_name: str, component_name: str, prop_name: str, type_info): + return PROP_TYPING.get(type_name, generate_any)( + type_info, component_name, prop_name + ) PROP_TYPING = { @@ -71,7 +124,7 @@ def get_prop_typing(type_name: str, type_info): "union": generate_union, "any": generate_any, "custom": generate_any, - "enum": generate_any, + "enum": generate_enum, "objectOf": generate_object_of, "tuple": generate_tuple, } From 88ca3f3670172ad2ff386896a6e0d29a7faf002c Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 25 Nov 2022 16:15:29 -0500 Subject: [PATCH 10/49] Generate typed dicts for shapes. --- dash/development/_py_components_generation.py | 6 ++- dash/development/_py_prop_typing.py | 53 +++++++++++++++---- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index 4670376820..6e6f308ada 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -9,7 +9,7 @@ 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, enums +from ._py_prop_typing import get_prop_typing, enums, shapes from .base_component import Component @@ -58,6 +58,7 @@ def generate_class_string( _namespace = '{namespace}' _type = '{typename}' {enums} +{shapes} @_explicitize_args def __init__( self, @@ -184,6 +185,7 @@ def __init__( children_props=nodes, base_nodes=filter_base_nodes(nodes) + ["children"], enums="\n".join(f" {k} = {k}" for k in enums.get(typename, {}).keys()), + shapes="\n".join(f" {k} = {k}" for k in shapes.get(typename, {}).keys()), ) ) @@ -212,6 +214,7 @@ def generate_class_file( "import typing # noqa: F401\n" "import numbers # noqa: F401\n" "import enum # noqa: F401\n" + "from typing_extensions import TypedDict, NotRequired # noqa: F401\n" "from dash.development.base_component import " "Component, _explicitize_args\n\n\n" ) @@ -225,6 +228,7 @@ def generate_class_file( with open(file_path, "w", encoding="utf-8") as f: f.write(import_string) f.write("\n\n".join(enums.get(typename, {}).values())) + f.write("\n\n".join(shapes.get(typename, {}).values())) f.write(class_string) print(f"Generated {file_name}") diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index 32af1dfcaf..4af724db23 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -1,7 +1,9 @@ import json +import string import stringcase + enums = {} enum_template = """ class {name}(enum.Enum): @@ -14,22 +16,51 @@ def to_plotly_json(self): enum_value_template = " {name} = {value}" +shapes = {} +shape_template = """ +class {name}(TypedDict): +{values} + +""" + + +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 in type_info["value"].values(): - prop_type = get_prop_typing(prop["name"], component_name, prop_name, prop) - if prop_type not in props: - props.append(prop_type) - - if len(props) == 0: - return "typing.Any" + for prop_key, prop_type in type_info["value"].items(): + if prop_key != _clean_key(prop_key): + # Got invalid keys + return "dict" + + 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] = shape_template.format( + name=name, values="\n".join(props) + ) - return f"typing.Dict[str, typing.Union[{', '.join(props)}]]" + return name def generate_union(type_info, component_name: str, prop_name: str): @@ -89,8 +120,8 @@ def generate_enum(type_info, component_name: str, prop_name: str): name=name, values="\n".join( enum_value_template.format( - name=str(x), - value=json.dumps(x), + name=f"_{x}" if not isinstance(x, str) else _clean_key(x), + value=json.dumps(x) if not isinstance(x, bool) else str(x), ) for x in values ), @@ -108,7 +139,7 @@ def get_prop_typing(type_name: str, component_name: str, prop_name: str, type_in PROP_TYPING = { "array": generate_type("typing.List"), "arrayOf": generate_array_of, - "object": generate_type("typing.Dict"), + "object": generate_type("dict"), "shape": generate_shape, "exact": generate_shape, "string": generate_type("str"), From c18f0fd396e54c18943e21a1b9af51a888a764ed Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 29 Nov 2022 11:58:50 -0500 Subject: [PATCH 11/49] Add stringcase & typing_extensions to requires. --- requires-dev.txt | 1 + requires-install.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requires-dev.txt b/requires-dev.txt index 22a770dca3..8e5610b5b3 100644 --- a/requires-dev.txt +++ b/requires-dev.txt @@ -2,3 +2,4 @@ coloredlogs>=15.0.1 fire>=0.4.0 PyYAML>=5.4.1 +stringcase>=1.2.0 diff --git a/requires-install.txt b/requires-install.txt index edcb71441b..f072c896d5 100644 --- a/requires-install.txt +++ b/requires-install.txt @@ -5,3 +5,4 @@ dash_core_components==2.0.0 dash_table==5.0.0 importlib-metadata==4.8.3;python_version<"3.7" contextvars==2.4;python_version<"3.7" +typing_extensions>=4.2.0 From 544dcab6900f8ae4f9fd0f9788096f882b6e69d3 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 29 Nov 2022 13:31:50 -0500 Subject: [PATCH 12/49] Fix whitespace around extra types & runtime classes. --- dash/development/_py_components_generation.py | 11 ++++++++--- dash/development/_py_prop_typing.py | 8 ++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index 6e6f308ada..bb2f00ccbf 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -1,10 +1,12 @@ from collections import OrderedDict import copy +import enum import numbers import os import typing from textwrap import fill, dedent +from typing_extensions import TypedDict, NotRequired from dash.development.base_component import _explicitize_args from dash.exceptions import NonExistentEventException from ._all_keywords import python_keywords @@ -51,7 +53,7 @@ def generate_class_string( # it to be `null` or whether that was just the default value. # The solution might be to deal with default values better although # not all component authors will supply those. - c = '''class {typename}(Component): + c = '''{extra_types}class {typename}(Component): """{docstring}""" _children_props = {children_props} _base_nodes = {base_nodes} @@ -184,6 +186,8 @@ def __init__( required_validation=required_validation, children_props=nodes, base_nodes=filter_base_nodes(nodes) + ["children"], + extra_types="".join(enums.get(typename, {}).values()) + + "".join(shapes.get(typename, {}).values()), enums="\n".join(f" {k} = {k}" for k in enums.get(typename, {}).keys()), shapes="\n".join(f" {k} = {k}" for k in shapes.get(typename, {}).keys()), ) @@ -227,8 +231,6 @@ def generate_class_file( file_path = os.path.join(namespace, file_name) with open(file_path, "w", encoding="utf-8") as f: f.write(import_string) - f.write("\n\n".join(enums.get(typename, {}).values())) - f.write("\n\n".join(shapes.get(typename, {}).values())) f.write(class_string) print(f"Generated {file_name}") @@ -283,6 +285,9 @@ def generate_class( "_explicitize_args": _explicitize_args, "typing": typing, "numbers": numbers, + "enum": enum, + "TypedDict": TypedDict, + "NotRequired": NotRequired, } # pylint: disable=exec-used exec(string, scope) diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index 4af724db23..4fdf7b0819 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -5,22 +5,22 @@ enums = {} -enum_template = """ -class {name}(enum.Enum): +enum_template = """class {name}(enum.Enum): {values} def to_plotly_json(self): return self.value + """ enum_value_template = " {name} = {value}" shapes = {} -shape_template = """ -class {name}(TypedDict): +shape_template = """class {name}(TypedDict): {values} + """ From 91a2a1d08877b842834770067bed8c58b581f0b0 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 29 Nov 2022 13:34:05 -0500 Subject: [PATCH 13/49] Fix metadata test --- tests/unit/development/metadata_test.py | 45 +++++++++++++++++-- .../development/test_generate_class_file.py | 6 ++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index e85a8c8ab0..1c64c0fab4 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -2,9 +2,41 @@ import typing # noqa: F401 import numbers # noqa: F401 +import enum # noqa: F401 +from typing_extensions import TypedDict, NotRequired # noqa: F401 from dash.development.base_component import Component, _explicitize_args +class OptionalEnum(enum.Enum): + News = "News" + Photos = "Photos" + + def to_plotly_json(self): + return self.value + + +class OptionalObjectWithExactAndNestedDescriptionFigure(TypedDict): + data: NotRequired[typing.List[dict]] + layout: NotRequired[dict] + + +class OptionalObjectWithExactAndNestedDescription(TypedDict): + color: NotRequired[str] + fontSize: NotRequired[numbers.Number] + figure: NotRequired[OptionalObjectWithExactAndNestedDescriptionFigure] + + +class OptionalObjectWithShapeAndNestedDescriptionFigure(TypedDict): + data: NotRequired[typing.List[dict]] + layout: NotRequired[dict] + + +class OptionalObjectWithShapeAndNestedDescription(TypedDict): + color: NotRequired[str] + fontSize: NotRequired[numbers.Number] + figure: NotRequired[OptionalObjectWithShapeAndNestedDescriptionFigure] + + class Table(Component): """A Table component. This is a description of the component. @@ -92,6 +124,11 @@ class Table(Component): _base_nodes = ['optionalNode', 'optionalElement', 'children'] _namespace = 'TableComponents' _type = 'Table' + OptionalEnum = OptionalEnum + OptionalObjectWithExactAndNestedDescriptionFigure = OptionalObjectWithExactAndNestedDescriptionFigure + OptionalObjectWithExactAndNestedDescription = OptionalObjectWithExactAndNestedDescription + OptionalObjectWithShapeAndNestedDescriptionFigure = OptionalObjectWithShapeAndNestedDescriptionFigure + OptionalObjectWithShapeAndNestedDescription = OptionalObjectWithShapeAndNestedDescription @_explicitize_args def __init__( self, @@ -100,18 +137,18 @@ def __init__( optionalBool: bool = Component.UNDEFINED, optionalFunc: typing.Any = Component.UNDEFINED, optionalNumber: numbers.Number = Component.UNDEFINED, - optionalObject: typing.Dict = Component.UNDEFINED, + optionalObject: dict = Component.UNDEFINED, optionalString: str = Component.UNDEFINED, optionalSymbol: typing.Any = Component.UNDEFINED, optionalNode: typing.Union[str, int, float, Component, typing.List[typing.Union[str, int, float, Component]]] = Component.UNDEFINED, optionalElement: Component = Component.UNDEFINED, optionalMessage: typing.Any = Component.UNDEFINED, - optionalEnum: typing.Any = Component.UNDEFINED, + optionalEnum: typing.Union[str, OptionalEnum] = Component.UNDEFINED, optionalUnion: typing.Union[str, numbers.Number, typing.Any] = Component.UNDEFINED, optionalArrayOf: typing.List[numbers.Number] = Component.UNDEFINED, optionalObjectOf: typing.Dict[typing.Union[str, float, int], numbers.Number] = Component.UNDEFINED, - optionalObjectWithExactAndNestedDescription: typing.Dict[str, typing.Union[str, numbers.Number, typing.Dict[str, typing.Union[typing.List[typing.Dict], typing.Dict]]]] = Component.UNDEFINED, - optionalObjectWithShapeAndNestedDescription: typing.Dict[str, typing.Union[str, numbers.Number, typing.Dict[str, typing.Union[typing.List[typing.Dict], typing.Dict]]]] = Component.UNDEFINED, + optionalObjectWithExactAndNestedDescription: OptionalObjectWithExactAndNestedDescription = Component.UNDEFINED, + optionalObjectWithShapeAndNestedDescription: OptionalObjectWithShapeAndNestedDescription = Component.UNDEFINED, optionalAny: typing.Any = Component.UNDEFINED, customProp: typing.Any = Component.UNDEFINED, customArrayProp: typing.List[typing.Any] = Component.UNDEFINED, diff --git a/tests/unit/development/test_generate_class_file.py b/tests/unit/development/test_generate_class_file.py index dc5c7f2fe5..9587e0c6bc 100644 --- a/tests/unit/development/test_generate_class_file.py +++ b/tests/unit/development/test_generate_class_file.py @@ -16,8 +16,10 @@ "# AUTO GENERATED FILE - DO NOT EDIT\n\n" "import typing # noqa: F401\n" "import numbers # noqa: F401\n" - + "from dash.development.base_component import" - + " Component, _explicitize_args\n\n\n" + "import enum # noqa: F401\n" + "from typing_extensions import TypedDict, NotRequired # noqa: F401\n" + "from dash.development.base_component import " + "Component, _explicitize_args\n\n\n" ) From ed360e1639eb5c5033f531128d659eb35f85a698 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 29 Nov 2022 14:46:01 -0500 Subject: [PATCH 14/49] Lock version of typing_extensions for 3.6 --- requires-install.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requires-install.txt b/requires-install.txt index f072c896d5..1deae00970 100644 --- a/requires-install.txt +++ b/requires-install.txt @@ -5,4 +5,5 @@ dash_core_components==2.0.0 dash_table==5.0.0 importlib-metadata==4.8.3;python_version<"3.7" contextvars==2.4;python_version<"3.7" -typing_extensions>=4.2.0 +typing_extensions==4.1.1;python_version<"3.7" +typing_extensions>=4.4.0;python_version>"3.6" From b3061a498adc5a487bb6205371b66948d4e97b60 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 29 Nov 2022 15:08:56 -0500 Subject: [PATCH 15/49] Fix keyword in enum & shapes, NoneType in enum + add enum suffix. --- dash/development/_py_prop_typing.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index 4fdf7b0819..756b81f286 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -3,6 +3,7 @@ import stringcase +from ._all_keywords import python_keywords enums = {} enum_template = """class {name}(enum.Enum): @@ -43,7 +44,7 @@ def generate_shape(type_info, component_name: str, prop_name: str): name = stringcase.pascalcase(prop_name) for prop_key, prop_type in type_info["value"].items(): - if prop_key != _clean_key(prop_key): + if prop_key in python_keywords or prop_key != _clean_key(prop_key): # Got invalid keys return "dict" @@ -110,20 +111,28 @@ def type_handler(*_): def generate_enum(type_info, component_name: str, prop_name: str): - name = stringcase.pascalcase(prop_name) + name = stringcase.pascalcase(prop_name) + "Enum" values = [json.loads(v["value"].replace("'", '"')) for v in type_info["value"] if v] - value_type = type(values[0]).__name__ + types = [type(v).__name__ for v in values] + value_type = ", ".join(v for v in types if v != "NoneType") + + if not value_type: + # FIXME tooltip_header type in dash table only got null. + return "typing.Any" enums.setdefault(component_name, {}) enums[component_name][name] = enum_template.format( name=name, values="\n".join( enum_value_template.format( - name=f"_{x}" if not isinstance(x, str) else _clean_key(x), + name=f"_{x}" + if not isinstance(x, str) or x in python_keywords + else _clean_key(x), value=json.dumps(x) if not isinstance(x, bool) else str(x), ) for x in values + if x is not None ), ) From a4ea22e79f7a7e409797a1238b004afa23593949 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 29 Nov 2022 15:58:30 -0500 Subject: [PATCH 16/49] Fix metadata test --- tests/unit/development/metadata_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index 1c64c0fab4..2cf887f29d 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -7,7 +7,7 @@ from dash.development.base_component import Component, _explicitize_args -class OptionalEnum(enum.Enum): +class OptionalEnumEnum(enum.Enum): News = "News" Photos = "Photos" @@ -124,7 +124,7 @@ class Table(Component): _base_nodes = ['optionalNode', 'optionalElement', 'children'] _namespace = 'TableComponents' _type = 'Table' - OptionalEnum = OptionalEnum + OptionalEnumEnum = OptionalEnumEnum OptionalObjectWithExactAndNestedDescriptionFigure = OptionalObjectWithExactAndNestedDescriptionFigure OptionalObjectWithExactAndNestedDescription = OptionalObjectWithExactAndNestedDescription OptionalObjectWithShapeAndNestedDescriptionFigure = OptionalObjectWithShapeAndNestedDescriptionFigure @@ -143,7 +143,7 @@ def __init__( optionalNode: typing.Union[str, int, float, Component, typing.List[typing.Union[str, int, float, Component]]] = Component.UNDEFINED, optionalElement: Component = Component.UNDEFINED, optionalMessage: typing.Any = Component.UNDEFINED, - optionalEnum: typing.Union[str, OptionalEnum] = Component.UNDEFINED, + optionalEnum: typing.Union[str, str, OptionalEnumEnum] = Component.UNDEFINED, optionalUnion: typing.Union[str, numbers.Number, typing.Any] = Component.UNDEFINED, optionalArrayOf: typing.List[numbers.Number] = Component.UNDEFINED, optionalObjectOf: typing.Dict[typing.Union[str, float, int], numbers.Number] = Component.UNDEFINED, From 8029f15c62baee7bb2771018ffd4d9e7086f4ddd Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 29 Nov 2022 16:28:31 -0500 Subject: [PATCH 17/49] Fix test_attrs_match_forbidden_props --- tests/unit/development/test_generate_class.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/unit/development/test_generate_class.py b/tests/unit/development/test_generate_class.py index 2c7c0de494..462416ea0a 100644 --- a/tests/unit/development/test_generate_class.py +++ b/tests/unit/development/test_generate_class.py @@ -128,6 +128,13 @@ 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({ + "OptionalEnumEnum", + "OptionalObjectWithExactAndNestedDescription", + "OptionalObjectWithExactAndNestedDescriptionFigure", + "OptionalObjectWithShapeAndNestedDescription", + "OptionalObjectWithShapeAndNestedDescriptionFigure", + }) c = component_class() base_attrs = set(dir(c)) extra_attrs = set(a for a in base_attrs if a[0] != "_") From b0ed806c77ecc3344ab23848f410c7f77ffff89a Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 30 Nov 2022 10:48:21 -0500 Subject: [PATCH 18/49] Add back base numbers. --- dash/development/_py_prop_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index 756b81f286..fcdcee5bc4 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -153,7 +153,7 @@ def get_prop_typing(type_name: str, component_name: str, prop_name: str, type_in "exact": generate_shape, "string": generate_type("str"), "bool": generate_type("bool"), - "number": generate_type("numbers.Number"), + "number": generate_type("typing.Union[int, float, numbers.Number]"), "node": generate_type( "typing.Union[str, int, float, Component," " typing.List[typing.Union" From ae345e02e93197371f10fe7ee11c1a5cb58bb1de Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 30 Nov 2022 10:50:42 -0500 Subject: [PATCH 19/49] Fix tests --- tests/integration/test_typing.py | 10 ++++++---- tests/unit/development/metadata_test.py | 12 ++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/integration/test_typing.py b/tests/integration/test_typing.py index 1a7257ac7b..f7d9a01b4d 100644 --- a/tests/integration/test_typing.py +++ b/tests/integration/test_typing.py @@ -69,7 +69,8 @@ def assert_pyright_output( { "expected_status": 1, "expected_outputs": [ - 'Argument of type "Literal[\'\']" cannot be assigned to parameter "a_number" of type "float | int"' + 'Argument of type "Literal[\'\']" cannot be assigned to parameter "a_number" ' + 'of type "int | float | Number"' ], }, ), @@ -102,7 +103,8 @@ def assert_pyright_output( { "expected_status": 1, "expected_outputs": [ - 'Argument of type "dict[Any, Any]" cannot be assigned to parameter "array_string" of type "List[str]"' + 'Argument of type "dict[Any, Any]" cannot be assigned to parameter "array_string" ' + 'of type "List[str]"' ], }, ), @@ -131,7 +133,7 @@ def assert_pyright_output( }, ), ( - "array_obj=[{}]", + "array_obj=[{'a': 'b'}]", { "expected_status": 0, }, @@ -202,7 +204,7 @@ def assert_pyright_output( "expected_status": 1, "expected_outputs": [ 'Argument of type "tuple[Literal[1], Literal[2]]" cannot be assigned ' - 'to parameter "a_tuple" of type "Tuple[float | int, str]"' + 'to parameter "a_tuple" of type "Tuple[int | float | Number, str]"' ], }, ), diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index 2cf887f29d..dc56114562 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -22,7 +22,7 @@ class OptionalObjectWithExactAndNestedDescriptionFigure(TypedDict): class OptionalObjectWithExactAndNestedDescription(TypedDict): color: NotRequired[str] - fontSize: NotRequired[numbers.Number] + fontSize: NotRequired[typing.Union[int, float, numbers.Number]] figure: NotRequired[OptionalObjectWithExactAndNestedDescriptionFigure] @@ -33,7 +33,7 @@ class OptionalObjectWithShapeAndNestedDescriptionFigure(TypedDict): class OptionalObjectWithShapeAndNestedDescription(TypedDict): color: NotRequired[str] - fontSize: NotRequired[numbers.Number] + fontSize: NotRequired[typing.Union[int, float, numbers.Number]] figure: NotRequired[OptionalObjectWithShapeAndNestedDescriptionFigure] @@ -136,7 +136,7 @@ def __init__( optionalArray: typing.List = Component.UNDEFINED, optionalBool: bool = Component.UNDEFINED, optionalFunc: typing.Any = Component.UNDEFINED, - optionalNumber: numbers.Number = Component.UNDEFINED, + optionalNumber: typing.Union[int, float, numbers.Number] = Component.UNDEFINED, optionalObject: dict = Component.UNDEFINED, optionalString: str = Component.UNDEFINED, optionalSymbol: typing.Any = Component.UNDEFINED, @@ -144,9 +144,9 @@ def __init__( optionalElement: Component = Component.UNDEFINED, optionalMessage: typing.Any = Component.UNDEFINED, optionalEnum: typing.Union[str, str, OptionalEnumEnum] = Component.UNDEFINED, - optionalUnion: typing.Union[str, numbers.Number, typing.Any] = Component.UNDEFINED, - optionalArrayOf: typing.List[numbers.Number] = Component.UNDEFINED, - optionalObjectOf: typing.Dict[typing.Union[str, float, int], numbers.Number] = Component.UNDEFINED, + optionalUnion: typing.Union[str, typing.Union[int, float, numbers.Number], typing.Any] = Component.UNDEFINED, + optionalArrayOf: typing.List[typing.Union[int, float, numbers.Number]] = Component.UNDEFINED, + optionalObjectOf: typing.Dict[typing.Union[str, float, int], typing.Union[int, float, numbers.Number]] = Component.UNDEFINED, optionalObjectWithExactAndNestedDescription: OptionalObjectWithExactAndNestedDescription = Component.UNDEFINED, optionalObjectWithShapeAndNestedDescription: OptionalObjectWithShapeAndNestedDescription = Component.UNDEFINED, optionalAny: typing.Any = Component.UNDEFINED, From d7136e3106895b83243be0302e57e32e2efbafc5 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 30 Nov 2022 11:01:42 -0500 Subject: [PATCH 20/49] Add pyright test cases for shapes --- tests/integration/test_typing.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/integration/test_typing.py b/tests/integration/test_typing.py index f7d9a01b4d..be38575d2a 100644 --- a/tests/integration/test_typing.py +++ b/tests/integration/test_typing.py @@ -214,6 +214,36 @@ def assert_pyright_output( "expected_status": 0, }, ), + ( + "obj=set()", + { + "expected_status": 1, + } + ), + ( + "obj={}", + { + "expected_status": 1, + "expected_outputs": [ + '"value" is required in "Obj"' + ] + } + ), + ( + "obj={'value': 'a', 'label': 1}", + { + "expected_status": 1, + "expected_outputs": [ + '"Literal[1]" is incompatible with "str"' + ] + } + ), + ( + "obj={'value': 'a', 'label': 'lab'}", + { + "expected_status": 0, + } + ) ], ) def test_component_typing(arguments, assertions, tmp_path): From 8e0dfea0078579a2a97a84153623f185adbe9b8e Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 27 Apr 2023 13:09:37 -0400 Subject: [PATCH 21/49] Fix enum duplicate union type. --- dash/development/_py_prop_typing.py | 3 ++- tests/unit/development/metadata_test.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index fcdcee5bc4..8ce071413a 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -4,6 +4,7 @@ import stringcase from ._all_keywords import python_keywords +from .._utils import OrderedSet enums = {} enum_template = """class {name}(enum.Enum): @@ -114,7 +115,7 @@ def generate_enum(type_info, component_name: str, prop_name: str): name = stringcase.pascalcase(prop_name) + "Enum" values = [json.loads(v["value"].replace("'", '"')) for v in type_info["value"] if v] - types = [type(v).__name__ for v in values] + types = OrderedSet(*[type(v).__name__ for v in values]) value_type = ", ".join(v for v in types if v != "NoneType") if not value_type: diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index dc56114562..8f51bf9a82 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -143,7 +143,7 @@ def __init__( optionalNode: typing.Union[str, int, float, Component, typing.List[typing.Union[str, int, float, Component]]] = Component.UNDEFINED, optionalElement: Component = Component.UNDEFINED, optionalMessage: typing.Any = Component.UNDEFINED, - optionalEnum: typing.Union[str, str, OptionalEnumEnum] = Component.UNDEFINED, + optionalEnum: typing.Union[str, OptionalEnumEnum] = Component.UNDEFINED, optionalUnion: typing.Union[str, typing.Union[int, float, numbers.Number], typing.Any] = Component.UNDEFINED, optionalArrayOf: typing.List[typing.Union[int, float, numbers.Number]] = Component.UNDEFINED, optionalObjectOf: typing.Dict[typing.Union[str, float, int], typing.Union[int, float, numbers.Number]] = Component.UNDEFINED, From 453834bbd0083d66d73752ca5d7c25b30d3583cc Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 27 Apr 2023 13:36:40 -0400 Subject: [PATCH 22/49] Add tuple to untyped array typing union. --- dash/development/_py_prop_typing.py | 2 +- tests/unit/development/metadata_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index 8ce071413a..4288d91d24 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -147,7 +147,7 @@ def get_prop_typing(type_name: str, component_name: str, prop_name: str, type_in PROP_TYPING = { - "array": generate_type("typing.List"), + "array": generate_type("typing.Union[typing.List, typing.Tuple]"), "arrayOf": generate_array_of, "object": generate_type("dict"), "shape": generate_shape, diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index 8f51bf9a82..a174a7a998 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -133,7 +133,7 @@ class Table(Component): def __init__( self, children: typing.Union[str, int, float, Component, typing.List[typing.Union[str, int, float, Component]]] = None, - optionalArray: typing.List = Component.UNDEFINED, + optionalArray: typing.Union[typing.List, typing.Tuple] = Component.UNDEFINED, optionalBool: bool = Component.UNDEFINED, optionalFunc: typing.Any = Component.UNDEFINED, optionalNumber: typing.Union[int, float, numbers.Number] = Component.UNDEFINED, From 13ff2965687b0b860fb85d7886ad479772c4db73 Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 28 Apr 2023 13:22:50 -0400 Subject: [PATCH 23/49] Add basic tuple as union of arrayOf props typing. --- dash/development/_py_prop_typing.py | 2 +- tests/unit/development/metadata_test.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index 4288d91d24..f233a94e34 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -94,7 +94,7 @@ def generate_array_of( typed = get_prop_typing( type_info["value"]["name"], component_name, prop_name, type_info["value"] ) - return f"typing.List[{typed}]" + return f"typing.Union[typing.List[{typed}], typing.Tuple]" def generate_object_of(type_info, component_name: str, prop_name: str): diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index a174a7a998..5d1ac57816 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -16,7 +16,7 @@ def to_plotly_json(self): class OptionalObjectWithExactAndNestedDescriptionFigure(TypedDict): - data: NotRequired[typing.List[dict]] + data: NotRequired[typing.Union[typing.List[dict], typing.Tuple]] layout: NotRequired[dict] @@ -27,7 +27,7 @@ class OptionalObjectWithExactAndNestedDescription(TypedDict): class OptionalObjectWithShapeAndNestedDescriptionFigure(TypedDict): - data: NotRequired[typing.List[dict]] + data: NotRequired[typing.Union[typing.List[dict], typing.Tuple]] layout: NotRequired[dict] @@ -145,13 +145,13 @@ def __init__( optionalMessage: typing.Any = Component.UNDEFINED, optionalEnum: typing.Union[str, OptionalEnumEnum] = Component.UNDEFINED, optionalUnion: typing.Union[str, typing.Union[int, float, numbers.Number], typing.Any] = Component.UNDEFINED, - optionalArrayOf: typing.List[typing.Union[int, float, numbers.Number]] = Component.UNDEFINED, + optionalArrayOf: typing.Union[typing.List[typing.Union[int, float, numbers.Number]], typing.Tuple] = Component.UNDEFINED, optionalObjectOf: typing.Dict[typing.Union[str, float, int], typing.Union[int, float, numbers.Number]] = Component.UNDEFINED, optionalObjectWithExactAndNestedDescription: OptionalObjectWithExactAndNestedDescription = Component.UNDEFINED, optionalObjectWithShapeAndNestedDescription: OptionalObjectWithShapeAndNestedDescription = Component.UNDEFINED, optionalAny: typing.Any = Component.UNDEFINED, customProp: typing.Any = Component.UNDEFINED, - customArrayProp: typing.List[typing.Any] = Component.UNDEFINED, + customArrayProp: typing.Union[typing.List[typing.Any], typing.Tuple] = Component.UNDEFINED, id: str = Component.UNDEFINED, **kwargs ): From 279950c0830b94709265f8e31deb96e273734d8d Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 1 May 2023 12:00:44 -0400 Subject: [PATCH 24/49] Fix typing tests. --- tests/integration/test_typing.py | 20 ++++++++----------- tests/unit/development/test_generate_class.py | 16 ++++++++------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/tests/integration/test_typing.py b/tests/integration/test_typing.py index be38575d2a..360a85c688 100644 --- a/tests/integration/test_typing.py +++ b/tests/integration/test_typing.py @@ -104,7 +104,7 @@ def assert_pyright_output( "expected_status": 1, "expected_outputs": [ 'Argument of type "dict[Any, Any]" cannot be assigned to parameter "array_string" ' - 'of type "List[str]"' + 'of type "List[str] | Tuple[Unknown, ...]"' ], }, ), @@ -218,32 +218,28 @@ def assert_pyright_output( "obj=set()", { "expected_status": 1, - } + }, ), ( "obj={}", { "expected_status": 1, - "expected_outputs": [ - '"value" is required in "Obj"' - ] - } + "expected_outputs": ['"value" is required in "Obj"'], + }, ), ( "obj={'value': 'a', 'label': 1}", { "expected_status": 1, - "expected_outputs": [ - '"Literal[1]" is incompatible with "str"' - ] - } + "expected_outputs": ['"Literal[1]" is incompatible with "str"'], + }, ), ( "obj={'value': 'a', 'label': 'lab'}", { "expected_status": 0, - } - ) + }, + ), ], ) def test_component_typing(arguments, assertions, tmp_path): diff --git a/tests/unit/development/test_generate_class.py b/tests/unit/development/test_generate_class.py index 462416ea0a..64500daae0 100644 --- a/tests/unit/development/test_generate_class.py +++ b/tests/unit/development/test_generate_class.py @@ -128,13 +128,15 @@ 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({ - "OptionalEnumEnum", - "OptionalObjectWithExactAndNestedDescription", - "OptionalObjectWithExactAndNestedDescriptionFigure", - "OptionalObjectWithShapeAndNestedDescription", - "OptionalObjectWithShapeAndNestedDescriptionFigure", - }) + expected_attrs.update( + { + "OptionalEnumEnum", + "OptionalObjectWithExactAndNestedDescription", + "OptionalObjectWithExactAndNestedDescriptionFigure", + "OptionalObjectWithShapeAndNestedDescription", + "OptionalObjectWithShapeAndNestedDescriptionFigure", + } + ) c = component_class() base_attrs = set(dir(c)) extra_attrs = set(a for a in base_attrs if a[0] != "_") From e6843fdae15e71028d751da54d06b5d9021f9b02 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 1 May 2023 17:02:46 -0400 Subject: [PATCH 25/49] Replace enum generation with Literal --- dash/development/_py_components_generation.py | 15 ++---- dash/development/_py_prop_typing.py | 54 ++++++------------- tests/unit/development/metadata_test.py | 14 +---- .../development/test_generate_class_file.py | 3 +- 4 files changed, 24 insertions(+), 62 deletions(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index bb2f00ccbf..cec95fd748 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -1,17 +1,16 @@ from collections import OrderedDict import copy -import enum import numbers import os import typing from textwrap import fill, dedent -from typing_extensions import TypedDict, NotRequired +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 ._py_prop_typing import get_prop_typing, enums, shapes +from ._py_prop_typing import get_prop_typing, shapes from .base_component import Component @@ -59,7 +58,6 @@ def generate_class_string( _base_nodes = {base_nodes} _namespace = '{namespace}' _type = '{typename}' -{enums} {shapes} @_explicitize_args def __init__( @@ -186,9 +184,7 @@ def __init__( required_validation=required_validation, children_props=nodes, base_nodes=filter_base_nodes(nodes) + ["children"], - extra_types="".join(enums.get(typename, {}).values()) - + "".join(shapes.get(typename, {}).values()), - enums="\n".join(f" {k} = {k}" for k in enums.get(typename, {}).keys()), + extra_types="".join(shapes.get(typename, {}).values()), shapes="\n".join(f" {k} = {k}" for k in shapes.get(typename, {}).keys()), ) ) @@ -217,8 +213,7 @@ def generate_class_file( "# AUTO GENERATED FILE - DO NOT EDIT\n\n" "import typing # noqa: F401\n" "import numbers # noqa: F401\n" - "import enum # noqa: F401\n" - "from typing_extensions import TypedDict, NotRequired # noqa: F401\n" + "from typing_extensions import TypedDict, NotRequired, Literal # noqa: F401\n" "from dash.development.base_component import " "Component, _explicitize_args\n\n\n" ) @@ -285,9 +280,9 @@ def generate_class( "_explicitize_args": _explicitize_args, "typing": typing, "numbers": numbers, - "enum": enum, "TypedDict": TypedDict, "NotRequired": NotRequired, + "Literal": Literal, } # pylint: disable=exec-used exec(string, scope) diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index f233a94e34..afdcafbaa6 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -4,18 +4,6 @@ import stringcase from ._all_keywords import python_keywords -from .._utils import OrderedSet - -enums = {} -enum_template = """class {name}(enum.Enum): -{values} - - def to_plotly_json(self): - return self.value - - -""" -enum_value_template = " {name} = {value}" shapes = {} @@ -111,33 +99,23 @@ def type_handler(*_): return type_handler -def generate_enum(type_info, component_name: str, prop_name: str): - name = stringcase.pascalcase(prop_name) + "Enum" - - values = [json.loads(v["value"].replace("'", '"')) for v in type_info["value"] if v] - types = OrderedSet(*[type(v).__name__ for v in values]) - value_type = ", ".join(v for v in types if v != "NoneType") - - if not value_type: - # FIXME tooltip_header type in dash table only got null. - return "typing.Any" - - enums.setdefault(component_name, {}) - enums[component_name][name] = enum_template.format( - name=name, - values="\n".join( - enum_value_template.format( - name=f"_{x}" - if not isinstance(x, str) or x in python_keywords - else _clean_key(x), - value=json.dumps(x) if not isinstance(x, bool) else str(x), - ) - for x in values - if x is not None - ), - ) +def _get_literal_value(value): + if value is None: + return "None" + + if isinstance(value, bool): + return str(value) + + return json.dumps(value) + - return f"typing.Union[{value_type}, {name}]" +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): diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index 5d1ac57816..72bf185d2e 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -2,19 +2,10 @@ import typing # noqa: F401 import numbers # noqa: F401 -import enum # noqa: F401 -from typing_extensions import TypedDict, NotRequired # noqa: F401 +from typing_extensions import TypedDict, NotRequired, Literal # noqa: F401 from dash.development.base_component import Component, _explicitize_args -class OptionalEnumEnum(enum.Enum): - News = "News" - Photos = "Photos" - - def to_plotly_json(self): - return self.value - - class OptionalObjectWithExactAndNestedDescriptionFigure(TypedDict): data: NotRequired[typing.Union[typing.List[dict], typing.Tuple]] layout: NotRequired[dict] @@ -124,7 +115,6 @@ class Table(Component): _base_nodes = ['optionalNode', 'optionalElement', 'children'] _namespace = 'TableComponents' _type = 'Table' - OptionalEnumEnum = OptionalEnumEnum OptionalObjectWithExactAndNestedDescriptionFigure = OptionalObjectWithExactAndNestedDescriptionFigure OptionalObjectWithExactAndNestedDescription = OptionalObjectWithExactAndNestedDescription OptionalObjectWithShapeAndNestedDescriptionFigure = OptionalObjectWithShapeAndNestedDescriptionFigure @@ -143,7 +133,7 @@ def __init__( optionalNode: typing.Union[str, int, float, Component, typing.List[typing.Union[str, int, float, Component]]] = Component.UNDEFINED, optionalElement: Component = Component.UNDEFINED, optionalMessage: typing.Any = Component.UNDEFINED, - optionalEnum: typing.Union[str, OptionalEnumEnum] = Component.UNDEFINED, + optionalEnum: Literal["News", "Photos"] = Component.UNDEFINED, optionalUnion: typing.Union[str, typing.Union[int, float, numbers.Number], typing.Any] = Component.UNDEFINED, optionalArrayOf: typing.Union[typing.List[typing.Union[int, float, numbers.Number]], typing.Tuple] = Component.UNDEFINED, optionalObjectOf: typing.Dict[typing.Union[str, float, int], typing.Union[int, float, numbers.Number]] = Component.UNDEFINED, diff --git a/tests/unit/development/test_generate_class_file.py b/tests/unit/development/test_generate_class_file.py index 9587e0c6bc..a84ca579c2 100644 --- a/tests/unit/development/test_generate_class_file.py +++ b/tests/unit/development/test_generate_class_file.py @@ -16,8 +16,7 @@ "# AUTO GENERATED FILE - DO NOT EDIT\n\n" "import typing # noqa: F401\n" "import numbers # noqa: F401\n" - "import enum # noqa: F401\n" - "from typing_extensions import TypedDict, NotRequired # noqa: F401\n" + "from typing_extensions import TypedDict, NotRequired, Literal # noqa: F401\n" "from dash.development.base_component import " "Component, _explicitize_args\n\n\n" ) From 6dc0fc18ce27afeba8422d802e12b633e782ccb0 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 1 May 2023 18:09:36 -0400 Subject: [PATCH 26/49] Allow all keys in TypedDicts and postpone annotations declared on the class component. --- dash/development/_py_components_generation.py | 5 +- dash/development/_py_prop_typing.py | 29 ++++----- tests/unit/development/metadata_test.py | 64 +++++++++++-------- tests/unit/development/test_generate_class.py | 1 - 4 files changed, 51 insertions(+), 48 deletions(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index cec95fd748..4abac595b3 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -52,7 +52,7 @@ def generate_class_string( # it to be `null` or whether that was just the default value. # The solution might be to deal with default values better although # not all component authors will supply those. - c = '''{extra_types}class {typename}(Component): + c = '''class {typename}(Component): """{docstring}""" _children_props = {children_props} _base_nodes = {base_nodes} @@ -184,8 +184,7 @@ def __init__( required_validation=required_validation, children_props=nodes, base_nodes=filter_base_nodes(nodes) + ["children"], - extra_types="".join(shapes.get(typename, {}).values()), - shapes="\n".join(f" {k} = {k}" for k in shapes.get(typename, {}).keys()), + shapes="\n".join(shapes.get(typename, {}).values()), ) ) diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index afdcafbaa6..9b05d7507a 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -1,16 +1,14 @@ import json import string - +import textwrap import stringcase -from ._all_keywords import python_keywords - shapes = {} -shape_template = """class {name}(TypedDict): -{values} - - +shape_template = """{name} = TypedDict( + "{name}", + {values} +) """ @@ -33,24 +31,23 @@ def generate_shape(type_info, component_name: str, prop_name: str): name = stringcase.pascalcase(prop_name) for prop_key, prop_type in type_info["value"].items(): - if prop_key in python_keywords or prop_key != _clean_key(prop_key): - # Got invalid keys - return "dict" - 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}]") + props.append(f' "{prop_key}": NotRequired[{typed}]') else: - props.append(f" {prop_key}: {typed}") + props.append(f' "{prop_key}": {typed}') shapes.setdefault(component_name, {}) - shapes[component_name][name] = shape_template.format( - name=name, values="\n".join(props) + shapes[component_name][name] = textwrap.indent( + shape_template.format( + name=name, values=" {\n" + ",\n".join(props) + "\n }" + ), + " ", ) - return name + return f'"{name}"' def generate_union(type_info, component_name: str, prop_name: str): diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index 72bf185d2e..11b9332e27 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -6,28 +6,6 @@ from dash.development.base_component import Component, _explicitize_args -class OptionalObjectWithExactAndNestedDescriptionFigure(TypedDict): - data: NotRequired[typing.Union[typing.List[dict], typing.Tuple]] - layout: NotRequired[dict] - - -class OptionalObjectWithExactAndNestedDescription(TypedDict): - color: NotRequired[str] - fontSize: NotRequired[typing.Union[int, float, numbers.Number]] - figure: NotRequired[OptionalObjectWithExactAndNestedDescriptionFigure] - - -class OptionalObjectWithShapeAndNestedDescriptionFigure(TypedDict): - data: NotRequired[typing.Union[typing.List[dict], typing.Tuple]] - layout: NotRequired[dict] - - -class OptionalObjectWithShapeAndNestedDescription(TypedDict): - color: NotRequired[str] - fontSize: NotRequired[typing.Union[int, float, numbers.Number]] - figure: NotRequired[OptionalObjectWithShapeAndNestedDescriptionFigure] - - class Table(Component): """A Table component. This is a description of the component. @@ -115,10 +93,40 @@ class Table(Component): _base_nodes = ['optionalNode', 'optionalElement', 'children'] _namespace = 'TableComponents' _type = 'Table' - OptionalObjectWithExactAndNestedDescriptionFigure = OptionalObjectWithExactAndNestedDescriptionFigure - OptionalObjectWithExactAndNestedDescription = OptionalObjectWithExactAndNestedDescription - OptionalObjectWithShapeAndNestedDescriptionFigure = OptionalObjectWithShapeAndNestedDescriptionFigure - OptionalObjectWithShapeAndNestedDescription = OptionalObjectWithShapeAndNestedDescription + OptionalObjectWithExactAndNestedDescriptionFigure = TypedDict( + "OptionalObjectWithExactAndNestedDescriptionFigure", + { + "data": NotRequired[typing.Union[typing.List[dict], typing.Tuple]], + "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.Union[typing.List[dict], typing.Tuple]], + "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, @@ -137,8 +145,8 @@ def __init__( optionalUnion: typing.Union[str, typing.Union[int, float, numbers.Number], typing.Any] = Component.UNDEFINED, optionalArrayOf: typing.Union[typing.List[typing.Union[int, float, numbers.Number]], typing.Tuple] = Component.UNDEFINED, optionalObjectOf: typing.Dict[typing.Union[str, float, int], typing.Union[int, float, numbers.Number]] = Component.UNDEFINED, - optionalObjectWithExactAndNestedDescription: OptionalObjectWithExactAndNestedDescription = Component.UNDEFINED, - optionalObjectWithShapeAndNestedDescription: OptionalObjectWithShapeAndNestedDescription = Component.UNDEFINED, + optionalObjectWithExactAndNestedDescription: "OptionalObjectWithExactAndNestedDescription" = Component.UNDEFINED, + optionalObjectWithShapeAndNestedDescription: "OptionalObjectWithShapeAndNestedDescription" = Component.UNDEFINED, optionalAny: typing.Any = Component.UNDEFINED, customProp: typing.Any = Component.UNDEFINED, customArrayProp: typing.Union[typing.List[typing.Any], typing.Tuple] = Component.UNDEFINED, diff --git a/tests/unit/development/test_generate_class.py b/tests/unit/development/test_generate_class.py index 64500daae0..2a07fee7ca 100644 --- a/tests/unit/development/test_generate_class.py +++ b/tests/unit/development/test_generate_class.py @@ -130,7 +130,6 @@ def test_attrs_match_forbidden_props(component_class): expected_attrs = set(reserved_words + ["children"]) - {"_.*"} expected_attrs.update( { - "OptionalEnumEnum", "OptionalObjectWithExactAndNestedDescription", "OptionalObjectWithExactAndNestedDescriptionFigure", "OptionalObjectWithShapeAndNestedDescription", From e6c23569f3d1c236cb4894a017b5f8b868ee39f5 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 2 May 2023 10:34:41 -0400 Subject: [PATCH 27/49] Relax version of typing_extensions>=4.1.1 --- requires-install.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requires-install.txt b/requires-install.txt index 1deae00970..1a758b4ef8 100644 --- a/requires-install.txt +++ b/requires-install.txt @@ -5,5 +5,4 @@ dash_core_components==2.0.0 dash_table==5.0.0 importlib-metadata==4.8.3;python_version<"3.7" contextvars==2.4;python_version<"3.7" -typing_extensions==4.1.1;python_version<"3.7" -typing_extensions>=4.4.0;python_version>"3.6" +typing_extensions>=4.1.1 From 7bc56b9d1d101260801b95da6ae78a0a89123f92 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 2 May 2023 12:10:54 -0400 Subject: [PATCH 28/49] Fix test_rdrh003_refresh_jwt --- tests/integration/renderer/test_request_hooks.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/integration/renderer/test_request_hooks.py b/tests/integration/renderer/test_request_hooks.py index 9b1e57f56f..ea2bede020 100644 --- a/tests/integration/renderer/test_request_hooks.py +++ b/tests/integration/renderer/test_request_hooks.py @@ -253,10 +253,7 @@ def wrap(*args, **kwargs): try: if flask.request.method == "OPTIONS": return func(*args, **kwargs) - token = ( - flask.request.authorization - or flask.request.headers.environ.get("HTTP_AUTHORIZATION") - ) + token = flask.request.headers.environ.get("HTTP_AUTHORIZATION") if required_jwt_len and ( not token or len(token) != required_jwt_len + len("Bearer ") ): From 9eee25342db57d0e2f8afc02d9eb62b14b0a2a1f Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 2 May 2023 13:37:31 -0400 Subject: [PATCH 29/49] Move stringcase requirement to requires-install --- requires-dev.txt | 1 - requires-install.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/requires-dev.txt b/requires-dev.txt index 8e5610b5b3..22a770dca3 100644 --- a/requires-dev.txt +++ b/requires-dev.txt @@ -2,4 +2,3 @@ coloredlogs>=15.0.1 fire>=0.4.0 PyYAML>=5.4.1 -stringcase>=1.2.0 diff --git a/requires-install.txt b/requires-install.txt index 1a758b4ef8..4cbb8fb4d7 100644 --- a/requires-install.txt +++ b/requires-install.txt @@ -6,3 +6,4 @@ dash_table==5.0.0 importlib-metadata==4.8.3;python_version<"3.7" contextvars==2.4;python_version<"3.7" typing_extensions>=4.1.1 +stringcase>=1.2.0 From f2281854a54e01b4cfc932b7aaacf8f41da6edf5 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 8 Aug 2024 09:43:22 -0400 Subject: [PATCH 30/49] Fix lint on 12 --- dash/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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] From 7b0da8ade4d9e27fb04e74c94f9c74809a909d89 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 8 Aug 2024 09:51:09 -0400 Subject: [PATCH 31/49] Remove py2 code from explicitize_args --- dash/development/base_component.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 7fbca11b63..8292dde8bc 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -410,13 +410,12 @@ def __repr__(self): return f"{self._type}({props_string})" +# 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: @@ -428,11 +427,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 From fb8decb1768c9e0c53a83aab0934471571880b6a Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 8 Aug 2024 10:13:05 -0400 Subject: [PATCH 32/49] Update browser-tools --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5e39efc783..b3c204d6d4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2.1 orbs: win: circleci/windows@5.0.0 percy: percy/agent@0.1.3 - browser-tools: circleci/browser-tools@1.4.6 + browser-tools: circleci/browser-tools@1.4.8 jobs: From 197180c324650806bafbe44514764796438bf703 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 8 Aug 2024 10:42:37 -0400 Subject: [PATCH 33/49] Support plotly.Figure --- dash/development/_py_components_generation.py | 10 +++++-- dash/development/_py_prop_typing.py | 28 +++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index 4abac595b3..507c58ffe8 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -10,7 +10,7 @@ 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 +from ._py_prop_typing import get_prop_typing, shapes, custom_imports from .base_component import Component @@ -150,7 +150,7 @@ def __init__( type_name = type_info.get("name") - typed = get_prop_typing(type_name, typename, prop_key, type_info) + typed = get_prop_typing(type_name, typename, prop_key, type_info, namespace) arg_value = f"{prop_key}: {typed} = {default_value}" @@ -220,6 +220,12 @@ def generate_class_file( class_string = generate_class_string( typename, props, description, namespace, prop_reorder_exceptions, max_props ) + + custom_imp = custom_imports[namespace][typename] + if custom_imports: + import_string += "\n".join(custom_imp) + import_string += "\n\n" + file_name = f"{typename:s}.py" file_path = os.path.join(namespace, file_name) diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index 9b05d7507a..05ff45934a 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -1,6 +1,8 @@ +import collections import json import string import textwrap + import stringcase @@ -10,6 +12,7 @@ {values} ) """ +custom_imports = collections.defaultdict(lambda: collections.defaultdict(list)) def _clean_key(key): @@ -115,10 +118,31 @@ def generate_enum(type_info, *_): return f"Literal[{', '.join(values)}]" -def get_prop_typing(type_name: str, component_name: str, prop_name: str, type_info): - return PROP_TYPING.get(type_name, generate_any)( +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 = { From 8734e1b39ff23a8a2873bb275d6d4ae9cd43200c Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 9 Aug 2024 15:49:14 -0400 Subject: [PATCH 34/49] Use ComponentType --- dash/development/_py_components_generation.py | 4 +++- dash/development/_py_prop_typing.py | 12 ++++++------ dash/development/base_component.py | 7 ++++++- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index 507c58ffe8..55334a81a6 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -11,7 +11,7 @@ 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 .base_component import Component +from .base_component import Component, ComponentType # pylint: disable=unused-argument,too-many-locals,too-many-branches @@ -213,6 +213,7 @@ def generate_class_file( "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" ) @@ -282,6 +283,7 @@ def generate_class( ) scope = { "Component": Component, + "ComponentType": ComponentType, "_explicitize_args": _explicitize_args, "typing": typing, "numbers": numbers, diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index 05ff45934a..5ee309b548 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -82,7 +82,7 @@ def generate_array_of( typed = get_prop_typing( type_info["value"]["name"], component_name, prop_name, type_info["value"] ) - return f"typing.Union[typing.List[{typed}], typing.Tuple]" + return f"typing.Union[typing.Sequence[{typed}], typing.Tuple]" def generate_object_of(type_info, component_name: str, prop_name: str): @@ -146,7 +146,7 @@ def generate_plotly_figure(*_): PROP_TYPING = { - "array": generate_type("typing.Union[typing.List, typing.Tuple]"), + "array": generate_type("typing.Union[typing.Sequence, typing.Tuple]"), "arrayOf": generate_array_of, "object": generate_type("dict"), "shape": generate_shape, @@ -155,12 +155,12 @@ def generate_plotly_figure(*_): "bool": generate_type("bool"), "number": generate_type("typing.Union[int, float, numbers.Number]"), "node": generate_type( - "typing.Union[str, int, float, Component," - " typing.List[typing.Union" - "[str, int, float, Component]]]" + "typing.Union[str, int, float, ComponentType," + " typing.Sequence[typing.Union" + "[str, int, float, ComponentType]]]" ), "func": generate_any, - "element": generate_type("Component"), + "element": generate_type("ComponentType"), "union": generate_union, "any": generate_any, "custom": generate_any, diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 8292dde8bc..23d1a15b23 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -76,6 +76,9 @@ def _check_if_has_indexable_children(item): class Component(metaclass=ComponentMeta): _children_props = [] _base_nodes = ["children"] + _namespace: str + _type: str + _prop_names: list[str] _valid_wildcard_attributes: typing.List[str] available_wildcard_properties: typing.List[str] @@ -101,7 +104,6 @@ def __str__(self): def __init__(self, **kwargs): 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 @@ -410,6 +412,9 @@ def __repr__(self): return f"{self._type}({props_string})" +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 From d5b0ebe27794aac2a333893f284352bbcf00f4ca Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 12 Aug 2024 09:17:47 -0400 Subject: [PATCH 35/49] Add Optional to every prop --- dash/development/_py_components_generation.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index 55334a81a6..426e6cb8ad 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -102,9 +102,9 @@ def __init__( prop_keys = list(props.keys()) if "children" in props and "children" in list_of_valid_keys: prop_keys.remove("children") - default_argtext = ( - f"children: {get_prop_typing('node', '', '', {})} = None,\n " - ) + # 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: @@ -138,21 +138,19 @@ def __init__( or prop_key == "setProps" ): continue - required = prop.get("required") - type_info = prop.get("type") - default_value = "Component.REQUIRED" if required else "Component.UNDEFINED" + type_info = prop.get("type") if not type_info: print(f"Invalid prop type for typing: {prop_key}") - default_arglist.append(f"{prop_key} = {default_value}") + 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}: {typed} = {default_value}" + arg_value = f"{prop_key}: typing.Optional[{typed}] = None" default_arglist.append(arg_value) From 4e583e3d884ca3bcdf474d12bdf5058565bf44e4 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 12 Aug 2024 09:48:23 -0400 Subject: [PATCH 36/49] Fix list[str] > List[str] --- dash/development/base_component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 23d1a15b23..3c84a07f56 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -78,7 +78,7 @@ class Component(metaclass=ComponentMeta): _base_nodes = ["children"] _namespace: str _type: str - _prop_names: list[str] + _prop_names: typing.List[str] _valid_wildcard_attributes: typing.List[str] available_wildcard_properties: typing.List[str] From 150c2a5e4b3f7da6284dd47a10c89a654e6d5b60 Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 16 Aug 2024 14:07:47 -0400 Subject: [PATCH 37/49] Fix test_typing --- tests/integration/test_typing.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/integration/test_typing.py b/tests/integration/test_typing.py index 360a85c688..da4a551bac 100644 --- a/tests/integration/test_typing.py +++ b/tests/integration/test_typing.py @@ -12,7 +12,7 @@ """ -def run_pyright(codefile): +def run_pyright(codefile: str): cmd = shlex.split( f"pyright {codefile}", @@ -33,14 +33,14 @@ def run_pyright(codefile): def assert_pyright_output( - codefile, expected_outputs=tuple(), expected_errors=tuple(), expected_status=0 + 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 + assert ex_out in output, f"Invalid output:\n {output}" for ex_err in expected_errors: assert ex_err in error @@ -54,7 +54,7 @@ def assert_pyright_output( { "expected_status": 1, "expected_outputs": [ - 'Argument of type "Literal[4]" cannot be assigned to parameter "a_string" of type "str"' + 'Argument of type "Literal[4]" cannot be assigned to parameter "a_string" of type "str | None"' ], }, ), @@ -70,7 +70,7 @@ def assert_pyright_output( "expected_status": 1, "expected_outputs": [ 'Argument of type "Literal[\'\']" cannot be assigned to parameter "a_number" ' - 'of type "int | float | Number"' + 'of type "int | float | Number | None"' ], }, ), @@ -104,7 +104,7 @@ def assert_pyright_output( "expected_status": 1, "expected_outputs": [ 'Argument of type "dict[Any, Any]" cannot be assigned to parameter "array_string" ' - 'of type "List[str] | Tuple[Unknown, ...]"' + 'of type "Sequence[str] | Tuple[Unknown, ...] | None"' ], }, ), @@ -204,7 +204,7 @@ def assert_pyright_output( "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]"' + 'to parameter "a_tuple" of type "Tuple[int | float | Number, str] | None"' ], }, ), @@ -224,14 +224,16 @@ def assert_pyright_output( "obj={}", { "expected_status": 1, - "expected_outputs": ['"value" is required in "Obj"'], + "expected_outputs": ['"dict[Any, Any]" is incompatible with "Obj"'], }, ), ( "obj={'value': 'a', 'label': 1}", { "expected_status": 1, - "expected_outputs": ['"Literal[1]" is incompatible with "str"'], + "expected_outputs": [ + '"dict[str, str | int]" is incompatible with "Obj"' + ], }, ), ( From 96b931a256484e1f058d2a5cc7b636066f0c622a Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 16 Aug 2024 14:14:51 -0400 Subject: [PATCH 38/49] Update pyright --- requirements/ci.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/ci.txt b/requirements/ci.txt index 11c1ddf83a..5816a5a51d 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -19,4 +19,4 @@ pyzmq==25.1.2 xlrd>=2.0.1 pytest-rerunfailures jupyterlab<4.0.0 -pyright==1.1.276;python_version>="3.7" +pyright==1.1.375;python_version>="3.7" From 4045a61d839e9a3a6779b292fed9a6d6f697aa1e Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 16 Aug 2024 14:59:52 -0400 Subject: [PATCH 39/49] Update pyright --- requirements/ci.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/ci.txt b/requirements/ci.txt index 5816a5a51d..b7f501bfb2 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -19,4 +19,4 @@ pyzmq==25.1.2 xlrd>=2.0.1 pytest-rerunfailures jupyterlab<4.0.0 -pyright==1.1.375;python_version>="3.7" +pyright==1.1.376;python_version>="3.7" From 46d05be3d88cfeca69232891804f34069319f7f1 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 19 Aug 2024 15:38:31 -0400 Subject: [PATCH 40/49] Fix custom imports. --- dash/development/_py_components_generation.py | 2 +- tests/unit/development/metadata_test.py | 47 ++++++++++--------- .../development/test_generate_class_file.py | 1 + 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index 426e6cb8ad..cf29f876ae 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -221,7 +221,7 @@ def generate_class_file( ) custom_imp = custom_imports[namespace][typename] - if custom_imports: + if custom_imp: import_string += "\n".join(custom_imp) import_string += "\n\n" diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index 11b9332e27..636fa8a671 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -3,6 +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 ComponentType # noqa: F401 from dash.development.base_component import Component, _explicitize_args @@ -96,7 +97,7 @@ class Table(Component): OptionalObjectWithExactAndNestedDescriptionFigure = TypedDict( "OptionalObjectWithExactAndNestedDescriptionFigure", { - "data": NotRequired[typing.Union[typing.List[dict], typing.Tuple]], + "data": NotRequired[typing.Union[typing.Sequence[dict], typing.Tuple]], "layout": NotRequired[dict] } ) @@ -113,7 +114,7 @@ class Table(Component): OptionalObjectWithShapeAndNestedDescriptionFigure = TypedDict( "OptionalObjectWithShapeAndNestedDescriptionFigure", { - "data": NotRequired[typing.Union[typing.List[dict], typing.Tuple]], + "data": NotRequired[typing.Union[typing.Sequence[dict], typing.Tuple]], "layout": NotRequired[dict] } ) @@ -130,27 +131,27 @@ class Table(Component): @_explicitize_args def __init__( self, - children: typing.Union[str, int, float, Component, typing.List[typing.Union[str, int, float, Component]]] = None, - optionalArray: typing.Union[typing.List, typing.Tuple] = Component.UNDEFINED, - optionalBool: bool = Component.UNDEFINED, - optionalFunc: typing.Any = Component.UNDEFINED, - optionalNumber: typing.Union[int, float, numbers.Number] = Component.UNDEFINED, - optionalObject: dict = Component.UNDEFINED, - optionalString: str = Component.UNDEFINED, - optionalSymbol: typing.Any = Component.UNDEFINED, - optionalNode: typing.Union[str, int, float, Component, typing.List[typing.Union[str, int, float, Component]]] = Component.UNDEFINED, - optionalElement: Component = Component.UNDEFINED, - optionalMessage: typing.Any = Component.UNDEFINED, - optionalEnum: Literal["News", "Photos"] = Component.UNDEFINED, - optionalUnion: typing.Union[str, typing.Union[int, float, numbers.Number], typing.Any] = Component.UNDEFINED, - optionalArrayOf: typing.Union[typing.List[typing.Union[int, float, numbers.Number]], typing.Tuple] = Component.UNDEFINED, - optionalObjectOf: typing.Dict[typing.Union[str, float, int], typing.Union[int, float, numbers.Number]] = Component.UNDEFINED, - optionalObjectWithExactAndNestedDescription: "OptionalObjectWithExactAndNestedDescription" = Component.UNDEFINED, - optionalObjectWithShapeAndNestedDescription: "OptionalObjectWithShapeAndNestedDescription" = Component.UNDEFINED, - optionalAny: typing.Any = Component.UNDEFINED, - customProp: typing.Any = Component.UNDEFINED, - customArrayProp: typing.Union[typing.List[typing.Any], typing.Tuple] = Component.UNDEFINED, - id: str = Component.UNDEFINED, + children: typing.Optional[typing.Union[str, int, float, ComponentType, typing.Sequence[typing.Union[str, int, float, ComponentType]]]] = None, + optionalArray: typing.Optional[typing.Union[typing.Sequence, typing.Tuple]] = 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.Union[typing.Sequence[typing.Union[int, float, numbers.Number]], typing.Tuple]] = 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.Union[typing.Sequence[typing.Any], typing.Tuple]] = 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'] diff --git a/tests/unit/development/test_generate_class_file.py b/tests/unit/development/test_generate_class_file.py index a84ca579c2..ec271ba613 100644 --- a/tests/unit/development/test_generate_class_file.py +++ b/tests/unit/development/test_generate_class_file.py @@ -17,6 +17,7 @@ "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" ) From 85c26b7cfad806f601114d7d11d49a9f5bacfcd6 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 20 Aug 2024 10:49:41 -0400 Subject: [PATCH 41/49] Include py.typed in manifest --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) 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 From 40bd21a79db8d5bd441f984335c2af468a92c027 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 20 Aug 2024 10:52:10 -0400 Subject: [PATCH 42/49] Add typing to callback function --- dash/_callback.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/dash/_callback.py b/dash/_callback.py index 0c7476fda1..d037753296 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 .exceptions import ( InvalidCallbackReturnValue, @@ -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, + cancel: Optional[List[Input]] = None, + manager: Optional[BaseLongCallbackManager] = None, + cache_args_to_ignore: Optional[list] = None, on_error: Optional[Callable[[Exception], Any]] = None, **_kwargs, ): @@ -154,7 +155,7 @@ def callback( callback_list = _kwargs.pop("callback_list", GLOBAL_CALLBACK_LIST) if background: - long_spec = { + long_spec: Any = { "interval": interval, } From 648a1c6881742dea27f93fa78df36d0652521829 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 20 Aug 2024 15:47:22 -0400 Subject: [PATCH 43/49] build From 8d185c88821522c575280dbfa25b6dd692028914 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Mon, 26 Aug 2024 08:52:38 -0400 Subject: [PATCH 44/49] Update tests/integration/test_typing.py Co-authored-by: Alex Johnson --- tests/integration/test_typing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/test_typing.py b/tests/integration/test_typing.py index da4a551bac..68cdfafe37 100644 --- a/tests/integration/test_typing.py +++ b/tests/integration/test_typing.py @@ -45,7 +45,6 @@ def assert_pyright_output( assert ex_err in error -@pytest.mark.skipif(sys.version_info < (3, 7), reason="pyright not available on 3.6") @pytest.mark.parametrize( "arguments, assertions", [ From ef6b05477baccfe818a66a4980b3291c262af736 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 26 Aug 2024 09:27:06 -0400 Subject: [PATCH 45/49] Remove tuple from list typing --- dash/__init__.py | 34 +++++++++++++++++++++++++++++ dash/development/_py_prop_typing.py | 4 ++-- 2 files changed, 36 insertions(+), 2 deletions(-) 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/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index 5ee309b548..bd38ec054c 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -82,7 +82,7 @@ def generate_array_of( typed = get_prop_typing( type_info["value"]["name"], component_name, prop_name, type_info["value"] ) - return f"typing.Union[typing.Sequence[{typed}], typing.Tuple]" + return f"typing.Sequence[{typed}]" def generate_object_of(type_info, component_name: str, prop_name: str): @@ -146,7 +146,7 @@ def generate_plotly_figure(*_): PROP_TYPING = { - "array": generate_type("typing.Union[typing.Sequence, typing.Tuple]"), + "array": generate_type("typing.Sequence"), "arrayOf": generate_array_of, "object": generate_type("dict"), "shape": generate_shape, From 6b30d0824728a0fbe29a968a4e7e57ac2c3627d8 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 26 Aug 2024 10:02:39 -0400 Subject: [PATCH 46/49] Deprecate runtime component loader --- dash/development/component_loader.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 518c7638a4..71e0c34342 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -1,6 +1,7 @@ import collections import json import os +import warnings from ._py_components_generation import ( generate_class_file, @@ -35,6 +36,14 @@ def load_components(metadata_path, namespace="default_namespace"): `type`, `valid_kwargs`, and `setup`. """ + warnings.warn( + DeprecationWarning( + "Dynamic components loading has been deprecated and will be removed" + " in dash 3.0.\n" + f"Update {namespace} to generate components with dash-generate-components" + ) + ) + # Register the component lib for index include. ComponentRegistry.registry.add(namespace) components = [] From 26a83e5502b25107a81d2914a76fe50839d0fe36 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 26 Aug 2024 12:17:07 -0400 Subject: [PATCH 47/49] Backward compatible ComponentType. --- dash/development/_py_components_generation.py | 30 +++++++++++-------- tests/unit/development/metadata_test.py | 15 ++++++---- .../development/test_generate_class_file.py | 12 +------- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index cf29f876ae..f7729adf4c 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -13,6 +13,20 @@ 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 + +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( @@ -206,15 +220,7 @@ def generate_class_file( Returns ------- """ - import_string = ( - "# AUTO GENERATED FILE - DO NOT EDIT\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" - ) + imports = import_string class_string = generate_class_string( typename, props, description, namespace, prop_reorder_exceptions, max_props @@ -222,14 +228,14 @@ def generate_class_file( custom_imp = custom_imports[namespace][typename] if custom_imp: - import_string += "\n".join(custom_imp) - import_string += "\n\n" + 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}") diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index 636fa8a671..3004924c8a 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -3,8 +3,11 @@ import typing # noqa: F401 import numbers # noqa: F401 from typing_extensions import TypedDict, NotRequired, Literal # noqa: F401 -from dash.development.base_component import ComponentType # 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): @@ -97,7 +100,7 @@ class Table(Component): OptionalObjectWithExactAndNestedDescriptionFigure = TypedDict( "OptionalObjectWithExactAndNestedDescriptionFigure", { - "data": NotRequired[typing.Union[typing.Sequence[dict], typing.Tuple]], + "data": NotRequired[typing.Sequence[dict]], "layout": NotRequired[dict] } ) @@ -114,7 +117,7 @@ class Table(Component): OptionalObjectWithShapeAndNestedDescriptionFigure = TypedDict( "OptionalObjectWithShapeAndNestedDescriptionFigure", { - "data": NotRequired[typing.Union[typing.Sequence[dict], typing.Tuple]], + "data": NotRequired[typing.Sequence[dict]], "layout": NotRequired[dict] } ) @@ -132,7 +135,7 @@ class Table(Component): 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.Union[typing.Sequence, typing.Tuple]] = 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, @@ -144,13 +147,13 @@ def __init__( 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.Union[typing.Sequence[typing.Union[int, float, numbers.Number]], typing.Tuple]] = 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.Union[typing.Sequence[typing.Any], typing.Tuple]] = None, + customArrayProp: typing.Optional[typing.Sequence[typing.Any]] = None, id: typing.Optional[str] = None, **kwargs ): diff --git a/tests/unit/development/test_generate_class_file.py b/tests/unit/development/test_generate_class_file.py index ec271ba613..f79121bf01 100644 --- a/tests/unit/development/test_generate_class_file.py +++ b/tests/unit/development/test_generate_class_file.py @@ -8,20 +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" - "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" -) - @pytest.fixture def make_component_dir(load_test_metadata_json): From 18fad2dea609a647399cd718e78529c9293c712d Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 9 Sep 2024 15:58:27 -0400 Subject: [PATCH 48/49] Remove component loader deprecation from pr. --- dash/development/component_loader.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 71e0c34342..518c7638a4 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -1,7 +1,6 @@ import collections import json import os -import warnings from ._py_components_generation import ( generate_class_file, @@ -36,14 +35,6 @@ def load_components(metadata_path, namespace="default_namespace"): `type`, `valid_kwargs`, and `setup`. """ - warnings.warn( - DeprecationWarning( - "Dynamic components loading has been deprecated and will be removed" - " in dash 3.0.\n" - f"Update {namespace} to generate components with dash-generate-components" - ) - ) - # Register the component lib for index include. ComponentRegistry.registry.add(namespace) components = [] From d13b343934185f9ccb1631e094d3d0c49660eb8d Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 8 Nov 2024 10:54:54 -0500 Subject: [PATCH 49/49] Fix type assertion --- tests/integration/test_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_typing.py b/tests/integration/test_typing.py index 68cdfafe37..cf49c1577d 100644 --- a/tests/integration/test_typing.py +++ b/tests/integration/test_typing.py @@ -103,7 +103,7 @@ def assert_pyright_output( "expected_status": 1, "expected_outputs": [ 'Argument of type "dict[Any, Any]" cannot be assigned to parameter "array_string" ' - 'of type "Sequence[str] | Tuple[Unknown, ...] | None"' + 'of type "Sequence[str] | None"' ], }, ),