Skip to content

Commit b57e0f5

Browse files
authored
Merge pull request #3488 from plotly/fix-pkgutil
Fix pkgutil.loader removal
2 parents 0221a97 + 20589e3 commit b57e0f5

File tree

23 files changed

+312
-199
lines changed

23 files changed

+312
-199
lines changed

.github/workflows/testing.yml

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

136136
- name: Run typing tests
137137
run: |
138-
cd tests
139-
pytest compliance/test_typing.py
138+
pytest tests/compliance/test_typing.py
140139
141140
background-callbacks:
142141
name: Run Background & Async Callback Tests (Python ${{ matrix.python-version }})

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ 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
2223

2324
## [3.2.0] - 2025-07-31
2425

dash/_callback.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import hashlib
33
from functools import wraps
44

5-
from typing import Callable, Optional, Any, List, Tuple, Union
5+
from typing import Callable, Optional, Any, List, Tuple, Union, Dict
66

77

88
import asyncio
@@ -59,10 +59,10 @@ def _invoke_callback(func, *args, **kwargs): # used to mark the frame for the d
5959
return func(*args, **kwargs) # %% callback invoked %%
6060

6161

62-
GLOBAL_CALLBACK_LIST = []
63-
GLOBAL_CALLBACK_MAP = {}
64-
GLOBAL_INLINE_SCRIPTS = []
65-
GLOBAL_API_PATHS = {}
62+
GLOBAL_CALLBACK_LIST: List[Any] = []
63+
GLOBAL_CALLBACK_MAP: Dict[str, Any] = {}
64+
GLOBAL_INLINE_SCRIPTS: List[Any] = []
65+
GLOBAL_API_PATHS: Dict[str, Any] = {}
6666

6767

6868
# pylint: disable=too-many-locals,too-many-arguments
@@ -177,7 +177,7 @@ def callback(
177177
callbacks in the Dash devtools.
178178
"""
179179

180-
background_spec = None
180+
background_spec: Any = None
181181

182182
config_prevent_initial_callbacks = _kwargs.pop(
183183
"config_prevent_initial_callbacks", False
@@ -186,7 +186,7 @@ def callback(
186186
callback_list = _kwargs.pop("callback_list", GLOBAL_CALLBACK_LIST)
187187

188188
if background:
189-
background_spec: Any = {
189+
background_spec = {
190190
"interval": interval,
191191
}
192192

@@ -687,7 +687,7 @@ def add_context(*args, **kwargs):
687687
args, kwargs, inputs_state_indices, has_output, insert_output
688688
)
689689

690-
response: dict = {"multi": True}
690+
response: dict = {"multi": True} # type: ignore
691691

692692
jsonResponse = None
693693
try:
@@ -759,7 +759,7 @@ async def async_add_context(*args, **kwargs):
759759
args, kwargs, inputs_state_indices, has_output, insert_output
760760
)
761761

762-
response: dict = {"multi": True}
762+
response = {"multi": True}
763763

764764
try:
765765
if background is not None:

dash/_callback_context.py

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

1212

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

1618

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
@@ -27,7 +27,9 @@
2727

2828
# pylint: disable=too-few-public-methods
2929
class _Hook(_tx.Generic[HookDataType]):
30-
def __init__(self, func, priority=0, final=False, data: HookDataType = None):
30+
def __init__(
31+
self, func, priority=0, final=False, data: _t.Optional[HookDataType] = None
32+
):
3133
self.func = func
3234
self.final = final
3335
self.data = data
@@ -39,7 +41,7 @@ def __call__(self, *args, **kwargs):
3941

4042
class _Hooks:
4143
def __init__(self) -> None:
42-
self._ns = {
44+
self._ns: _t.Dict[str, _t.List[_t.Any]] = {
4345
"setup": [],
4446
"layout": [],
4547
"routes": [],
@@ -49,14 +51,14 @@ def __init__(self) -> None:
4951
"custom_data": [],
5052
"dev_tools": [],
5153
}
52-
self._js_dist = []
53-
self._css_dist = []
54+
self._js_dist: _t.List[_t.Any] = []
55+
self._css_dist: _t.List[_t.Any] = []
5456
self._clientside_callbacks: _t.List[
5557
_t.Tuple[ClientsideFuncType, _t.Any, _t.Any]
5658
] = []
5759

5860
# final hooks are a single hook added to the end of regular hooks.
59-
self._finals = {}
61+
self._finals: _t.Dict[str, _t.Any] = {}
6062

6163
def add_hook(
6264
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)