Skip to content

Commit

Permalink
Merge pull request #3000 from plotly/master-2.18.1
Browse files Browse the repository at this point in the history
Master 2.18.1
  • Loading branch information
T4rk1n authored Sep 12, 2024
2 parents 851721b + cf596f4 commit 5c7287f
Show file tree
Hide file tree
Showing 13 changed files with 1,504 additions and 1,188 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).

## [2.18.1] - 2024-09-12

## Fixed

- [#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)
- [2988](https://github.com/plotly/dash/pull/2988) Fix error handler and grouped outputs. Fixes [#2983](https://github.com/plotly/dash/issues/2983)
- [#2841](https://github.com/plotly/dash/pull/2841) Fix typing on Dash init.
- [#1548](https://github.com/plotly/dash/pull/1548) Enable changing of selenium url, fix for selenium grid support.

## Deprecated

- [#2985](https://github.com/plotly/dash/pull/2985) Deprecate dynamic component loader.
- [#2985](https://github.com/plotly/dash/pull/2985) Deprecate `run_server`, use `run` instead.
- [#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.
- [#2995](https://github.com/plotly/dash/pull/2995) Deprecate `Dash.__init__` keywords:
- The `plugins` keyword will be removed.
- Old `long_callback_manager` keyword will be removed, can use `background_callback_manager` instead.

## [2.18.0] - 2024-09-04

## Added
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,30 @@
from dash.exceptions import PreventUpdate
from dash import Dash, Input, Output, dcc, html
import flask
import pytest
import time


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

@app.server.route("/_logout", methods=["POST"])
def on_logout():
rep = flask.redirect("/logged-out")
rep.set_cookie("logout-cookie", "", 0)
return rep

app.layout = html.Div(
[html.H2("Logout test"), dcc.Location(id="location"), html.Div(id="content")]
)

@app.callback(Output("content", "children"), [Input("location", "pathname")])
def on_location(location_path):
if location_path is None:
raise PreventUpdate

if "logged-out" in location_path:
return "Logged out"
with pytest.warns(
DeprecationWarning,
match="The Logout Button is no longer used with Dash Enterprise and can be replaced with a html.Button or html.A.",
):
app.layout = [
html.H2("Logout test"),
html.Div(id="content"),
]
if add_initial_logout_button:
app.layout.append(dcc.LogoutButton())
else:

@flask.after_this_request
def _insert_cookie(rep):
rep.set_cookie("logout-cookie", "logged-in")
return rep

return dcc.LogoutButton(id="logout-btn", logout_url="/_logout")

dash_dcc.start_server(app)
time.sleep(1)
dash_dcc.percy_snapshot("Core Logout button")

assert dash_dcc.driver.get_cookie("logout-cookie")["value"] == "logged-in"

dash_dcc.wait_for_element("#logout-btn").click()
dash_dcc.wait_for_text_to_equal("#content", "Logged out")
@app.callback(Output("content", "children"), Input("content", "id"))
def on_location(location_path):
return dcc.LogoutButton(id="logout-btn", logout_url="/_logout")

assert not dash_dcc.driver.get_cookie("logout-cookie")
dash_dcc.start_server(app)
time.sleep(1)

assert dash_dcc.get_logs() == []
assert dash_dcc.get_logs() == []
19 changes: 10 additions & 9 deletions dash/_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,10 +509,7 @@ def add_context(*args, **kwargs):

# If the error returns nothing, automatically puts NoUpdate for response.
if output_value is None:
if not multi:
output_value = NoUpdate()
else:
output_value = [NoUpdate() for _ in output_spec]
output_value = NoUpdate()
else:
raise err

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

# Flatten grouping and validate grouping structure
flat_output_values = flatten_grouping(output_value, output)
if NoUpdate.is_no_update(output_value):
flat_output_values = [output_value]
else:
# Flatten grouping and validate grouping structure
flat_output_values = flatten_grouping(output_value, output)

_validate.validate_multi_return(
output_spec, flat_output_values, callback_id
)
if not NoUpdate.is_no_update(output_value):
_validate.validate_multi_return(
output_spec, flat_output_values, callback_id
)

for val, spec in zip(flat_output_values, output_spec):
if NoUpdate.is_no_update(val):
Expand Down
5 changes: 3 additions & 2 deletions dash/_jupyter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import threading
import time

from typing import Optional
from typing_extensions import Literal

from werkzeug.serving import make_server
Expand Down Expand Up @@ -228,7 +229,7 @@ def _receive_message(msg):
def run_app(
self,
app,
mode: JupyterDisplayMode = None,
mode: Optional[JupyterDisplayMode] = None,
width="100%",
height=650,
host="127.0.0.1",
Expand Down Expand Up @@ -266,7 +267,7 @@ def run_app(
f" Received value of type {type(mode)}: {repr(mode)}"
)
else:
mode = mode.lower()
mode = mode.lower() # type: ignore
if mode not in valid_display_values:
raise ValueError(
f"Invalid display argument {mode}\n"
Expand Down
108 changes: 68 additions & 40 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import base64
import traceback
from urllib.parse import urlparse
from typing import Any, Callable, Dict, Optional, Union
from typing import Any, Callable, Dict, Optional, Union, List

import flask

Expand Down Expand Up @@ -175,11 +175,13 @@ def _do_skip(error):

# werkzeug<2.1.0
if hasattr(tbtools, "get_current_traceback"):
return tbtools.get_current_traceback(skip=_get_skip(error)).render_full()
return tbtools.get_current_traceback( # type: ignore
skip=_get_skip(error)
).render_full()

if hasattr(tbtools, "DebugTraceback"):
# pylint: disable=no-member
return tbtools.DebugTraceback(
return tbtools.DebugTraceback( # type: ignore
error, skip=_get_skip(error)
).render_debugger_html(True, secret, True)

Expand Down Expand Up @@ -378,41 +380,47 @@ class Dash:
_plotlyjs_url: str
STARTUP_ROUTES: list = []

server: flask.Flask

def __init__( # pylint: disable=too-many-statements
self,
name=None,
server=True,
assets_folder="assets",
pages_folder="pages",
use_pages=None,
assets_url_path="assets",
assets_ignore="",
assets_external_path=None,
eager_loading=False,
include_assets_files=True,
include_pages_meta=True,
url_base_pathname=None,
requests_pathname_prefix=None,
routes_pathname_prefix=None,
serve_locally=True,
compress=None,
meta_tags=None,
index_string=_default_index,
external_scripts=None,
external_stylesheets=None,
suppress_callback_exceptions=None,
prevent_initial_callbacks=False,
show_undo_redo=False,
extra_hot_reload_paths=None,
plugins=None,
title="Dash",
update_title="Updating...",
long_callback_manager=None,
background_callback_manager=None,
add_log_handler=True,
hooks: Union[RendererHooks, None] = None,
name: Optional[str] = None,
server: Union[bool, flask.Flask] = True,
assets_folder: str = "assets",
pages_folder: str = "pages",
use_pages: Optional[bool] = None,
assets_url_path: str = "assets",
assets_ignore: str = "",
assets_external_path: Optional[str] = None,
eager_loading: bool = False,
include_assets_files: bool = True,
include_pages_meta: bool = True,
url_base_pathname: Optional[str] = None,
requests_pathname_prefix: Optional[str] = None,
routes_pathname_prefix: Optional[str] = None,
serve_locally: bool = True,
compress: Optional[bool] = None,
meta_tags: Optional[List[Dict[str, Any]]] = None,
index_string: str = _default_index,
external_scripts: Optional[List[Union[str, Dict[str, Any]]]] = None,
external_stylesheets: Optional[List[Union[str, Dict[str, Any]]]] = None,
suppress_callback_exceptions: Optional[bool] = None,
prevent_initial_callbacks: bool = False,
show_undo_redo: bool = False,
extra_hot_reload_paths: Optional[List[str]] = None,
plugins: Optional[list] = None,
title: str = "Dash",
update_title: str = "Updating...",
long_callback_manager: Optional[
Any
] = None, # Type should be specified if possible
background_callback_manager: Optional[
Any
] = None, # Type should be specified if possible
add_log_handler: bool = True,
hooks: Optional[RendererHooks] = None,
routing_callback_inputs: Optional[Dict[str, Union[Input, State]]] = None,
description=None,
description: Optional[str] = None,
on_error: Optional[Callable[[Exception], Any]] = None,
**obsolete,
):
Expand All @@ -428,7 +436,7 @@ def __init__( # pylint: disable=too-many-statements
name = getattr(server, "name", caller_name)
elif isinstance(server, bool):
name = name if name else caller_name
self.server = flask.Flask(name) if server else None
self.server = flask.Flask(name) if server else None # type: ignore
else:
raise ValueError("server must be a Flask app or a boolean")

Expand All @@ -440,7 +448,7 @@ def __init__( # pylint: disable=too-many-statements
name=name,
assets_folder=os.path.join(
flask.helpers.get_root_path(name), assets_folder
),
), # type: ignore
assets_url_path=assets_url_path,
assets_ignore=assets_ignore,
assets_external_path=get_combined_config(
Expand Down Expand Up @@ -539,14 +547,29 @@ def __init__( # pylint: disable=too-many-statements

self._assets_files = []
self._long_callback_count = 0
if long_callback_manager:
warnings.warn(
DeprecationWarning(
"`long_callback_manager` is deprecated and will be remove in Dash 3.0, "
"use `background_callback_manager` instead."
)
)
self._background_manager = background_callback_manager or long_callback_manager

self.logger = logging.getLogger(__name__)

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

if isinstance(plugins, patch_collections_abc("Iterable")):
if plugins is not None and isinstance(
plugins, patch_collections_abc("Iterable")
):
warnings.warn(
DeprecationWarning(
"The `plugins` keyword will be removed from Dash init in Dash 3.0 "
"and replaced by a new hook system."
)
)
for plugin in plugins:
plugin.plug(self)

Expand Down Expand Up @@ -1961,7 +1984,7 @@ def run(
port="8050",
proxy=None,
debug=None,
jupyter_mode: JupyterDisplayMode = None,
jupyter_mode: Optional[JupyterDisplayMode] = None,
jupyter_width="100%",
jupyter_height=650,
jupyter_server_url=None,
Expand Down Expand Up @@ -2096,7 +2119,7 @@ def run(
port = int(port)
assert port in range(1, 65536)
except Exception as e:
e.args = [f"Expecting an integer from 1 to 65535, found port={repr(port)}"]
e.args = (f"Expecting an integer from 1 to 65535, found port={repr(port)}",)
raise

# so we only see the "Running on" message once with hot reloading
Expand Down Expand Up @@ -2256,4 +2279,9 @@ def run_server(self, *args, **kwargs):
See `app.run` for usage information.
"""
warnings.warn(
DeprecationWarning(
"Dash.run_server is deprecated and will be removed in Dash 3.0"
)
)
self.run(*args, **kwargs)
21 changes: 21 additions & 0 deletions dash/development/base_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,26 @@
import sys
import uuid
import random
import warnings
import textwrap

from .._utils import patch_collections_abc, stringify_id, OrderedSet

MutableSequence = patch_collections_abc("MutableSequence")

rd = random.Random(0)

_deprecated_components = {
"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'))
"""
)
}
}


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

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

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

def _validate_deprecation(self):
_type = getattr(self, "_type", "")
_ns = getattr(self, "_namespace", "")
deprecation_message = _deprecated_components.get(_ns, {}).get(_type)
if deprecation_message:
warnings.warn(DeprecationWarning(textwrap.dedent(deprecation_message)))


def _explicitize_args(func):
# Python 2
Expand Down
Loading

0 comments on commit 5c7287f

Please sign in to comment.