Skip to content

Commit 02e0be5

Browse files
authored
Merge branch 'dev' into bring-your-own-server
2 parents a10f86b + b3b148c commit 02e0be5

25 files changed

Lines changed: 394 additions & 200 deletions

.github/workflows/testing.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,7 @@ jobs:
139139

140140
- name: Run typing tests
141141
run: |
142-
cd tests
143-
pytest compliance/test_typing.py
142+
pytest tests/compliance/test_typing.py
144143
145144
background-callbacks:
146145
name: Run Background & Async Callback Tests (Python ${{ matrix.python-version }})

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ This project adheres to [Semantic Versioning](https://semver.org/).
1919
- [#3395](https://github.com/plotly/dash/pull/3395) Fix Components added through set_props() cannot trigger related callback functions. Fix [#3316](https://github.com/plotly/dash/issues/3316)
2020
- [#3415](https://github.com/plotly/dash/pull/3415) Fix the error triggered when only a single no_update is returned for client-side callback functions with multiple Outputs. Fix [#3366](https://github.com/plotly/dash/issues/3366)
2121
- [#3416](https://github.com/plotly/dash/issues/3416) Fix DeprecationWarning in dash/_jupyter.py by migrating from deprecated ipykernel.comm.Comm to comm module
22+
- [#3488](https://github.com/plotly/dash/pull/3488) Fix pkgutil.find_loader removal in Python 3.14
23+
24+
## Deprecated
25+
- [#3482](https://github.com/plotly/dash/pull/3482) Deprecate dash_table.DataTable with replacement from `dash[ag-grid]` extra requirement.
2226

2327
## [3.2.0] - 2025-07-31
2428

dash/_callback.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
from functools import wraps
33
import collections
44
import hashlib
5+
6+
from functools import wraps
7+
8+
from typing import Callable, Optional, Any, List, Tuple, Union, Dict
9+
510
import asyncio
611

712
from .dependencies import (
@@ -56,10 +61,10 @@ def _invoke_callback(func, *args, **kwargs): # used to mark the frame for the d
5661
return func(*args, **kwargs) # %% callback invoked %%
5762

5863

59-
GLOBAL_CALLBACK_LIST = []
60-
GLOBAL_CALLBACK_MAP = {}
61-
GLOBAL_INLINE_SCRIPTS = []
62-
GLOBAL_API_PATHS = {}
64+
GLOBAL_CALLBACK_LIST: List[Any] = []
65+
GLOBAL_CALLBACK_MAP: Dict[str, Any] = {}
66+
GLOBAL_INLINE_SCRIPTS: List[Any] = []
67+
GLOBAL_API_PATHS: Dict[str, Any] = {}
6368

6469

6570
# pylint: disable=too-many-locals,too-many-arguments
@@ -173,7 +178,9 @@ def callback(
173178
Note that the endpoint will not appear in the list of registered
174179
callbacks in the Dash devtools.
175180
"""
176-
background_spec = None
181+
182+
background_spec: Any = None
183+
177184

178185
config_prevent_initial_callbacks = _kwargs.pop(
179186
"config_prevent_initial_callbacks", False
@@ -182,7 +189,7 @@ def callback(
182189
callback_list = _kwargs.pop("callback_list", GLOBAL_CALLBACK_LIST)
183190

184191
if background:
185-
background_spec: Any = {
192+
background_spec = {
186193
"interval": interval,
187194
}
188195

@@ -686,7 +693,7 @@ def add_context(*args, **kwargs):
686693
args, kwargs, inputs_state_indices, has_output, insert_output
687694
)
688695

689-
response: dict = {"multi": True}
696+
response: dict = {"multi": True} # type: ignore
690697
jsonResponse: Optional[str] = None
691698
try:
692699
if background is not None:
@@ -758,7 +765,7 @@ async def async_add_context(*args, **kwargs):
758765
args, kwargs, inputs_state_indices, has_output, insert_output
759766
)
760767

761-
response: dict = {"multi": True}
768+
response = {"multi": True}
762769

763770
try:
764771
if background is not None:

dash/_callback_context.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
from ._utils import AttributeDict, stringify_id
1010

1111

12-
context_value = contextvars.ContextVar("callback_context")
12+
context_value: contextvars.ContextVar[
13+
typing.Dict[str, typing.Any]
14+
] = contextvars.ContextVar("callback_context")
1315
context_value.set({})
1416

1517

dash/_dash_renderer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import os
2+
from typing import Any, List, Dict
23

34
__version__ = "2.2.0"
45

56
_available_react_versions = {"18.3.1", "18.2.0", "16.14.0"}
67
_available_reactdom_versions = {"18.3.1", "18.2.0", "16.14.0"}
7-
_js_dist_dependencies = [] # to be set by _set_react_version
8+
_js_dist_dependencies: List[Dict[str, Any]] = [] # to be set by _set_react_version
89

910

1011
def _set_react_version(v_react, v_reactdom=None):

dash/_get_app.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
from contextvars import ContextVar, copy_context
44
from textwrap import dedent
5+
from typing import Any, Optional
56

6-
APP = None
7+
APP: Optional[Any] = None
78

8-
app_context = ContextVar("dash_app_context")
9+
app_context: ContextVar[Any] = ContextVar("dash_app_context")
910

1011

1112
def with_app_context(func):

dash/_hooks.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626

2727
# pylint: disable=too-few-public-methods
2828
class _Hook(_tx.Generic[HookDataType]):
29-
def __init__(self, func, priority=0, final=False, data: HookDataType = None):
29+
def __init__(
30+
self, func, priority=0, final=False, data: _t.Optional[HookDataType] = None
31+
):
3032
self.func = func
3133
self.final = final
3234
self.data = data
@@ -38,7 +40,7 @@ def __call__(self, *args, **kwargs):
3840

3941
class _Hooks:
4042
def __init__(self) -> None:
41-
self._ns = {
43+
self._ns: _t.Dict[str, _t.List[_t.Any]] = {
4244
"setup": [],
4345
"layout": [],
4446
"routes": [],
@@ -48,14 +50,14 @@ def __init__(self) -> None:
4850
"custom_data": [],
4951
"dev_tools": [],
5052
}
51-
self._js_dist = []
52-
self._css_dist = []
53+
self._js_dist: _t.List[_t.Any] = []
54+
self._css_dist: _t.List[_t.Any] = []
5355
self._clientside_callbacks: _t.List[
5456
_t.Tuple[ClientsideFuncType, _t.Any, _t.Any]
5557
] = []
5658

5759
# final hooks are a single hook added to the end of regular hooks.
58-
self._finals = {}
60+
self._finals: _t.Dict[str, _t.Any] = {}
5961

6062
def add_hook(
6163
self,

dash/_jupyter.py

Lines changed: 87 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# type: ignore
21
import asyncio
32
import io
43
import inspect
@@ -10,29 +9,83 @@
109
import threading
1110
import time
1211

13-
from typing import Optional
12+
from typing import Optional, Any
1413
from typing_extensions import Literal
1514

1615
from werkzeug.serving import make_server
1716

18-
1917
try:
20-
from IPython import get_ipython
21-
from IPython.display import IFrame, display, Javascript
22-
from IPython.core.display import HTML
23-
from IPython.core.ultratb import FormattedTB
24-
from retrying import retry
25-
from comm import create_comm
26-
import nest_asyncio
18+
from IPython import get_ipython # type: ignore[attr-defined]
19+
from IPython.display import IFrame, display, Javascript # type: ignore[import-not-found]
20+
from IPython.core.display import HTML # type: ignore[import-not-found]
21+
from IPython.core.ultratb import FormattedTB # type: ignore[import-not-found]
22+
from retrying import retry # type: ignore[import-untyped]
23+
from comm import create_comm # type: ignore[import-not-found]
24+
import nest_asyncio # type: ignore[import-untyped]
2725

28-
import requests
26+
import requests # type: ignore[import-untyped]
2927

30-
_dash_comm = create_comm(target_name="dash")
28+
_dash_comm = create_comm(target_name="dash") # type: ignore[misc]
3129
_dep_installed = True
3230
except ImportError:
3331
_dep_installed = False
34-
_dash_comm = None
35-
get_ipython = lambda: None
32+
_dash_comm = None # type: ignore[assignment]
33+
34+
# Stub implementations for when dependencies are not installed
35+
def get_ipython(): # type: ignore[misc]
36+
return None
37+
38+
# pylint: disable=unused-argument
39+
def retry(*args: Any, **kwargs: Any): # type: ignore[misc]
40+
def decorator(func: Any) -> Any:
41+
return func
42+
43+
return decorator
44+
45+
# pylint: disable=unused-argument,too-few-public-methods
46+
class IFrame: # type: ignore[no-redef]
47+
def __init__(self, *args: Any, **kwargs: Any) -> None:
48+
pass
49+
50+
# pylint: disable=unused-argument,too-few-public-methods
51+
def display(*args: Any, **kwargs: Any) -> None: # type: ignore[misc]
52+
pass
53+
54+
# pylint: disable=unused-argument,too-few-public-methods
55+
class Javascript: # type: ignore[no-redef]
56+
def __init__(self, *args: Any, **kwargs: Any) -> None:
57+
pass
58+
59+
# pylint: disable=unused-argument,too-few-public-methods
60+
class HTML: # type: ignore[no-redef]
61+
def __init__(self, *args: Any, **kwargs: Any) -> None:
62+
pass
63+
64+
# pylint: disable=unused-argument,too-few-public-methods
65+
class FormattedTB: # type: ignore[no-redef]
66+
def __init__(self, *args: Any, **kwargs: Any) -> None:
67+
pass
68+
69+
def __call__(self, *args: Any, **kwargs: Any) -> None:
70+
pass
71+
72+
# pylint: disable=unused-argument,too-few-public-methods
73+
class _RequestsModule: # type: ignore[misc]
74+
class ConnectionError(Exception):
75+
pass
76+
77+
def get(self, *args: Any, **kwargs: Any) -> Any:
78+
return None
79+
80+
requests = _RequestsModule() # type: ignore[assignment]
81+
82+
# pylint: disable=unused-argument,too-few-public-methods
83+
class _NestAsyncioModule: # type: ignore[misc]
84+
@staticmethod
85+
def apply(*args: Any, **kwargs: Any) -> None:
86+
pass
87+
88+
nest_asyncio = _NestAsyncioModule() # type: ignore[assignment]
3689

3790
JupyterDisplayMode = Literal["inline", "external", "jupyterlab", "tab", "_none"]
3891

@@ -44,7 +97,7 @@ def _get_skip(error: Exception):
4497

4598
tb = error.__traceback__
4699
skip = 1
47-
while tb.tb_next is not None:
100+
while tb is not None and tb.tb_next is not None:
48101
skip += 1
49102
tb = tb.tb_next
50103
if tb.tb_frame.f_code is _invoke_callback.__code__:
@@ -89,9 +142,9 @@ def convert(name, locals=locals, formatarg=formatarg, formatvalue=formatvalue):
89142
return "(\n " + ",\n ".join(specs) + "\n)"
90143

91144

92-
_jupyter_config = {}
145+
_jupyter_config: Any = {}
93146

94-
_caller = {}
147+
_caller: Any = {}
95148

96149

97150
def _send_jupyter_config_comm_request():
@@ -102,9 +155,10 @@ def _send_jupyter_config_comm_request():
102155
ipython is not None
103156
and hasattr(ipython, "kernel")
104157
and ipython.kernel is not None
158+
and _dash_comm is not None
105159
):
106160
_caller["parent"] = ipython.kernel.get_parent()
107-
_dash_comm.send({"type": "base_url_request"})
161+
_dash_comm.send({"type": "base_url_request"}) # type: ignore[attr-defined]
108162

109163

110164
def _jupyter_comm_response_received():
@@ -121,19 +175,19 @@ def _request_jupyter_config(timeout=2):
121175
_send_jupyter_config_comm_request()
122176

123177
# Get shell and kernel
124-
shell = get_ipython()
125-
kernel = shell.kernel
178+
shell = ipython
179+
kernel = shell.kernel # type: ignore[attr-defined]
126180

127181
# Start capturing shell events to replay later
128182
captured_events = []
129183

130184
def capture_event(stream, ident, parent):
131185
captured_events.append((stream, ident, parent))
132186

133-
kernel.shell_handlers["execute_request"] = capture_event
187+
kernel.shell_handlers["execute_request"] = capture_event # type: ignore[attr-defined]
134188

135189
# increment execution count to avoid collision error
136-
shell.execution_count += 1
190+
shell.execution_count += 1 # type: ignore[attr-defined]
137191

138192
# Allow kernel to execute comms until we receive the jupyter configuration comm
139193
# response
@@ -181,7 +235,7 @@ class JupyterDash:
181235
alive_token = str(uuid.uuid4())
182236
inline_exceptions: bool = True
183237

184-
_servers = {}
238+
_servers: Any = {}
185239

186240
def infer_jupyter_proxy_config(self):
187241
"""
@@ -343,7 +397,7 @@ def run_app(
343397
except ImportError:
344398
pass
345399

346-
err_q = queue.Queue()
400+
err_q: Any = queue.Queue()
347401

348402
server = make_server(host, port, app.server, threaded=True, processes=0)
349403
logging.getLogger("werkzeug").setLevel(logging.ERROR)
@@ -422,7 +476,7 @@ def wait_for_app():
422476
@staticmethod
423477
def _display_in_colab(dashboard_url, port, mode, width, height):
424478
# noinspection PyUnresolvedReferences
425-
from google.colab import output # pylint: disable=E0401,E0611,C0415
479+
from google.colab import output # type: ignore[import-not-found] # pylint: disable=E0401,E0611,C0415
426480

427481
if mode == "inline":
428482
output.serve_kernel_port_as_iframe(port, width=width, height=height)
@@ -444,13 +498,14 @@ def _display_in_jupyter(dashboard_url, port, mode, width, height):
444498
elif mode == "jupyterlab":
445499
# Update front-end extension
446500
# FIXME valid only in jupyterlab but accepted in regular notebooks show nothing.
447-
_dash_comm.send(
448-
{
449-
"type": "show",
450-
"port": port,
451-
"url": dashboard_url,
452-
}
453-
)
501+
if _dash_comm is not None:
502+
_dash_comm.send( # type: ignore[attr-defined]
503+
{
504+
"type": "show",
505+
"port": port,
506+
"url": dashboard_url,
507+
}
508+
)
454509

455510
@staticmethod
456511
def serve_alive():

0 commit comments

Comments
 (0)