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):