Skip to content

Commit 3432eac

Browse files
authored
Merge branch 'dev' into fix/xss-data-url
2 parents 35c847b + 0d96c02 commit 3432eac

File tree

9 files changed

+176
-7
lines changed

9 files changed

+176
-7
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React, { useState, useEffect } from "react";
2+
import PropTypes from "prop-types";
3+
4+
const DrawCounter = (props) => {
5+
const [count, setCount] = useState(0);
6+
useEffect(() => {
7+
setCount(count + 1);
8+
}, [props]);
9+
return <div id={props.id}>{count}</div>;
10+
};
11+
12+
DrawCounter.propTypes = {
13+
id: PropTypes.string,
14+
};
15+
export default DrawCounter;

@plotly/dash-test-components/src/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import MyPersistedComponentNested from './components/MyPersistedComponentNested'
77
import StyledComponent from './components/StyledComponent';
88
import WidthComponent from './components/WidthComponent';
99
import ComponentAsProp from './components/ComponentAsProp';
10+
11+
import DrawCounter from './components/DrawCounter';
1012
import AddPropsComponent from "./components/AddPropsComponent";
1113
import ReceivePropsComponent from "./components/ReceivePropsComponent";
1214

15+
1316
export {
1417
AsyncComponent,
1518
CollapseComponent,
@@ -20,6 +23,7 @@ export {
2023
StyledComponent,
2124
WidthComponent,
2225
ComponentAsProp,
26+
DrawCounter,
2327
AddPropsComponent,
2428
ReceivePropsComponent
2529
};

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
66

77
## Added
88

9+
- [#2832](https://github.com/plotly/dash/pull/2832) Add dash startup route setup on Dash init.
910
- [#2819](https://github.com/plotly/dash/pull/2819) Add dash subcomponents receive additional parameters passed by the parent component. Fixes [#2814](https://github.com/plotly/dash/issues/2814).
1011
- [#2826](https://github.com/plotly/dash/pull/2826) When using Pages, allows for `app.title` and (new) `app.description` to be used as defaults for the page title and description. Fixes [#2811](https://github.com/plotly/dash/issues/2811).
1112
- [#2795](https://github.com/plotly/dash/pull/2795) Allow list of components to be passed as layout.

dash/dash.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ class Dash:
372372
"""
373373

374374
_plotlyjs_url: str
375+
STARTUP_ROUTES: list = []
375376

376377
def __init__( # pylint: disable=too-many-statements
377378
self,
@@ -556,6 +557,7 @@ def __init__( # pylint: disable=too-many-statements
556557
"JupyterDash is deprecated, use Dash instead.\n"
557558
"See https://dash.plotly.com/dash-in-jupyter for more details."
558559
)
560+
self.setup_startup_routes()
559561

560562
def init_app(self, app=None, **kwargs):
561563
"""Initialize the parts of Dash that require a flask app."""
@@ -1626,6 +1628,39 @@ def display_content(path):
16261628
self.config.requests_pathname_prefix, path
16271629
)
16281630

1631+
@staticmethod
1632+
def add_startup_route(name, view_func, methods):
1633+
"""
1634+
Add a route to the app to be initialized at the end of Dash initialization.
1635+
Use this if the package requires a route to be added to the app, and you will not need to worry about at what point to add it.
1636+
1637+
:param name: The name of the route. eg "my-new-url/path".
1638+
:param view_func: The function to call when the route is requested. The function should return a JSON serializable object.
1639+
:param methods: The HTTP methods that the route should respond to. eg ["GET", "POST"] or either one.
1640+
"""
1641+
if not isinstance(name, str) or name.startswith("/"):
1642+
raise ValueError("name must be a string and should not start with '/'")
1643+
1644+
if not callable(view_func):
1645+
raise ValueError("view_func must be callable")
1646+
1647+
valid_methods = {"POST", "GET"}
1648+
if not set(methods).issubset(valid_methods):
1649+
raise ValueError(f"methods should only contain {valid_methods}")
1650+
1651+
if any(route[0] == name for route in Dash.STARTUP_ROUTES):
1652+
raise ValueError(f"Route name '{name}' is already in use.")
1653+
1654+
Dash.STARTUP_ROUTES.append((name, view_func, methods))
1655+
1656+
def setup_startup_routes(self):
1657+
"""
1658+
Initialize the startup routes stored in STARTUP_ROUTES.
1659+
"""
1660+
for _name, _view_func, _methods in self.STARTUP_ROUTES:
1661+
self._add_url(f"_dash_startup_route/{_name}", _view_func, _methods)
1662+
self.STARTUP_ROUTES = []
1663+
16291664
def _setup_dev_tools(self, **kwargs):
16301665
debug = kwargs.get("debug", False)
16311666
dev_tools = self._dev_tools = AttributeDict()

dash/testing/wait.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# pylint: disable=too-few-public-methods
22
"""Utils methods for pytest-dash such wait_for wrappers."""
3+
34
import time
45
import logging
56
from selenium.common.exceptions import WebDriverException
@@ -62,8 +63,9 @@ def __call__(self, driver):
6263
try:
6364
elem = driver.find_element(By.CSS_SELECTOR, self.selector)
6465
logger.debug("contains text {%s} => expected %s", elem.text, self.text)
65-
return self.text in str(elem.text) or self.text in str(
66-
elem.get_attribute("value")
66+
value = elem.get_attribute("value")
67+
return self.text in str(elem.text) or (
68+
value is not None and self.text in str(value)
6769
)
6870
except WebDriverException:
6971
return False
@@ -107,9 +109,9 @@ def __call__(self, driver):
107109
try:
108110
elem = self._get_element(driver)
109111
logger.debug("text to equal {%s} => expected %s", elem.text, self.text)
110-
return (
111-
str(elem.text) == self.text
112-
or str(elem.get_attribute("value")) == self.text
112+
value = elem.get_attribute("value")
113+
return str(elem.text) == self.text or (
114+
value is not None and str(value) == self.text
113115
)
114116
except WebDriverException:
115117
return False

tests/integration/multi_page/test_pages_layout.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22
import dash
3-
from dash import Dash, Input, State, dcc, html
3+
from dash import Dash, Input, State, dcc, html, Output
44
from dash.dash import _ID_LOCATION
55
from dash.exceptions import NoLayoutException
66

@@ -237,6 +237,35 @@ def test_pala005_routing_inputs(dash_duo, clear_pages_state):
237237
dash_duo.wait_for_text_to_equal("#contents", "Le hash dit: #123")
238238

239239

240+
def test_pala006_pages_external_library(dash_duo):
241+
import dash_test_components as dt
242+
243+
app = Dash(use_pages=True, pages_folder="")
244+
245+
@app.callback(
246+
Output("out", "children"),
247+
Input("button", "n_clicks"),
248+
prevent_initial_call=True,
249+
)
250+
def on_click(n_clicks):
251+
return f"Button has been clicked {n_clicks} times"
252+
253+
dash.register_page(
254+
"page",
255+
path="/",
256+
layout=html.Div(
257+
[
258+
dt.DelayedEventComponent(id="button"),
259+
html.Div("The button has not been clicked yet", id="out"),
260+
]
261+
),
262+
)
263+
264+
dash_duo.start_server(app)
265+
dash_duo.wait_for_element("#button").click()
266+
dash_duo.wait_for_text_to_equal("#out", "Button has been clicked 1 times")
267+
268+
240269
def get_app_title_description():
241270
app = Dash(
242271
__name__, use_pages=True, title="App Title", description="App Description"
@@ -252,7 +281,7 @@ def get_app_title_description():
252281
return app
253282

254283

255-
def test_pala006_app_title_discription(dash_duo, clear_pages_state):
284+
def test_pala007_app_title_discription(dash_duo, clear_pages_state):
256285
dash_duo.start_server(get_app_title_description())
257286

258287
assert dash.page_registry["home"]["title"] == "App Title"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import time
2+
from dash import Dash, Input, Output, html
3+
4+
import dash_test_components as dt
5+
6+
7+
def test_rdraw001_redraw(dash_duo):
8+
app = Dash()
9+
10+
app.layout = html.Div(
11+
[
12+
html.Div(
13+
dt.DrawCounter(id="counter"),
14+
id="redrawer",
15+
),
16+
html.Button("redraw", id="redraw"),
17+
]
18+
)
19+
20+
@app.callback(
21+
Output("redrawer", "children"),
22+
Input("redraw", "n_clicks"),
23+
prevent_initial_call=True,
24+
)
25+
def on_click(_):
26+
return dt.DrawCounter(id="counter")
27+
28+
dash_duo.start_server(app)
29+
30+
dash_duo.wait_for_text_to_equal("#counter", "1")
31+
dash_duo.find_element("#redraw").click()
32+
dash_duo.wait_for_text_to_equal("#counter", "2")
33+
time.sleep(1)
34+
dash_duo.wait_for_text_to_equal("#counter", "2")

tests/integration/test_duo.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ def test_duo001_wait_for_text_error(dash_duo):
1414

1515
assert err.value.args[0] == "text -> Invalid not found within 1.0s, found: Content"
1616

17+
with pytest.raises(TimeoutException) as err:
18+
dash_duo.wait_for_text_to_equal("#content", "None", timeout=1.0)
19+
20+
assert err.value.args[0] == "text -> None not found within 1.0s, found: Content"
21+
1722
with pytest.raises(TimeoutException) as err:
1823
dash_duo.wait_for_text_to_equal("#none", "None", timeout=1.0)
1924

@@ -27,10 +32,33 @@ def test_duo001_wait_for_text_error(dash_duo):
2732
== "text -> invalid not found inside element within 1.0s, found: Content"
2833
)
2934

35+
with pytest.raises(TimeoutException) as err:
36+
dash_duo.wait_for_contains_text("#content", "None", timeout=1.0)
37+
38+
assert (
39+
err.value.args[0]
40+
== "text -> None not found inside element within 1.0s, found: Content"
41+
)
42+
3043
with pytest.raises(TimeoutException) as err:
3144
dash_duo.wait_for_contains_text("#none", "none", timeout=1.0)
3245

3346
assert (
3447
err.value.args[0]
3548
== "text -> none not found inside element within 1.0s, #none not found"
3649
)
50+
51+
52+
def test_duo002_wait_for_text_value(dash_duo):
53+
app = Dash(__name__)
54+
app.layout = html.Div([html.Ol([html.Li("Item", id="value-item", value="100")])])
55+
dash_duo.start_server(app)
56+
57+
dash_duo.wait_for_text_to_equal("#value-item", "100")
58+
with pytest.raises(TimeoutException) as err:
59+
dash_duo.wait_for_contains_text("#value-item", "None", timeout=1.0)
60+
61+
assert (
62+
err.value.args[0]
63+
== "text -> None not found inside element within 1.0s, found: Item"
64+
)

tests/integration/test_integration.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,3 +511,24 @@ def on_nested_click(n_clicks):
511511

512512
dash_duo.wait_for_element("#nested").click()
513513
dash_duo.wait_for_text_to_equal("#nested-output", "Clicked 1 times")
514+
515+
516+
def test_inin030_add_startup_route(dash_duo):
517+
url = "my-new-route"
518+
519+
def my_route_f():
520+
return "hello"
521+
522+
Dash.add_startup_route(url, my_route_f, ["POST"])
523+
524+
import requests
525+
526+
app = Dash(__name__)
527+
Dash.STARTUP_ROUTES = []
528+
app.layout = html.Div("Hello World")
529+
dash_duo.start_server(app)
530+
531+
url = f"{dash_duo.server_url}{app.config.requests_pathname_prefix}_dash_startup_route/{url}"
532+
response = requests.post(url)
533+
assert response.status_code == 200
534+
assert response.text == "hello"

0 commit comments

Comments
 (0)