Skip to content

Commit 5c7287f

Browse files
authored
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

Lines changed: 18 additions & 0 deletions
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
Lines changed: 20 additions & 37 deletions
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

Lines changed: 10 additions & 9 deletions
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

Lines changed: 3 additions & 2 deletions
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

Lines changed: 68 additions & 40 deletions
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

Lines changed: 21 additions & 0 deletions
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

0 commit comments

Comments
 (0)