Skip to content

Commit d9a8d9c

Browse files
authored
Merge pull request #1384 from plotly/1383-callback-args-fix
Fix #1383 prevent_initial_call positional arg regression
2 parents 3222191 + 5a702bf commit d9a8d9c

File tree

8 files changed

+244
-275
lines changed

8 files changed

+244
-275
lines changed

Diff for: CHANGELOG.md

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

5+
## [UNRELEASED]
6+
### Fixed
7+
- [#1384](https://github.com/plotly/dash/pull/1384) Fixed a bug introduced by [#1180](https://github.com/plotly/dash/pull/1180) breaking use of `prevent_initial_call` as a positional arg in callback definitions
8+
59
## [1.15.0] - 2020-08-25
610
### Added
711
- [#1355](https://github.com/plotly/dash/pull/1355) Removed redundant log message and consolidated logger initialization. You can now control the log level - for example suppress informational messages from Dash with `app.logger.setLevel(logging.WARNING)`.

Diff for: dash/dependencies.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def handle_callback_args(args, kwargs):
156156
"""Split args into outputs, inputs and states"""
157157
prevent_initial_call = kwargs.get("prevent_initial_call", None)
158158
if prevent_initial_call is None and args and isinstance(args[-1], bool):
159-
prevent_initial_call = args.pop()
159+
args, prevent_initial_call = args[:-1], args[-1]
160160

161161
# flatten args, to support the older syntax where outputs, inputs, and states
162162
# each needed to be in their own list

Diff for: requires-testing.txt

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pytest-mock==2.0.0;python_version=="2.7"
66
lxml==4.5.0
77
selenium==3.141.0
88
percy==2.0.2
9+
cryptography==3.0
910
requests[security]==2.21.0
1011
beautifulsoup4==4.8.2
1112
waitress==1.4.3

Diff for: tests/integration/callbacks/test_prevent_initial.py

+18
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,21 @@ def test_cbpi003_multi_outputs(flavor, dash_duo):
333333
dash_duo.wait_for_text_to_equal("#c", "BlueCheese")
334334
dash_duo.wait_for_text_to_equal("#b", "Cheese")
335335
dash_duo.wait_for_text_to_equal("#a", "Blue")
336+
337+
338+
def test_cbpi004_positional_arg(dash_duo):
339+
app = dash.Dash(__name__)
340+
app.layout = html.Div([html.Button("click", id="btn"), html.Div(id="out")])
341+
342+
@app.callback(Output("out", "children"), Input("btn", "n_clicks"), True)
343+
def f(n):
344+
return n
345+
346+
dash_duo.start_server(app)
347+
dash_duo._wait_for_callbacks()
348+
349+
dash_duo.wait_for_text_to_equal("#out", "")
350+
351+
dash_duo.find_element("#btn").click()
352+
353+
dash_duo.wait_for_text_to_equal("#out", "1")

Diff for: tests/integration/callbacks/test_validation.py

+30
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,33 @@ def o2(i, s):
193193
dash_duo.start_server(app)
194194
dash_duo.wait_for_text_to_equal("#out1", "1: High")
195195
dash_duo.wait_for_text_to_equal("#out2", "2: High")
196+
197+
198+
def test_cbva005_tuple_args(dash_duo):
199+
app = Dash(__name__)
200+
app.layout = html.Div(
201+
[
202+
html.Div("Yo", id="in1"),
203+
html.Div("lo", id="in2"),
204+
html.Div(id="out1"),
205+
html.Div(id="out2"),
206+
]
207+
)
208+
209+
@app.callback(
210+
Output("out1", "children"), (Input("in1", "children"), Input("in2", "children"))
211+
)
212+
def f(i1, i2):
213+
return "1: " + i1 + i2
214+
215+
@app.callback(
216+
(Output("out2", "children"),),
217+
Input("in1", "children"),
218+
(State("in2", "children"),),
219+
)
220+
def g(i1, i2):
221+
return ("2: " + i1 + i2,)
222+
223+
dash_duo.start_server(app)
224+
dash_duo.wait_for_text_to_equal("#out1", "1: Yolo")
225+
dash_duo.wait_for_text_to_equal("#out2", "2: Yolo")

Diff for: tests/integration/renderer/test_request_hooks.py

+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import json
2+
3+
import dash_html_components as html
4+
import dash_core_components as dcc
5+
from dash import Dash
6+
from dash.dependencies import Output, Input
7+
8+
9+
def test_rdrh001_request_hooks(dash_duo):
10+
app = Dash(__name__)
11+
12+
app.index_string = """<!DOCTYPE html>
13+
<html>
14+
<head>
15+
{%metas%}
16+
<title>{%title%}</title>
17+
{%favicon%}
18+
{%css%}
19+
</head>
20+
<body>
21+
<div>Testing custom DashRenderer</div>
22+
{%app_entry%}
23+
<footer>
24+
{%config%}
25+
{%scripts%}
26+
<script id="_dash-renderer" type"application/json">
27+
const renderer = new DashRenderer({
28+
request_pre: (payload) => {
29+
var output = document.getElementById('output-pre')
30+
var outputPayload = document.getElementById('output-pre-payload')
31+
if(output) {
32+
output.innerHTML = 'request_pre changed this text!';
33+
}
34+
if(outputPayload) {
35+
outputPayload.innerHTML = JSON.stringify(payload);
36+
}
37+
},
38+
request_post: (payload, response) => {
39+
var output = document.getElementById('output-post')
40+
var outputPayload = document.getElementById('output-post-payload')
41+
var outputResponse = document.getElementById('output-post-response')
42+
if(output) {
43+
output.innerHTML = 'request_post changed this text!';
44+
}
45+
if(outputPayload) {
46+
outputPayload.innerHTML = JSON.stringify(payload);
47+
}
48+
if(outputResponse) {
49+
outputResponse.innerHTML = JSON.stringify(response);
50+
}
51+
}
52+
})
53+
</script>
54+
</footer>
55+
<div>With request hooks</div>
56+
</body>
57+
</html>"""
58+
59+
app.layout = html.Div(
60+
[
61+
dcc.Input(id="input", value="initial value"),
62+
html.Div(
63+
html.Div(
64+
[
65+
html.Div(id="output-1"),
66+
html.Div(id="output-pre"),
67+
html.Div(id="output-pre-payload"),
68+
html.Div(id="output-post"),
69+
html.Div(id="output-post-payload"),
70+
html.Div(id="output-post-response"),
71+
]
72+
)
73+
),
74+
]
75+
)
76+
77+
@app.callback(Output("output-1", "children"), [Input("input", "value")])
78+
def update_output(value):
79+
return value
80+
81+
dash_duo.start_server(app)
82+
83+
_in = dash_duo.find_element("#input")
84+
dash_duo.clear_input(_in)
85+
86+
_in.send_keys("fire request hooks")
87+
88+
dash_duo.wait_for_text_to_equal("#output-1", "fire request hooks")
89+
dash_duo.wait_for_text_to_equal("#output-pre", "request_pre changed this text!")
90+
dash_duo.wait_for_text_to_equal("#output-post", "request_post changed this text!")
91+
92+
assert json.loads(dash_duo.find_element("#output-pre-payload").text) == {
93+
"output": "output-1.children",
94+
"outputs": {"id": "output-1", "property": "children"},
95+
"changedPropIds": ["input.value"],
96+
"inputs": [{"id": "input", "property": "value", "value": "fire request hooks"}],
97+
}
98+
99+
assert json.loads(dash_duo.find_element("#output-post-payload").text) == {
100+
"output": "output-1.children",
101+
"outputs": {"id": "output-1", "property": "children"},
102+
"changedPropIds": ["input.value"],
103+
"inputs": [{"id": "input", "property": "value", "value": "fire request hooks"}],
104+
}
105+
106+
assert json.loads(dash_duo.find_element("#output-post-response").text) == {
107+
"output-1": {"children": "fire request hooks"}
108+
}
109+
110+
dash_duo.percy_snapshot(name="request-hooks render")
111+
112+
113+
def test_rdrh002_with_custom_renderer_interpolated(dash_duo):
114+
115+
renderer = """
116+
<script id="_dash-renderer" type="application/javascript">
117+
console.log('firing up a custom renderer!')
118+
const renderer = new DashRenderer({
119+
request_pre: () => {
120+
var output = document.getElementById('output-pre')
121+
if(output) {
122+
output.innerHTML = 'request_pre was here!';
123+
}
124+
},
125+
request_post: () => {
126+
var output = document.getElementById('output-post')
127+
if(output) {
128+
output.innerHTML = 'request_post!!!';
129+
}
130+
}
131+
})
132+
</script>
133+
"""
134+
135+
class CustomDash(Dash):
136+
def interpolate_index(self, **kwargs):
137+
return """<!DOCTYPE html>
138+
<html>
139+
<head>
140+
<title>My App</title>
141+
</head>
142+
<body>
143+
144+
<div id="custom-header">My custom header</div>
145+
{app_entry}
146+
{config}
147+
{scripts}
148+
{renderer}
149+
<div id="custom-footer">My custom footer</div>
150+
</body>
151+
</html>""".format(
152+
app_entry=kwargs["app_entry"],
153+
config=kwargs["config"],
154+
scripts=kwargs["scripts"],
155+
renderer=renderer,
156+
)
157+
158+
app = CustomDash()
159+
160+
app.layout = html.Div(
161+
[
162+
dcc.Input(id="input", value="initial value"),
163+
html.Div(
164+
html.Div(
165+
[
166+
html.Div(id="output-1"),
167+
html.Div(id="output-pre"),
168+
html.Div(id="output-post"),
169+
]
170+
)
171+
),
172+
]
173+
)
174+
175+
@app.callback(Output("output-1", "children"), [Input("input", "value")])
176+
def update_output(value):
177+
return value
178+
179+
dash_duo.start_server(app)
180+
181+
input1 = dash_duo.find_element("#input")
182+
dash_duo.clear_input(input1)
183+
184+
input1.send_keys("fire request hooks")
185+
186+
dash_duo.wait_for_text_to_equal("#output-1", "fire request hooks")
187+
assert dash_duo.find_element("#output-pre").text == "request_pre was here!"
188+
assert dash_duo.find_element("#output-post").text == "request_post!!!"
189+
190+
dash_duo.percy_snapshot(name="request-hooks interpolated")

0 commit comments

Comments
 (0)