From c8073a809bd9cde20761c4fa0c90aab3ede1c783 Mon Sep 17 00:00:00 2001 From: Greg Wilson Date: Mon, 31 Mar 2025 11:31:50 -0400 Subject: [PATCH 01/12] feat: adding more type annotations to satisfy pyright --- .../dash_core_components_base/__init__.py | 2 +- .../dash-table/dash_table_base/__init__.py | 2 + .../src/dash-table/dash/DataTable.js | 49 ++++++------------- dash/_callback.py | 9 ++-- dash/_callback_context.py | 4 +- dash/_grouping.py | 2 +- dash/_jupyter.py | 1 + dash/_pages.py | 11 +++-- dash/_utils.py | 16 +++--- dash/_watch.py | 2 +- dash/background_callback/_proxy_set_props.py | 4 +- .../managers/celery_manager.py | 6 ++- .../managers/diskcache_manager.py | 7 ++- dash/dash.py | 38 ++++++++------ dash/dependencies.py | 4 +- dash/development/_jl_components_generation.py | 1 + dash/development/_py_components_generation.py | 4 +- dash/development/_r_components_generation.py | 1 + dash/development/base_component.py | 14 +++--- dash/development/component_generator.py | 10 ++-- dash/testing/application_runners.py | 41 +++++++++------- dash/testing/browser.py | 15 +++--- dash/testing/dash_page.py | 5 ++ dash/testing/plugin.py | 40 +++++++++------ requirements/ci.txt | 2 +- 25 files changed, 157 insertions(+), 133 deletions(-) diff --git a/components/dash-core-components/dash_core_components_base/__init__.py b/components/dash-core-components/dash_core_components_base/__init__.py index 2072e1b1a2..200c6aba31 100644 --- a/components/dash-core-components/dash_core_components_base/__init__.py +++ b/components/dash-core-components/dash_core_components_base/__init__.py @@ -13,7 +13,7 @@ send_string, ) -__all__ = _components + [ +__all__ = _components + [ # type: ignore[reportUnsupportedDunderAll] "send_bytes", "send_data_frame", "send_file", diff --git a/components/dash-table/dash_table_base/__init__.py b/components/dash-table/dash_table_base/__init__.py index 7e3bd250b1..71d1025581 100644 --- a/components/dash-table/dash_table_base/__init__.py +++ b/components/dash-table/dash_table_base/__init__.py @@ -1,3 +1,5 @@ +# type: ignore + import os as _os import sys as _sys import json diff --git a/components/dash-table/src/dash-table/dash/DataTable.js b/components/dash-table/src/dash-table/dash/DataTable.js index 9974a0a638..d701b5b344 100644 --- a/components/dash-table/src/dash-table/dash/DataTable.js +++ b/components/dash-table/src/dash-table/dash/DataTable.js @@ -472,25 +472,14 @@ export const propTypes = { * View the documentation examples to learn more. * */ - fixed_columns: PropTypes.oneOfType([ - PropTypes.exact({ - /** - * Example `{'headers':False, 'data':0}` No columns are fixed (the default) - */ - - data: PropTypes.oneOf([0]), - headers: PropTypes.oneOf([false]) - }), - - PropTypes.exact({ - /** - * Example `{'headers':True, 'data':1}` one column is fixed. - */ + fixed_columns: PropTypes.exact({ + /** + * Example `{'headers':False, 'data':0}` No columns are fixed (the default) + */ - data: PropTypes.number, - headers: PropTypes.oneOf([true]).isRequired - }) - ]), + data: PropTypes.number, + headers: PropTypes.bool + }), /** * `fixed_rows` will "fix" the set of rows so that @@ -505,24 +494,14 @@ export const propTypes = { * way that your columns are rendered or sized. * View the documentation examples to learn more. */ - fixed_rows: PropTypes.oneOfType([ - PropTypes.exact({ - /** - * Example `{'headers':False, 'data':0}` No rows are fixed (the default) - */ - - data: PropTypes.oneOf([0]), - headers: PropTypes.oneOf([false]) - }), - PropTypes.exact({ - /** - * Example `{'headers':True, 'data':1}` one row is fixed. - */ + fixed_rows: PropTypes.exact({ + /** + * Example `{'headers':False, 'data':0}` No rows are fixed (the default) + */ - data: PropTypes.number, - headers: PropTypes.oneOf([true]).isRequired - }) - ]), + data: PropTypes.number, + headers: PropTypes.bool + }), /** * If `single`, then the user can select a single column or group diff --git a/dash/_callback.py b/dash/_callback.py index ada060f955..fa41873f7e 100644 --- a/dash/_callback.py +++ b/dash/_callback.py @@ -360,7 +360,9 @@ def register_callback( # pylint: disable=too-many-locals def wrap_func(func): - if background is not None: + if background is None: + background_key = None + else: background_key = BaseBackgroundCallbackManager.register_func( func, background.get("progress") is not None, @@ -515,7 +517,7 @@ def add_context(*args, **kwargs): return to_json(response) else: try: - output_value = _invoke_callback(func, *func_args, **func_kwargs) + output_value = _invoke_callback(func, *func_args, **func_kwargs) # type: ignore[reportArgumentType] except PreventUpdate as err: raise err except Exception as err: # pylint: disable=broad-exception-caught @@ -555,7 +557,7 @@ def add_context(*args, **kwargs): if NoUpdate.is_no_update(val): continue for vali, speci in ( - zip(val, spec) if isinstance(spec, list) else [[val, spec]] + zip(val, spec) if isinstance(spec, list) else [[val, spec]] # type: ignore[reportArgumentType]] ): if not NoUpdate.is_no_update(vali): has_update = True @@ -590,6 +592,7 @@ def add_context(*args, **kwargs): dist = app.get_dist(diff_packages) response["dist"] = dist + jsonResponse = None try: jsonResponse = to_json(response) except TypeError: diff --git a/dash/_callback_context.py b/dash/_callback_context.py index 55d3cf0a49..f64865c464 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -207,7 +207,7 @@ def response(self): @staticmethod @has_context - def record_timing(name, duration=None, description=None): + def record_timing(name, duration, description=None): """Records timing information for a server resource. :param name: The name of the resource. @@ -215,7 +215,7 @@ def record_timing(name, duration=None, description=None): :param duration: The time in seconds to report. Internally, this is rounded to the nearest millisecond. - :type duration: float or None + :type duration: float :param description: A description of the resource. :type description: string or None diff --git a/dash/_grouping.py b/dash/_grouping.py index 8d5ac51271..611fa92414 100644 --- a/dash/_grouping.py +++ b/dash/_grouping.py @@ -13,7 +13,7 @@ structure """ -from dash.exceptions import InvalidCallbackReturnValue +from .exceptions import InvalidCallbackReturnValue from ._utils import AttributeDict, stringify_id diff --git a/dash/_jupyter.py b/dash/_jupyter.py index 144c470a94..16de3b88f9 100644 --- a/dash/_jupyter.py +++ b/dash/_jupyter.py @@ -1,3 +1,4 @@ +# type: ignore import asyncio import io import inspect diff --git a/dash/_pages.py b/dash/_pages.py index 5bf0c14fc7..ba2855639a 100644 --- a/dash/_pages.py +++ b/dash/_pages.py @@ -1,5 +1,6 @@ import collections import importlib +import importlib.util # to make the type checker happy import os import re import sys @@ -85,10 +86,9 @@ def _infer_path(module_name, template): def _module_name_is_package(module_name): - return ( - module_name in sys.modules - and Path(sys.modules[module_name].__file__).name == "__init__.py" - ) + file_path = sys.modules[module_name].__file__ + assert file_path is not None # to make type checker happy + return module_name in sys.modules and Path(file_path).name == "__init__.py" def _path_to_module_name(path): @@ -441,6 +441,9 @@ def _import_layouts_from_pages(pages_folder): module_name = _infer_module_name(page_path) spec = importlib.util.spec_from_file_location(module_name, page_path) + assert ( + spec is not None and spec.loader is not None + ) # to satisfy type checking page_module = importlib.util.module_from_spec(spec) spec.loader.exec_module(page_module) sys.modules[module_name] = page_module diff --git a/dash/_utils.py b/dash/_utils.py index f1056d0130..736a081801 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -3,7 +3,7 @@ import sys import uuid import hashlib -import collections +from collections import abc import subprocess import logging import io @@ -15,8 +15,8 @@ from html import escape from functools import wraps -from typing import Union -from dash.types import RendererHooks +from typing import Union, cast +from .types import RendererHooks logger = logging.getLogger() @@ -58,7 +58,7 @@ def generate_hash(): # pylint: disable=no-member def patch_collections_abc(member): - return getattr(collections.abc, member) + return getattr(abc, member) class AttributeDict(dict): @@ -118,9 +118,11 @@ def __setitem__(self, key, val): return super().__setitem__(key, val) - def update(self, other): + def update(self, other=None, **kwargs): # Overrides dict.update() to use __setitem__ above - for k, v in other.items(): + # Needs default `None` and `kwargs` to satisfy type checking + source = cast(dict, other) if other is not None else kwargs + for k, v in source.items(): self[k] = v # pylint: disable=inconsistent-return-statements @@ -251,7 +253,7 @@ def gen_salt(chars): ) -class OrderedSet(collections.abc.MutableSet): +class OrderedSet(abc.MutableSet): def __init__(self, *args): self._data = [] for i in args: diff --git a/dash/_watch.py b/dash/_watch.py index 65c87e284a..c13d70f7a6 100644 --- a/dash/_watch.py +++ b/dash/_watch.py @@ -6,7 +6,7 @@ def watch(folders, on_change, pattern=None, sleep_time=0.1): pattern = re.compile(pattern) if pattern else None - watched = collections.defaultdict(lambda: -1) + watched = collections.defaultdict(lambda: -1.0) def walk(): walked = [] diff --git a/dash/background_callback/_proxy_set_props.py b/dash/background_callback/_proxy_set_props.py index f72fcf3737..4d89c5b157 100644 --- a/dash/background_callback/_proxy_set_props.py +++ b/dash/background_callback/_proxy_set_props.py @@ -14,5 +14,5 @@ def __setitem__(self, key, value): self._data.setdefault(key, {}) self._data[key] = {**self._data[key], **value} - def get(self, key): - return self._data.get(key) + def get(self, key, default=None): + return self._data.get(key, default) diff --git a/dash/background_callback/managers/celery_manager.py b/dash/background_callback/managers/celery_manager.py index 66ca9de51a..2a9d4ebb73 100644 --- a/dash/background_callback/managers/celery_manager.py +++ b/dash/background_callback/managers/celery_manager.py @@ -33,8 +33,8 @@ def __init__(self, celery_app, cache_by=None, expire=None): is determined by the default behavior of the celery result backend. """ try: - import celery # pylint: disable=import-outside-toplevel,import-error - from celery.backends.base import ( # pylint: disable=import-outside-toplevel,import-error + import celery # type: ignore[reportMissingImports]; pylint: disable=import-outside-toplevel,import-error + from celery.backends.base import ( # type: ignore[reportMissingImports]; pylint: disable=import-outside-toplevel,import-error DisabledBackend, ) except ImportError as missing_imports: @@ -157,11 +157,13 @@ def _set_props(_id, props): ctx = copy_context() def run(): + assert isinstance(context, dict) # to help type checking c = AttributeDict(**context) c.ignore_register_page = False c.updated_props = ProxySetProps(_set_props) context_value.set(c) errored = False + user_callback_output = None # to help type checking try: if isinstance(user_callback_args, dict): user_callback_output = fn(*maybe_progress, **user_callback_args) diff --git a/dash/background_callback/managers/diskcache_manager.py b/dash/background_callback/managers/diskcache_manager.py index 994da4c0ac..ede8c6afed 100644 --- a/dash/background_callback/managers/diskcache_manager.py +++ b/dash/background_callback/managers/diskcache_manager.py @@ -1,5 +1,6 @@ import traceback from contextvars import copy_context +from multiprocess import Process # type: ignore from . import BaseBackgroundCallbackManager from .._proxy_set_props import ProxySetProps @@ -32,7 +33,7 @@ def __init__(self, cache=None, cache_by=None, expire=None): is determined by the default behavior of the ``cache`` instance. """ try: - import diskcache # pylint: disable=import-outside-toplevel + import diskcache # type: ignore[reportMissingImports]; pylint: disable=import-outside-toplevel import psutil # noqa: F401,E402 pylint: disable=import-outside-toplevel,unused-import,unused-variable,import-error import multiprocess # noqa: F401,E402 pylint: disable=import-outside-toplevel,unused-import,unused-variable except ImportError as missing_imports: @@ -116,9 +117,6 @@ def clear_cache_entry(self, key): # noinspection PyUnresolvedReferences def call_job_fn(self, key, job_fn, args, context): - # pylint: disable-next=import-outside-toplevel,no-name-in-module,import-error - from multiprocess import Process - # pylint: disable-next=not-callable proc = Process( target=job_fn, @@ -189,6 +187,7 @@ def run(): c.updated_props = ProxySetProps(_set_props) context_value.set(c) errored = False + user_callback_output = None # initialized to prevent type checker warnings try: if isinstance(user_callback_args, dict): user_callback_output = fn(*maybe_progress, **user_callback_args) diff --git a/dash/dash.py b/dash/dash.py index e617901bd3..c73ae80637 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -452,6 +452,7 @@ def __init__( # pylint: disable=too-many-statements url_base_pathname, routes_pathname_prefix, requests_pathname_prefix ) + assert isinstance(name, str) # to satisfy type checking self.config = AttributeDict( name=name, assets_folder=os.path.join( @@ -651,7 +652,7 @@ def init_app(self, app=None, **kwargs): if config.compress: try: # pylint: disable=import-outside-toplevel - from flask_compress import Compress + from flask_compress import Compress # type: ignore[reportMissingImports] # gzip Compress(self.server) @@ -765,11 +766,15 @@ def layout(self, value): self.validation_layout = layout_value def _layout_value(self): - layout = self._layout() if self._layout_is_function else self._layout + if self._layout_is_function: + assert callable(self._layout) + layout = self._layout() + else: + layout = self._layout # Add any extra components if self._extra_components: - layout = html.Div(children=[layout] + self._extra_components) + layout = html.Div(children=[layout] + self._extra_components) # type: ignore[reportArgumentType] return layout @@ -879,8 +884,9 @@ def _relative_url_path(relative_package_path="", namespace=""): else: version = importlib.import_module(namespace).__version__ - module_path = os.path.join( - os.path.dirname(sys.modules[namespace].__file__), relative_package_path + module_path = os.path.join( # type: ignore[reportCallIssue] + os.path.dirname(sys.modules[namespace].__file__), # type: ignore[reportCallIssue] + relative_package_path, ) modified = int(os.stat(module_path).st_mtime) @@ -975,7 +981,7 @@ def _generate_scripts_html(self): dev = self._dev_tools.serve_dev_bundles srcs = ( self._collect_and_register_resources( - self.scripts._resources._filter_resources(deps, dev_bundles=dev) + self.scripts._resources._filter_resources(deps, dev_bundles=dev) # type: ignore[reportArgumentType] ) + self.config.external_scripts + self._collect_and_register_resources( @@ -1522,7 +1528,7 @@ def _walk_assets_directory(self): if f.endswith("js"): self.scripts.append_script(self._add_assets_resource(path, full)) elif f.endswith("css"): - self.css.append_css(self._add_assets_resource(path, full)) + self.css.append_css(self._add_assets_resource(path, full)) # type: ignore[reportArgumentType] elif f == "favicon.ico": self._favicon = path @@ -1896,30 +1902,30 @@ def enable_dev_tools( for index, package in enumerate(packages): if isinstance(package, AssertionRewritingHook): - dash_spec = importlib.util.find_spec("dash") + dash_spec = importlib.util.find_spec("dash") # type: ignore[reportAttributeAccess] dash_test_path = dash_spec.submodule_search_locations[0] setattr(dash_spec, "path", dash_test_path) packages[index] = dash_spec component_packages_dist = [ - dash_test_path + dash_test_path # type: ignore[reportPossiblyUnboundVariable] if isinstance(package, ModuleSpec) - else os.path.dirname(package.path) + else os.path.dirname(package.path) # type: ignore[reportAttributeAccessIssue] if hasattr(package, "path") else os.path.dirname( - package._path[0] # pylint: disable=protected-access + package._path[0] # type: ignore[reportAttributeAccessIssue]; pylint: disable=protected-access ) if hasattr(package, "_path") - else package.filename + else package.filename # type: ignore[reportAttributeAccessIssue] for package in packages ] for i, package in enumerate(packages): if hasattr(package, "path") and "dash/dash" in os.path.dirname( - package.path + package.path # type: ignore[reportAttributeAccessIssue] ): component_packages_dist[i : i + 1] = [ - os.path.join(os.path.dirname(package.path), x) + os.path.join(os.path.dirname(package.path), x) # type: ignore[reportAttributeAccessIssue] for x in ["dcc", "html", "dash_table"] ] @@ -2026,7 +2032,7 @@ def _on_assets_change(self, filename, modified, deleted): if filename.endswith("js"): self.scripts.append_script(res) elif filename.endswith("css"): - self.css.append_css(res) + self.css.append_css(res) # type: ignore[reportArgumentType] if deleted: if filename in self._assets_files: @@ -2282,7 +2288,7 @@ def router(): "pathname_": Input(_ID_LOCATION, "pathname"), "search_": Input(_ID_LOCATION, "search"), } - inputs.update(self.routing_callback_inputs) + inputs.update(self.routing_callback_inputs) # type: ignore[reportCallIssue] @self.callback( Output(_ID_CONTENT, "children"), diff --git a/dash/dependencies.py b/dash/dependencies.py index 819d134546..47f3da7562 100644 --- a/dash/dependencies.py +++ b/dash/dependencies.py @@ -1,4 +1,4 @@ -from dash.development.base_component import Component +from .development.base_component import Component from ._validate import validate_callback from ._grouping import flatten_grouping, make_grouping_by_index @@ -130,7 +130,7 @@ class State(DashDependency): # pylint: disable=too-few-public-methods class ClientsideFunction: # pylint: disable=too-few-public-methods - def __init__(self, namespace=None, function_name=None): + def __init__(self, namespace: str, function_name=None): if namespace.startswith("_dashprivate_"): raise ValueError("Namespaces cannot start with '_dashprivate_'.") diff --git a/dash/development/_jl_components_generation.py b/dash/development/_jl_components_generation.py index 24999603d1..60d0998c9b 100644 --- a/dash/development/_jl_components_generation.py +++ b/dash/development/_jl_components_generation.py @@ -1,4 +1,5 @@ # pylint: disable=consider-using-f-string +# type: ignore import copy import os import shutil diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index c18db36780..4a1e6178c2 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -769,7 +769,7 @@ def js_to_py_type(type_object, is_flow_type=False, indent_num=0): return "" if js_type_name in js_to_py_types: if js_type_name == "signature": # This is a Flow object w/ signature - return js_to_py_types[js_type_name](indent_num) + return js_to_py_types[js_type_name](indent_num) # type: ignore[reportCallIssue] # All other types - return js_to_py_types[js_type_name]() + return js_to_py_types[js_type_name]() # type: ignore[reportCallIssue] return "" diff --git a/dash/development/_r_components_generation.py b/dash/development/_r_components_generation.py index 9ad938744c..e279250d38 100644 --- a/dash/development/_r_components_generation.py +++ b/dash/development/_r_components_generation.py @@ -1,4 +1,5 @@ # pylint: disable=consider-using-f-string +# type: ignore import os import sys import shutil diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 39131a72d6..ea8a0fdf92 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -262,7 +262,7 @@ def _get_set_or_delete(self, id, operation, new_item=None): if isinstance(self.children, Component): if getattr(self.children, "id", None) is not None: # Woohoo! It's the item that we're looking for - if self.children.id == id: + if self.children.id == id: # type: ignore[reportAttributeAccessIssue] if operation == "get": return self.children if operation == "set": @@ -287,15 +287,17 @@ def _get_set_or_delete(self, id, operation, new_item=None): # if children is like a list if isinstance(self.children, (tuple, MutableSequence)): - for i, item in enumerate(self.children): + for i, item in enumerate(self.children): # type: ignore[reportOptionalIterable] # If the item itself is the one we're looking for if getattr(item, "id", None) == id: if operation == "get": return item if operation == "set": + assert self.children is not None # to satisfy type checking self.children[i] = new_item return if operation == "delete": + assert self.children is not None # to satisfy type checking del self.children[i] return @@ -366,7 +368,7 @@ def _traverse_with_paths(self): # children is a list of components elif isinstance(children, (tuple, MutableSequence)): - for idx, i in enumerate(children): + for idx, i in enumerate(children): # type: ignore[reportOptionalIterable] list_path = f"[{idx:d}] {type(i).__name__:s}{self._id_str(i)}" yield list_path, i @@ -384,7 +386,7 @@ def _traverse_ids(self): def __iter__(self): """Yield IDs in the tree of children.""" for t in self._traverse_ids(): - yield t.id + yield t.id # type: ignore[reportAttributeAccessIssue] def __len__(self): """Return the number of items in the tree.""" @@ -399,7 +401,7 @@ def __len__(self): length = 1 length += len(self.children) elif isinstance(self.children, (tuple, MutableSequence)): - for c in self.children: + for c in self.children: # type: ignore[reportOptionalIterable] length += 1 if isinstance(c, Component): length += len(c) @@ -456,5 +458,5 @@ def wrapper(*args, **kwargs): new_sig = inspect.signature(wrapper).replace( parameters=list(inspect.signature(func).parameters.values()) ) - wrapper.__signature__ = new_sig + wrapper.__signature__ = new_sig # type: ignore[reportFunctionMemberAccess] return wrapper diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 250fe9c0be..276dbfb0f5 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -114,10 +114,12 @@ def generate_components( generator_methods = [functools.partial(generate_class_file, **py_generator_kwargs)] + pkg_data = None if rprefix is not None or jlprefix is not None: with open("package.json", "r", encoding="utf-8") as f: pkg_data = safe_json_loads(f.read()) + rpkg_data = None if rprefix is not None: if not os.path.exists("man"): os.makedirs("man") @@ -126,8 +128,6 @@ def generate_components( if os.path.isfile("dash-info.yaml"): with open("dash-info.yaml", encoding="utf-8") as yamldata: rpkg_data = yaml.safe_load(yamldata) - else: - rpkg_data = None generator_methods.append( functools.partial(write_class_file, prefix=rprefix, rpkg_data=rpkg_data) ) @@ -283,12 +283,12 @@ def cli(): def byteify(input_object): if isinstance(input_object, dict): return OrderedDict( - [(byteify(key), byteify(value)) for key, value in input_object.iteritems()] + [(byteify(key), byteify(value)) for key, value in input_object.items()] ) if isinstance(input_object, list): return [byteify(element) for element in input_object] - if isinstance(input_object, unicode): # noqa:F821 - return input_object.encode("utf-8") + if isinstance(input_object, str): # noqa:F821 + return input_object.encode(encoding="utf-8") return input_object diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index f747ef2e3a..cc92e638dd 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -118,11 +118,11 @@ def tmp_app_path(self): class KillerThread(threading.Thread): def __init__(self, **kwargs): super().__init__(**kwargs) - self._old_threads = list(threading._active.keys()) # pylint: disable=W0212 + self._old_threads = list(threading._active.keys()) # type: ignore[reportAttributeAccessIssue]; pylint: disable=W0212 def kill(self): # Kill all the new threads. - for thread_id in list(threading._active): # pylint: disable=W0212 + for thread_id in list(threading._active): # type: ignore[reportAttributeAccessIssue]; pylint: disable=W0212 if thread_id in self._old_threads: continue @@ -149,7 +149,7 @@ def __init__(self, keep_open=False, stop_timeout=3): self.thread = None def running_and_accessible(self, url): - if self.thread.is_alive(): + if self.thread.is_alive(): # type: ignore[reportOptionalMemberAccess] return self.accessible(url) raise DashAppLoadingError("Thread is not alive.") @@ -202,14 +202,14 @@ def run(): retries += 1 time.sleep(1) - self.started = self.thread.is_alive() + self.started = self.thread.is_alive() # type: ignore[reportOptionalMemberAccess] if not self.started: raise DashAppLoadingError("threaded server failed to start") def stop(self): - self.thread.kill() - self.thread.join() - wait.until_not(self.thread.is_alive, self.stop_timeout) + self.thread.kill() # type: ignore[reportOptionalMemberAccess] + self.thread.join() # type: ignore[reportOptionalMemberAccess] + wait.until_not(self.thread.is_alive, self.stop_timeout) # type: ignore[reportOptionalMemberAccess] self.started = False @@ -237,14 +237,14 @@ def target(): logger.exception(error) raise error - self.proc = multiprocess.Process(target=target) # pylint: disable=not-callable + self.proc = multiprocess.Process(target=target) # type: ignore[reportAttributeAccessIssue]; pylint: disable=not-callable self.proc.start() wait.until(lambda: self.accessible(self.url), timeout=start_timeout) self.started = True def stop(self): - process = psutil.Process(self.proc.pid) + process = psutil.Process(self.proc.pid) # type: ignore[reportOptionalMemberAccess] for proc in process.children(recursive=True): try: @@ -322,11 +322,10 @@ def stop(self): logger.debug("removing temporary app path %s", self.tmp_app_path) shutil.rmtree(self.tmp_app_path) - _except = subprocess.TimeoutExpired # pylint:disable=no-member self.proc.communicate( timeout=self.stop_timeout # pylint: disable=unexpected-keyword-arg ) - except _except: + except subprocess.TimeoutExpired: logger.exception( "subprocess terminate not success, trying to kill " "the subprocess in a safe manner" @@ -342,7 +341,7 @@ def __init__(self, keep_open=False, stop_timeout=3): self.proc = None # pylint: disable=arguments-differ - def start(self, app, start_timeout=2, cwd=None): + def start(self, app, start_timeout=2, cwd=None): # type: ignore[reportIncompatibleMethodOverride] """Start the server with subprocess and Rscript.""" if os.path.isfile(app) and os.path.exists(app): @@ -353,9 +352,11 @@ def start(self, app, start_timeout=2, cwd=None): else: # app is a string chunk, we make a temporary folder to store app.R # and its relevant assets - self._tmp_app_path = os.path.join( - "/tmp" if not self.is_windows else os.getenv("TEMP"), uuid.uuid4().hex - ) + tmp_dir = "/tmp" if not self.is_windows else os.getenv("TEMP") + assert isinstance(tmp_dir, str) + hex_id = uuid.uuid4().hex + self._tmp_app_path = os.path.join(tmp_dir, hex_id) + assert isinstance(self.tmp_app_path, str) # to satisfy type checking try: os.mkdir(self.tmp_app_path) except OSError: @@ -439,7 +440,7 @@ def __init__(self, keep_open=False, stop_timeout=3): self.proc = None # pylint: disable=arguments-differ - def start(self, app, start_timeout=30, cwd=None): + def start(self, app, start_timeout=30, cwd=None): # type: ignore[reportIncompatibleMethodOverride] """Start the server with subprocess and julia.""" if os.path.isfile(app) and os.path.exists(app): @@ -450,9 +451,11 @@ def start(self, app, start_timeout=30, cwd=None): else: # app is a string chunk, we make a temporary folder to store app.jl # and its relevant assets - self._tmp_app_path = os.path.join( - "/tmp" if not self.is_windows else os.getenv("TEMP"), uuid.uuid4().hex - ) + tmp_dir = "/tmp" if not self.is_windows else os.getenv("TEMP") + assert isinstance(tmp_dir, str) # to satisfy typing + hex_id = uuid.uuid4().hex + self._tmp_app_path = os.path.join(tmp_dir, hex_id) + assert isinstance(self.tmp_app_path, str) # to satisfy typing try: os.mkdir(self.tmp_app_path) except OSError: diff --git a/dash/testing/browser.py b/dash/testing/browser.py index c4fcb8fca8..f1ed3fbf57 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -102,7 +102,7 @@ def __exit__(self, exc_type, exc_val, traceback): logger.info("percy finalize relies on CI job") except WebDriverException: logger.exception("webdriver quit was not successful") - except percy.errors.Error: + except percy.errors.Error: # type: ignore[reportAttributeAccessIssue] logger.exception("percy runner failed to finalize properly") def visit_and_snapshot( @@ -115,10 +115,11 @@ def visit_and_snapshot( stay_on_page=False, widths=None, ): + path = resource_path.lstrip("/") try: - path = resource_path.lstrip("/") if path != resource_path: logger.warning("we stripped the left '/' in resource_path") + assert isinstance(self.server_url, str) # to satisfy type checking self.driver.get(f"{self.server_url.rstrip('/')}/{path}") # wait for the hook_id to present and all callbacks get fired @@ -199,7 +200,7 @@ def percy_snapshot( self.percy_runner.snapshot(name=name, widths=widths) except requests.HTTPError as err: # Ignore retries. - if err.request.status_code != 400: + if err.request.status_code != 400: # type: ignore[reportAttributeAccessIssue] raise err if convert_canvases: @@ -227,6 +228,7 @@ def take_snapshot(self, name): running selenium session id """ target = "/tmp/dash_artifacts" if not self._is_windows() else os.getenv("TEMP") + assert isinstance(target, str) # to satisfy type checking if not os.path.exists(target): try: os.mkdir(target) @@ -402,7 +404,7 @@ def wait_for_page(self, url=None, timeout=10): ) except TimeoutException as exc: logger.exception("dash server is not loaded within %s seconds", timeout) - logs = "\n".join((str(log) for log in self.get_logs())) + logs = "\n".join((str(log) for log in self.get_logs())) # type: ignore[reportOptionalIterable] logger.debug(logs) html = self.find_element("body").get_property("innerHTML") raise DashAppLoadingError( @@ -497,7 +499,7 @@ def _get_chrome(self): options.add_argument("--remote-debugging-port=9222") chrome = ( - webdriver.Remote(command_executor=self._remote_url, options=options) + webdriver.Remote(command_executor=self._remote_url, options=options) # type: ignore[reportAttributeAccessIssue] if self._remote else webdriver.Chrome(options=options) ) @@ -505,7 +507,7 @@ def _get_chrome(self): # https://bugs.chromium.org/p/chromium/issues/detail?id=696481 if self._headless: # pylint: disable=protected-access - chrome.command_executor._commands["send_command"] = ( + chrome.command_executor._commands["send_command"] = ( # type: ignore[reportArgumentType] "POST", "/session/$sessionId/chromium/send_command", ) @@ -531,6 +533,7 @@ def _get_firefox(self): "browser.helperApps.neverAsk.saveToDisk", "application/octet-stream", # this MIME is generic for binary ) + assert isinstance(self._remote_url, str) # to satisfy type checking return ( webdriver.Remote( command_executor=self._remote_url, diff --git a/dash/testing/dash_page.py b/dash/testing/dash_page.py index 2f2d340d57..2f4d69fd35 100644 --- a/dash/testing/dash_page.py +++ b/dash/testing/dash_page.py @@ -1,3 +1,8 @@ +# type: ignore[reportAttributeAccessIssue] +# Ignore attribute access issues when type checking because mixin +# class depends on other class lineage to supply things. We could use +# a protocol definition here instead… + from bs4 import BeautifulSoup diff --git a/dash/testing/plugin.py b/dash/testing/plugin.py index 02386dc458..1b917d7657 100644 --- a/dash/testing/plugin.py +++ b/dash/testing/plugin.py @@ -1,4 +1,6 @@ # pylint: disable=missing-docstring,redefined-outer-name +from typing import Any + import pytest from .consts import SELENIUM_GRID_DEFAULT @@ -11,6 +13,16 @@ def __init__(self, **kwargs): "Please install to use the dash testing fixtures." ) + def __enter__(self) -> Any: + """Implemented to satisfy type checking.""" + + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> bool: + """Implemented to satisfy type checking.""" + + return False + try: from dash.testing.application_runners import ( @@ -127,39 +139,39 @@ def pytest_runtest_makereport(item, call): # pylint: disable=unused-argument @pytest.fixture -def dash_thread_server() -> ThreadedRunner: +def dash_thread_server() -> ThreadedRunner: # type: ignore[reportInvalidTypeForm] """Start a local dash server in a new thread.""" with ThreadedRunner() as starter: yield starter @pytest.fixture -def dash_process_server() -> ProcessRunner: +def dash_process_server() -> ProcessRunner: # type: ignore[reportInvalidTypeForm] """Start a Dash server with subprocess.Popen and waitress-serve.""" with ProcessRunner() as starter: yield starter @pytest.fixture -def dash_multi_process_server() -> MultiProcessRunner: +def dash_multi_process_server() -> MultiProcessRunner: # type: ignore[reportInvalidTypeForm] with MultiProcessRunner() as starter: yield starter @pytest.fixture -def dashr_server() -> RRunner: +def dashr_server() -> RRunner: # type: ignore[reportInvalidTypeForm] with RRunner() as starter: yield starter @pytest.fixture -def dashjl_server() -> JuliaRunner: +def dashjl_server() -> JuliaRunner: # type: ignore[reportInvalidTypeForm] with JuliaRunner() as starter: yield starter @pytest.fixture -def dash_br(request, tmpdir) -> Browser: +def dash_br(request, tmpdir) -> Browser: # type: ignore[reportInvalidTypeForm] with Browser( browser=request.config.getoption("webdriver"), remote=request.config.getoption("remote"), @@ -175,9 +187,9 @@ def dash_br(request, tmpdir) -> Browser: @pytest.fixture -def dash_duo(request, dash_thread_server, tmpdir) -> DashComposite: +def dash_duo(request, dash_thread_server, tmpdir) -> DashComposite: # type: ignore[reportInvalidTypeForm] with DashComposite( - dash_thread_server, + server=dash_thread_server, browser=request.config.getoption("webdriver"), remote=request.config.getoption("remote"), remote_url=request.config.getoption("remote_url"), @@ -192,9 +204,9 @@ def dash_duo(request, dash_thread_server, tmpdir) -> DashComposite: @pytest.fixture -def dash_duo_mp(request, dash_multi_process_server, tmpdir) -> DashComposite: +def dash_duo_mp(request, dash_multi_process_server, tmpdir) -> DashComposite: # type: ignore[reportInvalidTypeForm] with DashComposite( - dash_multi_process_server, + server=dash_multi_process_server, browser=request.config.getoption("webdriver"), remote=request.config.getoption("remote"), remote_url=request.config.getoption("remote_url"), @@ -209,9 +221,9 @@ def dash_duo_mp(request, dash_multi_process_server, tmpdir) -> DashComposite: @pytest.fixture -def dashr(request, dashr_server, tmpdir) -> DashRComposite: +def dashr(request, dashr_server, tmpdir) -> DashRComposite: # type: ignore[reportInvalidTypeForm] with DashRComposite( - dashr_server, + server=dashr_server, browser=request.config.getoption("webdriver"), remote=request.config.getoption("remote"), remote_url=request.config.getoption("remote_url"), @@ -226,9 +238,9 @@ def dashr(request, dashr_server, tmpdir) -> DashRComposite: @pytest.fixture -def dashjl(request, dashjl_server, tmpdir) -> DashJuliaComposite: +def dashjl(request, dashjl_server, tmpdir) -> DashJuliaComposite: # type: ignore[reportInvalidTypeForm] with DashJuliaComposite( - dashjl_server, + server=dashjl_server, browser=request.config.getoption("webdriver"), remote=request.config.getoption("remote"), remote_url=request.config.getoption("remote_url"), diff --git a/requirements/ci.txt b/requirements/ci.txt index aa3cd94bfb..96495aa4f9 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -18,4 +18,4 @@ pyzmq==25.1.2 xlrd>=2.0.1 pytest-rerunfailures jupyterlab<4.0.0 -pyright==1.1.376;python_version>="3.7" +pyright==1.1.398;python_version>="3.7" From 449fd22dacb6ba424889768c7cc3c67782a4341f Mon Sep 17 00:00:00 2001 From: Greg Wilson Date: Tue, 8 Apr 2025 09:24:43 -0400 Subject: [PATCH 02/12] feat: replace 'assert' added to satisfy type checking Based on feedback from @T4rk1n, removed assertions added to satisfy type checker and used other mechanisms instead. --- dash/_pages.py | 11 ++++------- .../background_callback/managers/celery_manager.py | 3 +-- dash/dash.py | 7 +++---- dash/development/base_component.py | 6 ++---- dash/testing/application_runners.py | 14 ++++++++------ dash/testing/browser.py | 7 ++++--- 6 files changed, 22 insertions(+), 26 deletions(-) diff --git a/dash/_pages.py b/dash/_pages.py index ba2855639a..ac015a5bf0 100644 --- a/dash/_pages.py +++ b/dash/_pages.py @@ -8,6 +8,7 @@ from pathlib import Path from os.path import isfile, join from urllib.parse import parse_qs, unquote +from typing import cast import flask @@ -86,8 +87,7 @@ def _infer_path(module_name, template): def _module_name_is_package(module_name): - file_path = sys.modules[module_name].__file__ - assert file_path is not None # to make type checker happy + file_path = cast(str, sys.modules[module_name].__file__) # to satisfy type checking return module_name in sys.modules and Path(file_path).name == "__init__.py" @@ -441,11 +441,8 @@ def _import_layouts_from_pages(pages_folder): module_name = _infer_module_name(page_path) spec = importlib.util.spec_from_file_location(module_name, page_path) - assert ( - spec is not None and spec.loader is not None - ) # to satisfy type checking - page_module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(page_module) + page_module = importlib.util.module_from_spec(spec) # type: ignore[reportArgumentType] + spec.loader.exec_module(page_module) # type: ignore[reportOptionalMemberAccess] sys.modules[module_name] = page_module if ( diff --git a/dash/background_callback/managers/celery_manager.py b/dash/background_callback/managers/celery_manager.py index 2a9d4ebb73..da60fa153b 100644 --- a/dash/background_callback/managers/celery_manager.py +++ b/dash/background_callback/managers/celery_manager.py @@ -157,8 +157,7 @@ def _set_props(_id, props): ctx = copy_context() def run(): - assert isinstance(context, dict) # to help type checking - c = AttributeDict(**context) + c = AttributeDict(**context) # type: ignore[reportCallIssue] c.ignore_register_page = False c.updated_props = ProxySetProps(_set_props) context_value.set(c) diff --git a/dash/dash.py b/dash/dash.py index 025f5fda80..30d2143d07 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -18,7 +18,7 @@ import base64 import traceback from urllib.parse import urlparse -from typing import Any, Callable, Dict, Optional, Union, Sequence +from typing import Any, Callable, Dict, Optional, Union, Sequence, cast import flask @@ -452,7 +452,7 @@ def __init__( # pylint: disable=too-many-statements url_base_pathname, routes_pathname_prefix, requests_pathname_prefix ) - assert isinstance(name, str) # to satisfy type checking + name = cast(str, name) # to satisfy type checking self.config = AttributeDict( name=name, assets_folder=os.path.join( @@ -767,8 +767,7 @@ def layout(self, value): def _layout_value(self): if self._layout_is_function: - assert callable(self._layout) - layout = self._layout() + layout = self._layout() # type: ignore[reportOptionalCall] else: layout = self._layout diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 9e97dfbd0e..a54a0a9f42 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -301,12 +301,10 @@ def _get_set_or_delete(self, id, operation, new_item=None): if operation == "get": return item if operation == "set": - assert self.children is not None # to satisfy type checking - self.children[i] = new_item + self.children[i] = new_item # type: ignore[reportOptionalSubscript] return if operation == "delete": - assert self.children is not None # to satisfy type checking - del self.children[i] + del self.children[i] # type: ignore[reportOptionalSubscript] return # Otherwise, recursively dig into that item's subtree diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index cc92e638dd..efc1d06a4c 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -9,6 +9,7 @@ import logging import inspect import ctypes +from typing import cast import runpy import requests @@ -353,15 +354,16 @@ def start(self, app, start_timeout=2, cwd=None): # type: ignore[reportIncompati # app is a string chunk, we make a temporary folder to store app.R # and its relevant assets tmp_dir = "/tmp" if not self.is_windows else os.getenv("TEMP") - assert isinstance(tmp_dir, str) + tmp_dir = cast(str, tmp_dir) # to satisfy type checking hex_id = uuid.uuid4().hex - self._tmp_app_path = os.path.join(tmp_dir, hex_id) - assert isinstance(self.tmp_app_path, str) # to satisfy type checking + self._tmp_app_path = cast( + str, os.path.join(tmp_dir, hex_id) + ) # to satisfy type checking try: - os.mkdir(self.tmp_app_path) + os.mkdir(self.tmp_app_path) # type: ignore[reportArgumentType] except OSError: logger.exception("cannot make temporary folder %s", self.tmp_app_path) - path = os.path.join(self.tmp_app_path, "app.R") + path = os.path.join(self.tmp_app_path, "app.R") # type: ignore[reportCallIssue] logger.info("RRunner start => app is R code chunk") logger.info("make a temporary R file for execution => %s", path) @@ -392,7 +394,7 @@ def start(self, app, start_timeout=2, cwd=None): # type: ignore[reportIncompati ] for asset in assets: - target = os.path.join(self.tmp_app_path, os.path.basename(asset)) + target = os.path.join(self.tmp_app_path, os.path.basename(asset)) # type: ignore[reportCallIssue] if os.path.exists(target): logger.debug("delete existing target %s", target) shutil.rmtree(target) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index f1ed3fbf57..22bbe90c68 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -3,6 +3,7 @@ import sys import time import logging +from typing import cast import warnings import percy import requests @@ -119,7 +120,7 @@ def visit_and_snapshot( try: if path != resource_path: logger.warning("we stripped the left '/' in resource_path") - assert isinstance(self.server_url, str) # to satisfy type checking + self.server_url = cast(str, self.server_url) # to satisfy type checking self.driver.get(f"{self.server_url.rstrip('/')}/{path}") # wait for the hook_id to present and all callbacks get fired @@ -228,7 +229,7 @@ def take_snapshot(self, name): running selenium session id """ target = "/tmp/dash_artifacts" if not self._is_windows() else os.getenv("TEMP") - assert isinstance(target, str) # to satisfy type checking + target = cast(str, target) # to satisfy type checking if not os.path.exists(target): try: os.mkdir(target) @@ -533,7 +534,7 @@ def _get_firefox(self): "browser.helperApps.neverAsk.saveToDisk", "application/octet-stream", # this MIME is generic for binary ) - assert isinstance(self._remote_url, str) # to satisfy type checking + self._remote_url = cast(str, self._remote_url) # to satisfy type checking return ( webdriver.Remote( command_executor=self._remote_url, From 79fa3562d01f95cf9fd6605b1b905161605e96bd Mon Sep 17 00:00:00 2001 From: Greg Wilson Date: Tue, 8 Apr 2025 09:42:28 -0400 Subject: [PATCH 03/12] feat: improve type checking in dash/dash.py based on feedback from @T4rk1n --- dash/dash.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 30d2143d07..f7b13b678b 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -434,17 +434,17 @@ def __init__( # pylint: disable=too-many-statements ): _validate.check_obsolete(obsolete) - caller_name = None if name else get_caller_name() + caller_name: str = name if name is not None else get_caller_name() # We have 3 cases: server is either True (we create the server), False # (defer server creation) or a Flask app instance (we use their server) if isinstance(server, flask.Flask): self.server = server if name is None: - name = getattr(server, "name", caller_name) + caller_name = getattr(server, "name", caller_name) elif isinstance(server, bool): name = name if name else caller_name - self.server = flask.Flask(name) if server else None # type: ignore + self.server = flask.Flask(caller_name) if server else None # type: ignore else: raise ValueError("server must be a Flask app or a boolean") @@ -454,16 +454,16 @@ def __init__( # pylint: disable=too-many-statements name = cast(str, name) # to satisfy type checking self.config = AttributeDict( - name=name, + name=caller_name, assets_folder=os.path.join( - flask.helpers.get_root_path(name), assets_folder + flask.helpers.get_root_path(caller_name), assets_folder ), # type: ignore assets_url_path=assets_url_path, assets_ignore=assets_ignore, assets_external_path=get_combined_config( "assets_external_path", assets_external_path, "" ), - pages_folder=pages_folder_config(name, pages_folder, use_pages), + pages_folder=pages_folder_config(caller_name, pages_folder, use_pages), eager_loading=eager_loading, include_assets_files=get_combined_config( "include_assets_files", include_assets_files, True From 6924a0ddb82c326d181d2c336fd715d24044296a Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 8 Apr 2025 10:10:14 -0400 Subject: [PATCH 04/12] Fix remaining pyright errors in dash.Dash --- dash/dash.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index f7b13b678b..53d7fc46e7 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -393,6 +393,10 @@ class Dash(ObsoleteChecker): server: flask.Flask + # Layout is a complex type which can be many things + _layout: Any + _extra_components: Any + def __init__( # pylint: disable=too-many-statements self, name: Optional[str] = None, @@ -1896,7 +1900,7 @@ def enable_dev_tools( if "_pytest" in sys.modules: from _pytest.assertion.rewrite import ( # pylint: disable=import-outside-toplevel - AssertionRewritingHook, + AssertionRewritingHook, # type: ignore[reportPrivateImportUsage] ) for index, package in enumerate(packages): From 2d3fd9c386c11d7cdbe18027c78fe831316aaa21 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 8 Apr 2025 11:04:25 -0400 Subject: [PATCH 05/12] Add more typing for browser.py --- dash/testing/browser.py | 53 +++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 22bbe90c68..22b12e1b9a 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -3,12 +3,13 @@ import sys import time import logging -from typing import cast +from typing import Union, Optional import warnings import percy import requests from selenium import webdriver +from selenium.webdriver.remote.webdriver import BaseWebDriver from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By from selenium.webdriver.support.wait import WebDriverWait @@ -38,20 +39,22 @@ class Browser(DashPageMixin): + _url: str + # pylint: disable=too-many-arguments def __init__( self, - browser, - remote=False, - remote_url=None, - headless=False, - options=None, - download_path="", - percy_run=True, - percy_finalize=True, - percy_assets_root="", - wait_timeout=10, - pause=False, + browser: str, + remote: bool = False, + remote_url: Optional[str] = None, + headless: bool = False, + options: Optional[Union[dict, list]] = None, + download_path: str = "", + percy_run: bool = True, + percy_finalize: bool = True, + percy_assets_root: str = "", + wait_timeout: int = 10, + pause: bool = False, ): self._browser = browser.lower() self._remote_url = remote_url @@ -71,7 +74,7 @@ def __init__( self._wd_wait = WebDriverWait(self.driver, wait_timeout) self._last_ts = 0 - self._url = None + self._url = "" self._window_idx = 0 # switch browser tabs @@ -108,8 +111,8 @@ def __exit__(self, exc_type, exc_val, traceback): def visit_and_snapshot( self, - resource_path, - hook_id, + resource_path: str, + hook_id: str, wait_for_callbacks=True, convert_canvases=False, assert_check=True, @@ -120,7 +123,7 @@ def visit_and_snapshot( try: if path != resource_path: logger.warning("we stripped the left '/' in resource_path") - self.server_url = cast(str, self.server_url) # to satisfy type checking + self.server_url = self.server_url self.driver.get(f"{self.server_url.rstrip('/')}/{path}") # wait for the hook_id to present and all callbacks get fired @@ -219,7 +222,7 @@ def percy_snapshot( """ ) - def take_snapshot(self, name): + def take_snapshot(self, name: str): """Hook method to take snapshot when a selenium test fails. The snapshot is placed under. @@ -228,8 +231,10 @@ def take_snapshot(self, name): with a filename combining test case name and the running selenium session id """ - target = "/tmp/dash_artifacts" if not self._is_windows() else os.getenv("TEMP") - target = cast(str, target) # to satisfy type checking + target = ( + "/tmp/dash_artifacts" if not self._is_windows() else os.getenv("TEMP", "") + ) + if not os.path.exists(target): try: os.mkdir(target) @@ -286,7 +291,7 @@ def _wait_for(self, method, timeout, msg): message = msg(self.driver) else: message = msg - raise TimeoutException(message) from err + raise TimeoutException(str(message)) from err def wait_for_element(self, selector, timeout=None): """wait_for_element is shortcut to `wait_for_element_by_css_selector` @@ -534,10 +539,12 @@ def _get_firefox(self): "browser.helperApps.neverAsk.saveToDisk", "application/octet-stream", # this MIME is generic for binary ) - self._remote_url = cast(str, self._remote_url) # to satisfy type checking + if not self._remote_url and self._remote: + raise TypeError("remote_url was not provided but required for Firefox") + return ( webdriver.Remote( - command_executor=self._remote_url, + command_executor=self._remote_url, # type: ignore[reportTypeArgument] options=options, ) if self._remote @@ -633,7 +640,7 @@ def session_id(self): return self.driver.session_id @property - def server_url(self): + def server_url(self) -> str: return self._url @server_url.setter From 461cd3c21c2c7a6e3f34051b342ffac4fc3b3aa5 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 10 Apr 2025 14:02:07 -0400 Subject: [PATCH 06/12] Put back multiprocess import inside call_job_fn --- dash/background_callback/managers/diskcache_manager.py | 5 ++++- dash/testing/browser.py | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dash/background_callback/managers/diskcache_manager.py b/dash/background_callback/managers/diskcache_manager.py index ede8c6afed..877af98f89 100644 --- a/dash/background_callback/managers/diskcache_manager.py +++ b/dash/background_callback/managers/diskcache_manager.py @@ -1,6 +1,6 @@ import traceback from contextvars import copy_context -from multiprocess import Process # type: ignore + from . import BaseBackgroundCallbackManager from .._proxy_set_props import ProxySetProps @@ -117,6 +117,9 @@ def clear_cache_entry(self, key): # noinspection PyUnresolvedReferences def call_job_fn(self, key, job_fn, args, context): + # pylint: disable-next=import-outside-toplevel,no-name-in-module,import-error + from multiprocess import Process # type: ignore + # pylint: disable-next=not-callable proc = Process( target=job_fn, diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 22b12e1b9a..dd07da39ba 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -9,7 +9,6 @@ import requests from selenium import webdriver -from selenium.webdriver.remote.webdriver import BaseWebDriver from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By from selenium.webdriver.support.wait import WebDriverWait From 45781d7bfeaa9d7975c58e8e5706d069639f0659 Mon Sep 17 00:00:00 2001 From: philippe Date: Thu, 10 Apr 2025 15:44:55 -0400 Subject: [PATCH 07/12] Fix number types. --- dash/development/_py_prop_typing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dash/development/_py_prop_typing.py b/dash/development/_py_prop_typing.py index 1c3f673977..5b9c0f264b 100644 --- a/dash/development/_py_prop_typing.py +++ b/dash/development/_py_prop_typing.py @@ -183,7 +183,9 @@ def get_prop_typing( "exact": generate_shape, "string": generate_type("str"), "bool": generate_type("bool"), - "number": generate_type("typing.Union[int, float, numbers.Number]"), + "number": generate_type( + "typing.Union[typing.SupportsFloat, typing.SupportsInt, typing.SupportsComplex]" + ), "node": generate_type( "typing.Union[str, int, float, ComponentType," " typing.Sequence[typing.Union" From 0443c55a2a46a37327f08286afc334e29e0cccfd Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 11 Apr 2025 09:17:42 -0400 Subject: [PATCH 08/12] Regen metadata_test.py --- tests/unit/development/metadata_test.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/development/metadata_test.py b/tests/unit/development/metadata_test.py index 2388f13c11..439ba0f3c3 100644 --- a/tests/unit/development/metadata_test.py +++ b/tests/unit/development/metadata_test.py @@ -109,7 +109,7 @@ class Table(Component): "OptionalObjectWithExactAndNestedDescription", { "color": NotRequired[str], - "fontSize": NotRequired[typing.Union[int, float, numbers.Number]], + "fontSize": NotRequired[typing.Union[typing.SupportsFloat, typing.SupportsInt, typing.SupportsComplex]], "figure": NotRequired["OptionalObjectWithExactAndNestedDescriptionFigure"] } ) @@ -126,7 +126,7 @@ class Table(Component): "OptionalObjectWithShapeAndNestedDescription", { "color": NotRequired[str], - "fontSize": NotRequired[typing.Union[int, float, numbers.Number]], + "fontSize": NotRequired[typing.Union[typing.SupportsFloat, typing.SupportsInt, typing.SupportsComplex]], "figure": NotRequired["OptionalObjectWithShapeAndNestedDescriptionFigure"] } ) @@ -139,7 +139,7 @@ def __init__( 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, + optionalNumber: typing.Optional[typing.Union[typing.SupportsFloat, typing.SupportsInt, typing.SupportsComplex]] = None, optionalObject: typing.Optional[dict] = None, optionalString: typing.Optional[str] = None, optionalSymbol: typing.Optional[typing.Any] = None, @@ -147,9 +147,9 @@ def __init__( optionalElement: typing.Optional[ComponentType] = None, optionalMessage: typing.Optional[typing.Any] = None, optionalEnum: typing.Optional[Literal["News", "Photos"]] = None, - optionalUnion: typing.Optional[typing.Union[str, typing.Union[int, float, numbers.Number], typing.Any]] = None, - optionalArrayOf: typing.Optional[typing.Sequence[typing.Union[int, float, numbers.Number]]] = None, - optionalObjectOf: typing.Optional[typing.Dict[typing.Union[str, float, int], typing.Union[int, float, numbers.Number]]] = None, + optionalUnion: typing.Optional[typing.Union[str, typing.Union[typing.SupportsFloat, typing.SupportsInt, typing.SupportsComplex], typing.Any]] = None, + optionalArrayOf: typing.Optional[typing.Sequence[typing.Union[typing.SupportsFloat, typing.SupportsInt, typing.SupportsComplex]]] = None, + optionalObjectOf: typing.Optional[typing.Dict[typing.Union[str, float, int], typing.Union[typing.SupportsFloat, typing.SupportsInt, typing.SupportsComplex]]] = None, optionalObjectWithExactAndNestedDescription: typing.Optional["OptionalObjectWithExactAndNestedDescription"] = None, optionalObjectWithShapeAndNestedDescription: typing.Optional["OptionalObjectWithShapeAndNestedDescription"] = None, optionalAny: typing.Optional[typing.Any] = None, From 435f7ef90e4cec16212a65dac77cdbfa687a01cd Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 11 Apr 2025 09:45:49 -0400 Subject: [PATCH 09/12] Add more types to Dash --- dash/dash.py | 61 +++++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 53d7fc46e7..71e0d08ab5 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -18,10 +18,11 @@ import base64 import traceback from urllib.parse import urlparse -from typing import Any, Callable, Dict, Optional, Union, Sequence, cast +from typing import Any, Callable, Dict, Optional, Union, Sequence, cast, Literal import flask +from flask.typing import RouteCallable from importlib_metadata import version as _get_distribution_version from dash import dcc @@ -622,7 +623,7 @@ def _setup_hooks(self): if self._hooks.get_hooks("error"): self._on_error = self._hooks.HookErrorHandler(self._on_error) - def init_app(self, app=None, **kwargs): + def init_app(self, app: Optional[flask.Flask] = None, **kwargs) -> None: """Initialize the parts of Dash that require a flask app.""" config = self.config @@ -694,7 +695,7 @@ def _handle_error(_): self._setup_plotlyjs() - def _add_url(self, name, view_func, methods=("GET",)): + def _add_url(self, name: str, view_func: RouteCallable, methods=("GET",)) -> None: full_name = self.config.routes_pathname_prefix + name self.server.add_url_rule( @@ -748,11 +749,11 @@ def _setup_plotlyjs(self): self._plotlyjs_url = url @property - def layout(self): + def layout(self) -> Any: return self._layout @layout.setter - def layout(self, value): + def layout(self, value: Any): _validate.validate_layout_type(value) self._layout_is_function = callable(value) self._layout = value @@ -782,11 +783,11 @@ def _layout_value(self): return layout @property - def index_string(self): + def index_string(self) -> str: return self._index_string @index_string.setter - def index_string(self, value): + def index_string(self, value: str) -> None: checks = (_re_index_entry, _re_index_config, _re_index_scripts) _validate.validate_index("index string", checks, value) self._index_string = value @@ -861,7 +862,7 @@ def serve_reload_hash(self): } ) - def get_dist(self, libraries): + def get_dist(self, libraries: Sequence[str]) -> list: dists = [] for dist_type in ("_js_dist", "_css_dist"): resources = ComponentRegistry.get_resources(dist_type, libraries) @@ -963,7 +964,7 @@ def _generate_css_dist_html(self): ] ) - def _generate_scripts_html(self): + def _generate_scripts_html(self) -> str: # Dash renderer has dependencies like React which need to be rendered # before every other script. However, the dash renderer bundle # itself needs to be rendered after all of the component's @@ -1020,10 +1021,10 @@ def _generate_scripts_html(self): + [f"" for src in self._inline_scripts] ) - def _generate_config_html(self): + def _generate_config_html(self) -> str: return f'' - def _generate_renderer(self): + def _generate_renderer(self) -> str: return f'' def _generate_meta(self): @@ -1545,7 +1546,7 @@ def _serve_default_favicon(): pkgutil.get_data("dash", "favicon.ico"), content_type="image/x-icon" ) - def csp_hashes(self, hash_algorithm="sha256"): + def csp_hashes(self, hash_algorithm="sha256") -> Sequence[str]: """Calculates CSP hashes (sha + base64) of all inline scripts, such that one of the biggest benefits of CSP (disallowing general inline scripts) can be utilized together with Dash clientside callbacks (inline scripts). @@ -1584,7 +1585,7 @@ def _hash(script): for script in (self._inline_scripts + [self.renderer]) ] - def get_asset_url(self, path): + def get_asset_url(self, path: str) -> str: """ Return the URL for the provided `path` in the assets directory. @@ -1655,7 +1656,7 @@ def display_content(path): self.config.requests_pathname_prefix, path ) - def strip_relative_path(self, path): + def strip_relative_path(self, path: str) -> Union[str, None]: """ Return a path with `requests_pathname_prefix` and leading and trailing slashes stripped from it. Also, if None is passed in, None is returned. @@ -1707,7 +1708,9 @@ def display_content(path): ) @staticmethod - def add_startup_route(name, view_func, methods): + def add_startup_route( + name: str, view_func: RouteCallable, methods: Sequence[Literal["POST", "GET"]] + ) -> None: """ Add a route to the app to be initialized at the end of Dash initialization. Use this if the package requires a route to be added to the app, and you will not need to worry about at what point to add it. @@ -1731,7 +1734,7 @@ def add_startup_route(name, view_func, methods): Dash.STARTUP_ROUTES.append((name, view_func, methods)) - def setup_startup_routes(self): + def setup_startup_routes(self) -> None: """ Initialize the startup routes stored in STARTUP_ROUTES. """ @@ -1774,18 +1777,18 @@ def _setup_dev_tools(self, **kwargs): def enable_dev_tools( self, - debug=None, - dev_tools_ui=None, - dev_tools_props_check=None, - dev_tools_serve_dev_bundles=None, - dev_tools_hot_reload=None, - dev_tools_hot_reload_interval=None, - dev_tools_hot_reload_watch_interval=None, - dev_tools_hot_reload_max_retry=None, - dev_tools_silence_routes_logging=None, - dev_tools_disable_version_check=None, - dev_tools_prune_errors=None, - ): + debug: Optional[bool] = None, + dev_tools_ui: Optional[bool] = None, + dev_tools_props_check: Optional[bool] = None, + dev_tools_serve_dev_bundles: Optional[bool] = None, + dev_tools_hot_reload: Optional[bool] = None, + dev_tools_hot_reload_interval: Optional[int] = None, + dev_tools_hot_reload_watch_interval: Optional[int] = None, + dev_tools_hot_reload_max_retry: Optional[int] = None, + dev_tools_silence_routes_logging: Optional[bool] = None, + dev_tools_disable_version_check: Optional[bool] = None, + dev_tools_prune_errors: Optional[bool] = None, + ) -> None: """Activate the dev tools, called by `run`. If your application is served by wsgi and you want to activate the dev tools, you can call this method out of `__main__`. @@ -2275,7 +2278,7 @@ def verify_url_part(served_part, url_part, part_name): else: self.server.run(host=host, port=port, debug=debug, **flask_run_options) - def enable_pages(self): + def enable_pages(self) -> None: if not self.use_pages: return if self.pages_folder: From 395852e76c478077ea22baa5f01763243c4b7c86 Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 11 Apr 2025 10:40:33 -0400 Subject: [PATCH 10/12] Fix test typing assertions --- tests/integration/test_typing.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_typing.py b/tests/integration/test_typing.py index d6e7069698..d2894d75ca 100644 --- a/tests/integration/test_typing.py +++ b/tests/integration/test_typing.py @@ -68,8 +68,10 @@ 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 | None"' + 'Argument of type "Literal[\'\']" cannot be assigned to parameter "a_number" ', + '"__float__" is not present', + '"__int__" is not present', + '"__complex__" is not present', ], }, ), @@ -203,7 +205,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] | None"' + 'to parameter "a_tuple" of type "Tuple[SupportsFloat | SupportsInt | SupportsComplex, str] | None' ], }, ), From 102e72bb110546db81870df7c8dfbd0e5d92e10e Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 11 Apr 2025 13:25:41 -0400 Subject: [PATCH 11/12] Add typing to hooks & patch --- dash/_hooks.py | 12 +++++++----- dash/_patch.py | 53 +++++++++++++++++++++++++++++--------------------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/dash/_hooks.py b/dash/_hooks.py index 1790aa3e3d..41d5220b85 100644 --- a/dash/_hooks.py +++ b/dash/_hooks.py @@ -61,8 +61,8 @@ def add_hook( hook: str, func: _t.Callable, priority: _t.Optional[int] = None, - final=False, - data=None, + final: bool = False, + data: _t.Optional[HookDataType] = None, ): if final: existing = self._finals.get(hook) @@ -117,7 +117,7 @@ def route( name: _t.Optional[str] = None, methods: _t.Sequence[str] = ("GET",), priority: _t.Optional[int] = None, - final=False, + final: bool = False, ): """ Add a route to the Dash server. @@ -136,7 +136,7 @@ def wrap(func: _t.Callable[[], _f.Response]): return wrap - def error(self, priority: _t.Optional[int] = None, final=False): + def error(self, priority: _t.Optional[int] = None, final: bool = False): """Automatically add an error handler to the dash app.""" def _error(func: _t.Callable[[Exception], _t.Any]): @@ -145,7 +145,9 @@ def _error(func: _t.Callable[[Exception], _t.Any]): return _error - def callback(self, *args, priority: _t.Optional[int] = None, final=False, **kwargs): + def callback( + self, *args, priority: _t.Optional[int] = None, final: bool = False, **kwargs + ): """ Add a callback to all the apps with the hook installed. """ diff --git a/dash/_patch.py b/dash/_patch.py index 2f21e9ce87..f0694ac909 100644 --- a/dash/_patch.py +++ b/dash/_patch.py @@ -1,11 +1,16 @@ +from typing import List, Union, Optional, Any + + def _operation(name, location, **kwargs): return {"operation": name, "location": location, "params": dict(**kwargs)} _noop = object() +_KeyType = Union[str, int] + -def validate_slice(obj): +def validate_slice(obj: Any): if isinstance(obj, slice): raise TypeError("a slice is not a valid index for patch") @@ -19,7 +24,11 @@ class Patch: Supported prop types: Dictionaries and lists. """ - def __init__(self, location=None, parent=None): + def __init__( + self, + location: Optional[List[_KeyType]] = None, + parent: Optional["Patch"] = None, + ): if location is not None: self._location = location else: @@ -36,11 +45,11 @@ def __getstate__(self): def __setstate__(self, state): vars(self).update(state) - def __getitem__(self, item) -> "Patch": + def __getitem__(self, item: _KeyType) -> "Patch": validate_slice(item) return Patch(location=self._location + [item], parent=self) - def __getattr__(self, item) -> "Patch": + def __getattr__(self, item: _KeyType) -> "Patch": if item == "tolist": # to_json fix raise AttributeError @@ -50,16 +59,16 @@ def __getattr__(self, item) -> "Patch": return self._operations # type: ignore return self.__getitem__(item) - def __setattr__(self, key, value): + def __setattr__(self, key: _KeyType, value: Any): if key in ("_location", "_operations"): self.__dict__[key] = value else: self.__setitem__(key, value) - def __delattr__(self, item): + def __delattr__(self, item: _KeyType): self.__delitem__(item) - def __setitem__(self, key, value): + def __setitem__(self, key: _KeyType, value: Any): validate_slice(key) if value is _noop: # The += set themselves. @@ -72,11 +81,11 @@ def __setitem__(self, key, value): ) ) - def __delitem__(self, key): + def __delitem__(self, key: _KeyType): validate_slice(key) self._operations.append(_operation("Delete", self._location + [key])) - def __iadd__(self, other): + def __iadd__(self, other: Any): if isinstance(other, (list, tuple)): self.extend(other) else: @@ -85,25 +94,25 @@ def __iadd__(self, other): return self return _noop - def __isub__(self, other): + def __isub__(self, other: Any): self._operations.append(_operation("Sub", self._location, value=other)) if not self._location: return self return _noop - def __imul__(self, other): + def __imul__(self, other: Any) -> "Patch": self._operations.append(_operation("Mul", self._location, value=other)) if not self._location: return self return _noop - def __itruediv__(self, other): + def __itruediv__(self, other: Any): self._operations.append(_operation("Div", self._location, value=other)) if not self._location: return self return _noop - def __ior__(self, other): + def __ior__(self, other: Any): self.update(E=other) if not self._location: return self @@ -115,39 +124,39 @@ def __iter__(self): def __repr__(self): return f"" - def append(self, item): + def append(self, item: Any) -> None: """Add the item to the end of a list""" self._operations.append(_operation("Append", self._location, value=item)) - def prepend(self, item): + def prepend(self, item: Any) -> None: """Add the item to the start of a list""" self._operations.append(_operation("Prepend", self._location, value=item)) - def insert(self, index, item): + def insert(self, index: int, item: Any) -> None: """Add the item at the index of a list""" self._operations.append( _operation("Insert", self._location, value=item, index=index) ) - def clear(self): + def clear(self) -> None: """Remove all items in a list""" self._operations.append(_operation("Clear", self._location)) - def reverse(self): + def reverse(self) -> None: """Reversal of the order of items in a list""" self._operations.append(_operation("Reverse", self._location)) - def extend(self, item): + def extend(self, item: Union[list, tuple]) -> None: """Add all the items to the end of a list""" if not isinstance(item, (list, tuple)): raise TypeError(f"{item} should be a list or tuple") self._operations.append(_operation("Extend", self._location, value=item)) - def remove(self, item): + def remove(self, item: Any) -> None: """filter the item out of a list on the frontend""" self._operations.append(_operation("Remove", self._location, value=item)) - def update(self, E=None, **F): + def update(self, E: Any = None, **F) -> None: """Merge a dict or keyword arguments with another dictionary""" value = E or {} value.update(F) @@ -159,7 +168,7 @@ def sort(self): "sort is reserved for future use, use brackets to access this key on your object" ) - def to_plotly_json(self): + def to_plotly_json(self) -> Any: return { "__dash_patch_update": "__dash_patch_update", "operations": self._operations, From c763880aaf51b0ee5ca8efd9df31fd9851062193 Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 11 Apr 2025 15:09:49 -0400 Subject: [PATCH 12/12] Replace cast with typing --- dash/_pages.py | 9 ++++++--- dash/_utils.py | 4 ++-- dash/dash.py | 6 ++---- dash/testing/application_runners.py | 7 ++----- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/dash/_pages.py b/dash/_pages.py index ac015a5bf0..e21aab86e2 100644 --- a/dash/_pages.py +++ b/dash/_pages.py @@ -8,7 +8,6 @@ from pathlib import Path from os.path import isfile, join from urllib.parse import parse_qs, unquote -from typing import cast import flask @@ -87,8 +86,12 @@ def _infer_path(module_name, template): def _module_name_is_package(module_name): - file_path = cast(str, sys.modules[module_name].__file__) # to satisfy type checking - return module_name in sys.modules and Path(file_path).name == "__init__.py" + file_path = sys.modules[module_name].__file__ + return ( + file_path + and module_name in sys.modules + and Path(file_path).name == "__init__.py" + ) def _path_to_module_name(path): diff --git a/dash/_utils.py b/dash/_utils.py index c4505a2e61..f118e61538 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -15,7 +15,7 @@ from html import escape from functools import wraps -from typing import Union, cast +from typing import Union from .types import RendererHooks logger = logging.getLogger() @@ -121,7 +121,7 @@ def __setitem__(self, key, val): def update(self, other=None, **kwargs): # Overrides dict.update() to use __setitem__ above # Needs default `None` and `kwargs` to satisfy type checking - source = cast(dict, other) if other is not None else kwargs + source = other if other is not None else kwargs for k, v in source.items(): self[k] = v diff --git a/dash/dash.py b/dash/dash.py index 71e0d08ab5..c54c26d5be 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -18,7 +18,7 @@ import base64 import traceback from urllib.parse import urlparse -from typing import Any, Callable, Dict, Optional, Union, Sequence, cast, Literal +from typing import Any, Callable, Dict, Optional, Union, Sequence, Literal import flask @@ -448,7 +448,6 @@ def __init__( # pylint: disable=too-many-statements if name is None: caller_name = getattr(server, "name", caller_name) elif isinstance(server, bool): - name = name if name else caller_name self.server = flask.Flask(caller_name) if server else None # type: ignore else: raise ValueError("server must be a Flask app or a boolean") @@ -457,7 +456,6 @@ def __init__( # pylint: disable=too-many-statements url_base_pathname, routes_pathname_prefix, requests_pathname_prefix ) - name = cast(str, name) # to satisfy type checking self.config = AttributeDict( name=caller_name, assets_folder=os.path.join( @@ -1788,7 +1786,7 @@ def enable_dev_tools( dev_tools_silence_routes_logging: Optional[bool] = None, dev_tools_disable_version_check: Optional[bool] = None, dev_tools_prune_errors: Optional[bool] = None, - ) -> None: + ) -> bool: """Activate the dev tools, called by `run`. If your application is served by wsgi and you want to activate the dev tools, you can call this method out of `__main__`. diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index efc1d06a4c..dc88afe844 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -9,7 +9,6 @@ import logging import inspect import ctypes -from typing import cast import runpy import requests @@ -354,11 +353,9 @@ def start(self, app, start_timeout=2, cwd=None): # type: ignore[reportIncompati # app is a string chunk, we make a temporary folder to store app.R # and its relevant assets tmp_dir = "/tmp" if not self.is_windows else os.getenv("TEMP") - tmp_dir = cast(str, tmp_dir) # to satisfy type checking + tmp_dir = str(tmp_dir) # to satisfy type checking hex_id = uuid.uuid4().hex - self._tmp_app_path = cast( - str, os.path.join(tmp_dir, hex_id) - ) # to satisfy type checking + self._tmp_app_path = os.path.join(tmp_dir, hex_id) try: os.mkdir(self.tmp_app_path) # type: ignore[reportArgumentType] except OSError: