Skip to content

Commit 5c7287f

Browse files
authoredSep 12, 2024··
Merge pull request #3000 from plotly/master-2.18.1
Master 2.18.1
2 parents 851721b + cf596f4 commit 5c7287f

File tree

13 files changed

+1504
-1188
lines changed

13 files changed

+1504
-1188
lines changed
 

‎CHANGELOG.md

+18
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5+
## [2.18.1] - 2024-09-12
6+
7+
## Fixed
8+
9+
- [#2987](https://github.com/plotly/dash/pull/2987) Fix multioutput requiring same number of no_update. Fixes [#2986](https://github.com/plotly/dash/issues/2986)
10+
- [2988](https://github.com/plotly/dash/pull/2988) Fix error handler and grouped outputs. Fixes [#2983](https://github.com/plotly/dash/issues/2983)
11+
- [#2841](https://github.com/plotly/dash/pull/2841) Fix typing on Dash init.
12+
- [#1548](https://github.com/plotly/dash/pull/1548) Enable changing of selenium url, fix for selenium grid support.
13+
14+
## Deprecated
15+
16+
- [#2985](https://github.com/plotly/dash/pull/2985) Deprecate dynamic component loader.
17+
- [#2985](https://github.com/plotly/dash/pull/2985) Deprecate `run_server`, use `run` instead.
18+
- [#2899](https://github.com/plotly/dash/pull/2899) Deprecate `dcc.LogoutButton`, can be replaced with a `html.Button` or `html.A`. eg: `html.A(href=os.getenv('DASH_LOGOUT_URL'))` on a Dash Enterprise instance.
19+
- [#2995](https://github.com/plotly/dash/pull/2995) Deprecate `Dash.__init__` keywords:
20+
- The `plugins` keyword will be removed.
21+
- Old `long_callback_manager` keyword will be removed, can use `background_callback_manager` instead.
22+
523
## [2.18.0] - 2024-09-04
624

725
## Added
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,30 @@
1-
from dash.exceptions import PreventUpdate
21
from dash import Dash, Input, Output, dcc, html
3-
import flask
2+
import pytest
43
import time
54

65

7-
def test_llgo001_location_logout(dash_dcc):
6+
@pytest.mark.parametrize("add_initial_logout_button", [False, True])
7+
def test_llgo001_location_logout(dash_dcc, add_initial_logout_button):
8+
# FIXME: Logout button is deprecated, remove this test for dash 3.0
89
app = Dash(__name__)
910

10-
@app.server.route("/_logout", methods=["POST"])
11-
def on_logout():
12-
rep = flask.redirect("/logged-out")
13-
rep.set_cookie("logout-cookie", "", 0)
14-
return rep
15-
16-
app.layout = html.Div(
17-
[html.H2("Logout test"), dcc.Location(id="location"), html.Div(id="content")]
18-
)
19-
20-
@app.callback(Output("content", "children"), [Input("location", "pathname")])
21-
def on_location(location_path):
22-
if location_path is None:
23-
raise PreventUpdate
24-
25-
if "logged-out" in location_path:
26-
return "Logged out"
11+
with pytest.warns(
12+
DeprecationWarning,
13+
match="The Logout Button is no longer used with Dash Enterprise and can be replaced with a html.Button or html.A.",
14+
):
15+
app.layout = [
16+
html.H2("Logout test"),
17+
html.Div(id="content"),
18+
]
19+
if add_initial_logout_button:
20+
app.layout.append(dcc.LogoutButton())
2721
else:
2822

29-
@flask.after_this_request
30-
def _insert_cookie(rep):
31-
rep.set_cookie("logout-cookie", "logged-in")
32-
return rep
33-
34-
return dcc.LogoutButton(id="logout-btn", logout_url="/_logout")
35-
36-
dash_dcc.start_server(app)
37-
time.sleep(1)
38-
dash_dcc.percy_snapshot("Core Logout button")
39-
40-
assert dash_dcc.driver.get_cookie("logout-cookie")["value"] == "logged-in"
41-
42-
dash_dcc.wait_for_element("#logout-btn").click()
43-
dash_dcc.wait_for_text_to_equal("#content", "Logged out")
23+
@app.callback(Output("content", "children"), Input("content", "id"))
24+
def on_location(location_path):
25+
return dcc.LogoutButton(id="logout-btn", logout_url="/_logout")
4426

45-
assert not dash_dcc.driver.get_cookie("logout-cookie")
27+
dash_dcc.start_server(app)
28+
time.sleep(1)
4629

47-
assert dash_dcc.get_logs() == []
30+
assert dash_dcc.get_logs() == []

‎dash/_callback.py

+10-9
Original file line numberDiff line numberDiff line change
@@ -509,10 +509,7 @@ def add_context(*args, **kwargs):
509509

510510
# If the error returns nothing, automatically puts NoUpdate for response.
511511
if output_value is None:
512-
if not multi:
513-
output_value = NoUpdate()
514-
else:
515-
output_value = [NoUpdate() for _ in output_spec]
512+
output_value = NoUpdate()
516513
else:
517514
raise err
518515

@@ -528,12 +525,16 @@ def add_context(*args, **kwargs):
528525
# list or tuple
529526
output_value = list(output_value)
530527

531-
# Flatten grouping and validate grouping structure
532-
flat_output_values = flatten_grouping(output_value, output)
528+
if NoUpdate.is_no_update(output_value):
529+
flat_output_values = [output_value]
530+
else:
531+
# Flatten grouping and validate grouping structure
532+
flat_output_values = flatten_grouping(output_value, output)
533533

534-
_validate.validate_multi_return(
535-
output_spec, flat_output_values, callback_id
536-
)
534+
if not NoUpdate.is_no_update(output_value):
535+
_validate.validate_multi_return(
536+
output_spec, flat_output_values, callback_id
537+
)
537538

538539
for val, spec in zip(flat_output_values, output_spec):
539540
if NoUpdate.is_no_update(val):

‎dash/_jupyter.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import threading
1010
import time
1111

12+
from typing import Optional
1213
from typing_extensions import Literal
1314

1415
from werkzeug.serving import make_server
@@ -228,7 +229,7 @@ def _receive_message(msg):
228229
def run_app(
229230
self,
230231
app,
231-
mode: JupyterDisplayMode = None,
232+
mode: Optional[JupyterDisplayMode] = None,
232233
width="100%",
233234
height=650,
234235
host="127.0.0.1",
@@ -266,7 +267,7 @@ def run_app(
266267
f" Received value of type {type(mode)}: {repr(mode)}"
267268
)
268269
else:
269-
mode = mode.lower()
270+
mode = mode.lower() # type: ignore
270271
if mode not in valid_display_values:
271272
raise ValueError(
272273
f"Invalid display argument {mode}\n"

‎dash/dash.py

+68-40
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import base64
1717
import traceback
1818
from urllib.parse import urlparse
19-
from typing import Any, Callable, Dict, Optional, Union
19+
from typing import Any, Callable, Dict, Optional, Union, List
2020

2121
import flask
2222

@@ -175,11 +175,13 @@ def _do_skip(error):
175175

176176
# werkzeug<2.1.0
177177
if hasattr(tbtools, "get_current_traceback"):
178-
return tbtools.get_current_traceback(skip=_get_skip(error)).render_full()
178+
return tbtools.get_current_traceback( # type: ignore
179+
skip=_get_skip(error)
180+
).render_full()
179181

180182
if hasattr(tbtools, "DebugTraceback"):
181183
# pylint: disable=no-member
182-
return tbtools.DebugTraceback(
184+
return tbtools.DebugTraceback( # type: ignore
183185
error, skip=_get_skip(error)
184186
).render_debugger_html(True, secret, True)
185187

@@ -378,41 +380,47 @@ class Dash:
378380
_plotlyjs_url: str
379381
STARTUP_ROUTES: list = []
380382

383+
server: flask.Flask
384+
381385
def __init__( # pylint: disable=too-many-statements
382386
self,
383-
name=None,
384-
server=True,
385-
assets_folder="assets",
386-
pages_folder="pages",
387-
use_pages=None,
388-
assets_url_path="assets",
389-
assets_ignore="",
390-
assets_external_path=None,
391-
eager_loading=False,
392-
include_assets_files=True,
393-
include_pages_meta=True,
394-
url_base_pathname=None,
395-
requests_pathname_prefix=None,
396-
routes_pathname_prefix=None,
397-
serve_locally=True,
398-
compress=None,
399-
meta_tags=None,
400-
index_string=_default_index,
401-
external_scripts=None,
402-
external_stylesheets=None,
403-
suppress_callback_exceptions=None,
404-
prevent_initial_callbacks=False,
405-
show_undo_redo=False,
406-
extra_hot_reload_paths=None,
407-
plugins=None,
408-
title="Dash",
409-
update_title="Updating...",
410-
long_callback_manager=None,
411-
background_callback_manager=None,
412-
add_log_handler=True,
413-
hooks: Union[RendererHooks, None] = None,
387+
name: Optional[str] = None,
388+
server: Union[bool, flask.Flask] = True,
389+
assets_folder: str = "assets",
390+
pages_folder: str = "pages",
391+
use_pages: Optional[bool] = None,
392+
assets_url_path: str = "assets",
393+
assets_ignore: str = "",
394+
assets_external_path: Optional[str] = None,
395+
eager_loading: bool = False,
396+
include_assets_files: bool = True,
397+
include_pages_meta: bool = True,
398+
url_base_pathname: Optional[str] = None,
399+
requests_pathname_prefix: Optional[str] = None,
400+
routes_pathname_prefix: Optional[str] = None,
401+
serve_locally: bool = True,
402+
compress: Optional[bool] = None,
403+
meta_tags: Optional[List[Dict[str, Any]]] = None,
404+
index_string: str = _default_index,
405+
external_scripts: Optional[List[Union[str, Dict[str, Any]]]] = None,
406+
external_stylesheets: Optional[List[Union[str, Dict[str, Any]]]] = None,
407+
suppress_callback_exceptions: Optional[bool] = None,
408+
prevent_initial_callbacks: bool = False,
409+
show_undo_redo: bool = False,
410+
extra_hot_reload_paths: Optional[List[str]] = None,
411+
plugins: Optional[list] = None,
412+
title: str = "Dash",
413+
update_title: str = "Updating...",
414+
long_callback_manager: Optional[
415+
Any
416+
] = None, # Type should be specified if possible
417+
background_callback_manager: Optional[
418+
Any
419+
] = None, # Type should be specified if possible
420+
add_log_handler: bool = True,
421+
hooks: Optional[RendererHooks] = None,
414422
routing_callback_inputs: Optional[Dict[str, Union[Input, State]]] = None,
415-
description=None,
423+
description: Optional[str] = None,
416424
on_error: Optional[Callable[[Exception], Any]] = None,
417425
**obsolete,
418426
):
@@ -428,7 +436,7 @@ def __init__( # pylint: disable=too-many-statements
428436
name = getattr(server, "name", caller_name)
429437
elif isinstance(server, bool):
430438
name = name if name else caller_name
431-
self.server = flask.Flask(name) if server else None
439+
self.server = flask.Flask(name) if server else None # type: ignore
432440
else:
433441
raise ValueError("server must be a Flask app or a boolean")
434442

@@ -440,7 +448,7 @@ def __init__( # pylint: disable=too-many-statements
440448
name=name,
441449
assets_folder=os.path.join(
442450
flask.helpers.get_root_path(name), assets_folder
443-
),
451+
), # type: ignore
444452
assets_url_path=assets_url_path,
445453
assets_ignore=assets_ignore,
446454
assets_external_path=get_combined_config(
@@ -539,14 +547,29 @@ def __init__( # pylint: disable=too-many-statements
539547

540548
self._assets_files = []
541549
self._long_callback_count = 0
550+
if long_callback_manager:
551+
warnings.warn(
552+
DeprecationWarning(
553+
"`long_callback_manager` is deprecated and will be remove in Dash 3.0, "
554+
"use `background_callback_manager` instead."
555+
)
556+
)
542557
self._background_manager = background_callback_manager or long_callback_manager
543558

544559
self.logger = logging.getLogger(__name__)
545560

546561
if not self.logger.handlers and add_log_handler:
547562
self.logger.addHandler(logging.StreamHandler(stream=sys.stdout))
548563

549-
if isinstance(plugins, patch_collections_abc("Iterable")):
564+
if plugins is not None and isinstance(
565+
plugins, patch_collections_abc("Iterable")
566+
):
567+
warnings.warn(
568+
DeprecationWarning(
569+
"The `plugins` keyword will be removed from Dash init in Dash 3.0 "
570+
"and replaced by a new hook system."
571+
)
572+
)
550573
for plugin in plugins:
551574
plugin.plug(self)
552575

@@ -1961,7 +1984,7 @@ def run(
19611984
port="8050",
19621985
proxy=None,
19631986
debug=None,
1964-
jupyter_mode: JupyterDisplayMode = None,
1987+
jupyter_mode: Optional[JupyterDisplayMode] = None,
19651988
jupyter_width="100%",
19661989
jupyter_height=650,
19671990
jupyter_server_url=None,
@@ -2096,7 +2119,7 @@ def run(
20962119
port = int(port)
20972120
assert port in range(1, 65536)
20982121
except Exception as e:
2099-
e.args = [f"Expecting an integer from 1 to 65535, found port={repr(port)}"]
2122+
e.args = (f"Expecting an integer from 1 to 65535, found port={repr(port)}",)
21002123
raise
21012124

21022125
# so we only see the "Running on" message once with hot reloading
@@ -2256,4 +2279,9 @@ def run_server(self, *args, **kwargs):
22562279
22572280
See `app.run` for usage information.
22582281
"""
2282+
warnings.warn(
2283+
DeprecationWarning(
2284+
"Dash.run_server is deprecated and will be removed in Dash 3.0"
2285+
)
2286+
)
22592287
self.run(*args, **kwargs)

‎dash/development/base_component.py

+21
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,26 @@
44
import sys
55
import uuid
66
import random
7+
import warnings
8+
import textwrap
79

810
from .._utils import patch_collections_abc, stringify_id, OrderedSet
911

1012
MutableSequence = patch_collections_abc("MutableSequence")
1113

1214
rd = random.Random(0)
1315

16+
_deprecated_components = {
17+
"dash_core_components": {
18+
"LogoutButton": textwrap.dedent(
19+
"""
20+
The Logout Button is no longer used with Dash Enterprise and can be replaced with a html.Button or html.A.
21+
eg: html.A(href=os.getenv('DASH_LOGOUT_URL'))
22+
"""
23+
)
24+
}
25+
}
26+
1427

1528
# pylint: disable=no-init,too-few-public-methods
1629
class ComponentRegistry:
@@ -95,6 +108,7 @@ def __str__(self):
95108
REQUIRED = _REQUIRED()
96109

97110
def __init__(self, **kwargs):
111+
self._validate_deprecation()
98112
import dash # pylint: disable=import-outside-toplevel, cyclic-import
99113

100114
# pylint: disable=super-init-not-called
@@ -405,6 +419,13 @@ def __repr__(self):
405419
props_string = repr(getattr(self, "children", None))
406420
return f"{self._type}({props_string})"
407421

422+
def _validate_deprecation(self):
423+
_type = getattr(self, "_type", "")
424+
_ns = getattr(self, "_namespace", "")
425+
deprecation_message = _deprecated_components.get(_ns, {}).get(_type)
426+
if deprecation_message:
427+
warnings.warn(DeprecationWarning(textwrap.dedent(deprecation_message)))
428+
408429

409430
def _explicitize_args(func):
410431
# Python 2

‎dash/development/component_loader.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import collections
22
import json
33
import os
4+
import warnings
5+
46

57
from ._py_components_generation import (
68
generate_class_file,
@@ -34,7 +36,13 @@ def load_components(metadata_path, namespace="default_namespace"):
3436
components -- a list of component objects with keys
3537
`type`, `valid_kwargs`, and `setup`.
3638
"""
37-
39+
warnings.warn(
40+
DeprecationWarning(
41+
"Dynamic components loading has been deprecated and will be removed"
42+
" in dash 3.0.\n"
43+
f"Update {namespace} to generate components with dash-generate-components"
44+
)
45+
)
3846
# Register the component lib for index include.
3947
ComponentRegistry.registry.add(namespace)
4048
components = []

‎dash/testing/application_runners.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ class BaseDashRunner:
6161

6262
_next_port = 58050
6363

64-
def __init__(self, keep_open, stop_timeout):
64+
def __init__(self, keep_open, stop_timeout, scheme="http", host="localhost"):
65+
self.scheme = scheme
66+
self.host = host
6567
self.port = 8050
6668
self.started = None
6769
self.keep_open = keep_open
@@ -102,7 +104,7 @@ def __exit__(self, exc_type, exc_val, traceback):
102104
@property
103105
def url(self):
104106
"""The default server url."""
105-
return f"http://localhost:{self.port}"
107+
return f"{self.scheme}://{self.host}:{self.port}"
106108

107109
@property
108110
def is_windows(self):

‎dash/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.18.0"
1+
__version__ = "2.18.1"

‎package-lock.json

+1,296-1,095
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tests/integration/callbacks/test_callback_error.py

+17
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ def global_callback_error_handler(err):
1010
app.layout = [
1111
html.Button("start", id="start-local"),
1212
html.Button("start-global", id="start-global"),
13+
html.Button("start-grouped", id="start-grouped"),
1314
html.Div(id="output"),
1415
html.Div(id="output-global"),
1516
html.Div(id="error-message"),
17+
# test for #2983
18+
html.Div("default-value", id="grouped-output"),
1619
]
1720

1821
def on_callback_error(err):
@@ -36,6 +39,14 @@ def on_start(_):
3639
def on_start_global(_):
3740
raise Exception("global error")
3841

42+
@app.callback(
43+
output=dict(test=Output("grouped-output", "children")),
44+
inputs=dict(start=Input("start-grouped", "n_clicks")),
45+
prevent_initial_call=True,
46+
)
47+
def on_start_grouped(start=0):
48+
raise Exception("grouped error")
49+
3950
dash_duo.start_server(app)
4051
dash_duo.find_element("#start-local").click()
4152

@@ -44,3 +55,9 @@ def on_start_global(_):
4455

4556
dash_duo.find_element("#start-global").click()
4657
dash_duo.wait_for_text_to_equal("#output-global", "global: global error")
58+
59+
dash_duo.find_element("#start-grouped").click()
60+
dash_duo.wait_for_text_to_equal("#output-global", "global: grouped error")
61+
dash_duo.wait_for_text_to_equal("#grouped-output", "default-value")
62+
63+
assert dash_duo.get_logs() == []

‎tests/integration/callbacks/test_missing_outputs.py

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import time
2+
13
import pytest
24
from multiprocessing import Lock, Value
35

46
import dash
5-
from dash import Dash, Input, Output, ALL, MATCH, html, dcc
7+
from dash import Dash, Input, Output, ALL, MATCH, html, dcc, no_update
68

79
from dash.testing.wait import until
810

@@ -336,3 +338,34 @@ def chapter2_assertions():
336338
dash_duo._wait_for_callbacks()
337339
chapter2_assertions()
338340
assert not dash_duo.get_logs()
341+
342+
343+
def test_cbmo005_no_update_single_to_multi(dash_duo):
344+
# Bugfix for #2986
345+
app = dash.Dash(__name__)
346+
347+
app.layout = html.Div(
348+
[
349+
dcc.Input(id="input-box", type="text", value=""),
350+
html.Button("Submit", id="button"),
351+
html.Div(id="output-1", children="Output 1 will be displayed here"),
352+
html.Div(id="output-2", children="Output 2 will be displayed here"),
353+
]
354+
)
355+
356+
@app.callback(
357+
Output("output-1", "children"),
358+
Output("output-2", "children"),
359+
Input("button", "n_clicks"),
360+
Input("input-box", "value"),
361+
)
362+
def update_outputs(n_clicks, value):
363+
if n_clicks is None:
364+
return no_update
365+
return "Hello", "world!"
366+
367+
dash_duo.start_server(app)
368+
369+
# just wait to make sure the error get logged.
370+
time.sleep(1)
371+
assert dash_duo.get_logs() == []

‎tests/integration/renderer/test_dependencies.py

+3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
from dash import Dash, html, dcc, Input, Output
44

5+
from flaky import flaky
56

7+
8+
@flaky(max_runs=3)
69
def test_rddp001_dependencies_on_components_that_dont_exist(dash_duo):
710
app = Dash(__name__, suppress_callback_exceptions=True)
811
app.layout = html.Div(

0 commit comments

Comments
 (0)
Please sign in to comment.