From 254a697de2db16d07a4d100295b6d89b1b2928ec Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 10 Apr 2026 23:57:10 -0700 Subject: [PATCH 1/2] Add missing stacklevel=2 to 9 warnings.warn() calls Without stacklevel, warnings.warn() reports the location of the warn() call itself rather than the caller's location, making it harder for users to identify the source of the warning in their own code. This adds stacklevel=2 to all 9 warnings.warn() calls across 7 files that were missing it. --- dash/_callback_context.py | 3 +++ dash/dash.py | 3 ++- dash/development/_jl_components_generation.py | 3 ++- dash/development/_r_components_generation.py | 3 ++- dash/development/base_component.py | 2 +- dash/resources.py | 3 ++- dash/testing/browser.py | 2 +- 7 files changed, 13 insertions(+), 6 deletions(-) diff --git a/dash/_callback_context.py b/dash/_callback_context.py index 09faf6f9a3..f4e12693a7 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -177,6 +177,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 +189,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 +201,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 122cf54dd6..5666dc0ce7 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -643,7 +643,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() diff --git a/dash/development/_jl_components_generation.py b/dash/development/_jl_components_generation.py index 771ac0180a..4b7a7be4e1 100644 --- a/dash/development/_jl_components_generation.py +++ b/dash/development/_jl_components_generation.py @@ -468,7 +468,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..e392dc9aca 100644 --- a/dash/development/_r_components_generation.py +++ b/dash/development/_r_components_generation.py @@ -216,7 +216,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..0e93af5e3e 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -451,7 +451,7 @@ 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..62fddb24af 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -111,7 +111,8 @@ 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: diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 8a82399a97..09af45c76d 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -624,7 +624,7 @@ 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): From 081281d73f4db04c26a5b1a02c29f4d83a8365a7 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 17 Apr 2026 01:17:44 -0700 Subject: [PATCH 2/2] Run black formatter and add changelog entry --- CHANGELOG.md | 1 + dash/_callback_context.py | 7 +-- dash/dash.py | 58 ++++++++++--------- dash/development/_jl_components_generation.py | 8 ++- dash/development/_r_components_generation.py | 1 - dash/development/base_component.py | 38 +++++------- dash/resources.py | 13 ++--- dash/testing/browser.py | 18 +++--- 8 files changed, 65 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a78eabf786..46217dfe61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,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 ## [4.1.0] - 2026-03-23 diff --git a/dash/_callback_context.py b/dash/_callback_context.py index f4e12693a7..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({}) diff --git a/dash/dash.py b/dash/dash.py index 5666dc0ce7..6ba3979ef2 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -1118,9 +1118,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) ] ) @@ -1174,9 +1176,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] @@ -1653,9 +1657,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 ] @@ -2165,9 +2171,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] = [ @@ -2496,14 +2500,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") @@ -2615,16 +2617,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: @@ -2681,15 +2683,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 4b7a7be4e1..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 ] diff --git a/dash/development/_r_components_generation.py b/dash/development/_r_components_generation.py index e392dc9aca..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 diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 0e93af5e3e..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)), stacklevel=2) + 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 62fddb24af..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 @@ -116,12 +113,10 @@ def _filter_resources( ) 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 09af45c76d..05af2ec1f4 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -33,7 +33,6 @@ from dash.testing.errors import DashAppLoadingError, BrowserError, TestingTimeoutError from dash.testing.consts import SELENIUM_GRID_DEFAULT - logger = logging.getLogger(__name__) @@ -187,8 +186,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'); @@ -202,8 +200,7 @@ def percy_snapshot( c.parentElement.insertBefore(i, c); c.parentElement.removeChild(c); }); - """ - ) + """) try: self.percy_runner.snapshot(name=name, widths=widths) @@ -213,8 +210,7 @@ def percy_snapshot( raise err if convert_canvases: - self.driver.execute_script( - """ + self.driver.execute_script(""" const stash = window._canvasStash; Array.from( document.querySelectorAll('img[data-canvasnum]') @@ -224,8 +220,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 @@ -624,7 +619,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", stacklevel=2) + warnings.warn( + "get_logs always return None with webdrivers other than Chrome", + stacklevel=2, + ) return None def reset_log_timestamp(self):