diff --git a/CHANGELOG.md b/CHANGELOG.md index 962b384db7..7588798069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Fixed - [#3690](https://github.com/plotly/dash/pull/3690) Fixes Input when min or max is set to None - [#3723](https://github.com/plotly/dash/pull/3723) Fix misaligned `dcc.Slider` marks when some labels are empty strings +- [#3738](https://github.com/plotly/dash/pull/3738) Add missing `stacklevel=2` to `warnings.warn()` calls so warnings report the caller's location instead of internal Dash source lines - [#3740](https://github.com/plotly/dash/pull/3740) Fix cannot tab into dropdowns in Safari ## [4.1.0] - 2026-03-23 diff --git a/dash/_callback_context.py b/dash/_callback_context.py index 09faf6f9a3..81e06036fb 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -9,10 +9,9 @@ from . import exceptions from ._utils import AttributeDict, stringify_id - -context_value: contextvars.ContextVar[ - typing.Dict[str, typing.Any] -] = contextvars.ContextVar("callback_context") +context_value: contextvars.ContextVar[typing.Dict[str, typing.Any]] = ( + contextvars.ContextVar("callback_context") +) context_value.set({}) @@ -177,6 +176,7 @@ def outputs_list(self): warnings.warn( "outputs_list is deprecated, use outputs_grouping instead", DeprecationWarning, + stacklevel=2, ) return getattr(_get_context_value(), "outputs_list", []) @@ -188,6 +188,7 @@ def inputs_list(self): warnings.warn( "inputs_list is deprecated, use args_grouping instead", DeprecationWarning, + stacklevel=2, ) return getattr(_get_context_value(), "inputs_list", []) @@ -199,6 +200,7 @@ def states_list(self): warnings.warn( "states_list is deprecated, use args_grouping instead", DeprecationWarning, + stacklevel=2, ) return getattr(_get_context_value(), "states_list", []) diff --git a/dash/dash.py b/dash/dash.py index 340c112569..b40a6544eb 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -663,7 +663,8 @@ def __init__( # pylint: disable=too-many-statements if self.__class__.__name__ == "JupyterDash": warnings.warn( "JupyterDash is deprecated, use Dash instead.\n" - "See https://dash.plotly.com/dash-in-jupyter for more details." + "See https://dash.plotly.com/dash-in-jupyter for more details.", + stacklevel=2, ) self.setup_startup_routes() @@ -1139,9 +1140,11 @@ def _generate_css_dist_html(self): return "\n".join( [ - format_tag("link", link, opened=True) - if isinstance(link, dict) - else f'' + ( + format_tag("link", link, opened=True) + if isinstance(link, dict) + else f'' + ) for link in (external_links + links) ] ) @@ -1195,9 +1198,11 @@ def _generate_scripts_html(self) -> str: return "\n".join( [ - format_tag("script", src) - if isinstance(src, dict) - else f'' + ( + format_tag("script", src) + if isinstance(src, dict) + else f'' + ) for src in srcs ] + [f"" for src in self._inline_scripts] @@ -1674,9 +1679,11 @@ def _setup_server(self): # For each callback function, if the hidden parameter uses the default value None, # replace it with the actual value of the self.config.hide_all_callbacks. self._callback_list = [ - {**_callback, "hidden": self.config.get("hide_all_callbacks", False)} - if _callback.get("hidden") is None - else _callback + ( + {**_callback, "hidden": self.config.get("hide_all_callbacks", False)} + if _callback.get("hidden") is None + else _callback + ) for _callback in self._callback_list ] @@ -2186,9 +2193,7 @@ def enable_dev_tools( # pylint: disable=too-many-branches pkg_dir = ( package.submodule_search_locations[0] if package.submodule_search_locations - else os.path.dirname(package.origin) - if package.origin - else None + else os.path.dirname(package.origin) if package.origin else None ) if pkg_dir and "dash/dash" in pkg_dir: component_packages_dist[i : i + 1] = [ @@ -2517,14 +2522,12 @@ def run( def verify_url_part(served_part, url_part, part_name): if served_part != url_part: - raise ProxyError( - f""" + raise ProxyError(f""" {part_name}: {url_part} is incompatible with the proxy: {proxy} To see your app at {proxied_url.geturl()}, you must use {part_name}: {served_part} - """ - ) + """) verify_url_part(served_url.scheme, protocol, "protocol") verify_url_part(served_url.hostname, host, "host") @@ -2636,16 +2639,16 @@ async def update(pathname_, search_, **states): if not self.config.suppress_callback_exceptions: self.validation_layout = html.Div( [ - asyncio.run(execute_async_function(page["layout"])) - if callable(page["layout"]) - else page["layout"] + ( + asyncio.run(execute_async_function(page["layout"])) + if callable(page["layout"]) + else page["layout"] + ) for page in _pages.PAGE_REGISTRY.values() ] + [ # pylint: disable=not-callable - self.layout() - if callable(self.layout) - else self.layout + self.layout() if callable(self.layout) else self.layout ] ) if _ID_CONTENT not in self.validation_layout: @@ -2702,15 +2705,15 @@ def update(pathname_, search_, **states): if not isinstance(layout, list): layout = [ # pylint: disable=not-callable - self.layout() - if callable(self.layout) - else self.layout + self.layout() if callable(self.layout) else self.layout ] self.validation_layout = html.Div( [ - page["layout"]() - if callable(page["layout"]) - else page["layout"] + ( + page["layout"]() + if callable(page["layout"]) + else page["layout"] + ) for page in _pages.PAGE_REGISTRY.values() ] + layout diff --git a/dash/development/_jl_components_generation.py b/dash/development/_jl_components_generation.py index 771ac0180a..b9b5aec21a 100644 --- a/dash/development/_jl_components_generation.py +++ b/dash/development/_jl_components_generation.py @@ -355,9 +355,11 @@ def nothing_or_string(v): external_url=nothing_or_string(resource.get("external_url", "")), dynamic=str(resource.get("dynamic", "nothing")).lower(), type=metatype, - async_string=":{}".format(str(resource.get("async")).lower()) - if "async" in resource.keys() - else "nothing", + async_string=( + ":{}".format(str(resource.get("async")).lower()) + if "async" in resource.keys() + else "nothing" + ), ) for resource in resources ] @@ -468,7 +470,8 @@ def generate_class_string(name, props, description, project_shortname, prefix): ( 'WARNING: prop "{}" in component "{}" is a Julia keyword' " - REMOVED FROM THE JULIA COMPONENT" - ).format(item, name) + ).format(item, name), + stacklevel=2, ) default_paramtext += ", ".join(":{}".format(p) for p in prop_keys) diff --git a/dash/development/_r_components_generation.py b/dash/development/_r_components_generation.py index 18ac60d5d8..54d7b04dc2 100644 --- a/dash/development/_r_components_generation.py +++ b/dash/development/_r_components_generation.py @@ -11,7 +11,6 @@ from ._all_keywords import r_keywords from ._py_components_generation import reorder_props - # Declaring longer string templates as globals to improve # readability, make method logic clearer to anyone inspecting # code below @@ -216,7 +215,8 @@ def generate_class_string(name, props, project_shortname, prefix): ( 'WARNING: prop "{}" in component "{}" is an R keyword' " - REMOVED FROM THE R COMPONENT" - ).format(item, name) + ).format(item, name), + stacklevel=2, ) default_argtext += ", ".join("{}=NULL".format(p) for p in prop_keys) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 02579ff2e2..eb37a58359 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -15,22 +15,14 @@ rd = random.Random(0) _deprecated_components = { - "dash_core_components": { - "LogoutButton": textwrap.dedent( - """ + "dash_core_components": {"LogoutButton": textwrap.dedent(""" The Logout Button is no longer used with Dash Enterprise and can be replaced with a html.Button or html.A. eg: html.A(href=os.getenv('DASH_LOGOUT_URL')) - """ - ) - }, - "dash_table": { - "DataTable": textwrap.dedent( - """ + """)}, + "dash_table": {"DataTable": textwrap.dedent(""" The dash_table.DataTable will be removed from the builtin dash components in a future major version. We recommend using dash-ag-grid as a replacement. Install with `pip install dash[ag-grid]`. - """ - ) - }, + """)}, } @@ -39,9 +31,9 @@ class ComponentRegistry: """Holds a registry of the namespaces used by components.""" registry = OrderedSet() - children_props: typing.DefaultDict[ - str, typing.Dict[str, typing.Any] - ] = collections.defaultdict(dict) + children_props: typing.DefaultDict[str, typing.Dict[str, typing.Any]] = ( + collections.defaultdict(dict) + ) namespace_to_package: typing.Dict[str, str] = {} @classmethod @@ -221,26 +213,22 @@ def _set_random_id(self): kind = f"`{self._namespace}.{self._type}`" # pylint: disable=no-member if getattr(self, "persistence", False): - raise RuntimeError( - f""" + raise RuntimeError(f""" Attempting to use an auto-generated ID with the `persistence` prop. This is prohibited because persistence is tied to component IDs and auto-generated IDs can easily change. Please assign an explicit ID to this {kind} component. - """ - ) + """) if "dash_snapshots" in sys.modules: - raise RuntimeError( - f""" + raise RuntimeError(f""" Attempting to use an auto-generated ID in an app with `dash_snapshots`. This is prohibited because snapshots saves the whole app layout, including component IDs, and auto-generated IDs can easily change. Callbacks referencing the new IDs will not work with old snapshots. Please assign an explicit ID to this {kind} component. - """ - ) + """) v = str(uuid.UUID(int=rd.randint(0, 2**128))) setattr(self, "id", v) @@ -451,7 +439,9 @@ def _validate_deprecation(self): _ns = getattr(self, "_namespace", "") deprecation_message = _deprecated_components.get(_ns, {}).get(_type) if deprecation_message: - warnings.warn(DeprecationWarning(textwrap.dedent(deprecation_message))) + warnings.warn( + DeprecationWarning(textwrap.dedent(deprecation_message)), stacklevel=2 + ) ComponentSingleType = typing.Union[str, int, float, Component, None] diff --git a/dash/resources.py b/dash/resources.py index e197b6753e..cc392701b1 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -9,7 +9,6 @@ from .development.base_component import ComponentRegistry from . import exceptions - # ResourceType has `async` key, use the init form to be able to provide it. ResourceType = _tx.TypedDict( "ResourceType", @@ -59,12 +58,10 @@ def _filter_resources( filtered_resource["dynamic"] = s["dynamic"] if "async" in s: if "dynamic" in s: - raise exceptions.ResourceException( - f""" + raise exceptions.ResourceException(f""" Can't have both 'dynamic' and 'async'. {json.dumps(filtered_resource)} - """ - ) + """) # Async assigns a value dynamically to 'dynamic' # based on the value of 'async' and config.eager_loading @@ -111,16 +108,15 @@ def _filter_resources( "or `app.css.append_css`, use `external_scripts` " "or `external_stylesheets` instead.\n" "See https://dash.plotly.com/external-resources" - ) + ), + stacklevel=2, ) continue else: - raise exceptions.ResourceException( - f""" + raise exceptions.ResourceException(f""" {json.dumps(filtered_resource)} does not have a relative_package_path, absolute_path, or an external_url. - """ - ) + """) if valid_resource: filtered_resources.append(filtered_resource) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 6287e2af2e..55d5d3f0d4 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -32,7 +32,6 @@ from dash.testing.errors import DashAppLoadingError, BrowserError, TestingTimeoutError from dash.testing.consts import SELENIUM_GRID_DEFAULT - logger = logging.getLogger(__name__) @@ -179,8 +178,7 @@ def percy_snapshot( ) if convert_canvases: - self.driver.execute_script( - """ + self.driver.execute_script(""" const stash = window._canvasStash = []; Array.from(document.querySelectorAll('canvas')).forEach(c => { const i = document.createElement('img'); @@ -194,8 +192,7 @@ def percy_snapshot( c.parentElement.insertBefore(i, c); c.parentElement.removeChild(c); }); - """ - ) + """) # NEW: Use percy-python-selenium SDK try: @@ -208,8 +205,7 @@ def percy_snapshot( logger.warning("Percy snapshot failed: %s", err) if convert_canvases: - self.driver.execute_script( - """ + self.driver.execute_script(""" const stash = window._canvasStash; Array.from( document.querySelectorAll('img[data-canvasnum]') @@ -219,8 +215,7 @@ def percy_snapshot( i.parentElement.removeChild(i); }); delete window._canvasStash; - """ - ) + """) def take_snapshot(self, name: str): """Hook method to take snapshot when a selenium test fails. The @@ -629,7 +624,10 @@ def get_logs(self): for entry in self.driver.get_log("browser") if entry["timestamp"] > self._last_ts ] - warnings.warn("get_logs always return None with webdrivers other than Chrome") + warnings.warn( + "get_logs always return None with webdrivers other than Chrome", + stacklevel=2, + ) return None def reset_log_timestamp(self):