Skip to content

Commit

Permalink
Merge pull request #1384 from plotly/1383-callback-args-fix
Browse files Browse the repository at this point in the history
Fix #1383 prevent_initial_call positional arg regression
  • Loading branch information
alexcjohnson committed Aug 27, 2020
2 parents 3222191 + 5a702bf commit d9a8d9c
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 275 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).

## [UNRELEASED]
### Fixed
- [#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

## [1.15.0] - 2020-08-25
### Added
- [#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)`.
Expand Down
2 changes: 1 addition & 1 deletion dash/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def handle_callback_args(args, kwargs):
"""Split args into outputs, inputs and states"""
prevent_initial_call = kwargs.get("prevent_initial_call", None)
if prevent_initial_call is None and args and isinstance(args[-1], bool):
prevent_initial_call = args.pop()
args, prevent_initial_call = args[:-1], args[-1]

# flatten args, to support the older syntax where outputs, inputs, and states
# each needed to be in their own list
Expand Down
1 change: 1 addition & 0 deletions requires-testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pytest-mock==2.0.0;python_version=="2.7"
lxml==4.5.0
selenium==3.141.0
percy==2.0.2
cryptography==3.0
requests[security]==2.21.0
beautifulsoup4==4.8.2
waitress==1.4.3
18 changes: 18 additions & 0 deletions tests/integration/callbacks/test_prevent_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,21 @@ def test_cbpi003_multi_outputs(flavor, dash_duo):
dash_duo.wait_for_text_to_equal("#c", "BlueCheese")
dash_duo.wait_for_text_to_equal("#b", "Cheese")
dash_duo.wait_for_text_to_equal("#a", "Blue")


def test_cbpi004_positional_arg(dash_duo):
app = dash.Dash(__name__)
app.layout = html.Div([html.Button("click", id="btn"), html.Div(id="out")])

@app.callback(Output("out", "children"), Input("btn", "n_clicks"), True)
def f(n):
return n

dash_duo.start_server(app)
dash_duo._wait_for_callbacks()

dash_duo.wait_for_text_to_equal("#out", "")

dash_duo.find_element("#btn").click()

dash_duo.wait_for_text_to_equal("#out", "1")
30 changes: 30 additions & 0 deletions tests/integration/callbacks/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,33 @@ def o2(i, s):
dash_duo.start_server(app)
dash_duo.wait_for_text_to_equal("#out1", "1: High")
dash_duo.wait_for_text_to_equal("#out2", "2: High")


def test_cbva005_tuple_args(dash_duo):
app = Dash(__name__)
app.layout = html.Div(
[
html.Div("Yo", id="in1"),
html.Div("lo", id="in2"),
html.Div(id="out1"),
html.Div(id="out2"),
]
)

@app.callback(
Output("out1", "children"), (Input("in1", "children"), Input("in2", "children"))
)
def f(i1, i2):
return "1: " + i1 + i2

@app.callback(
(Output("out2", "children"),),
Input("in1", "children"),
(State("in2", "children"),),
)
def g(i1, i2):
return ("2: " + i1 + i2,)

dash_duo.start_server(app)
dash_duo.wait_for_text_to_equal("#out1", "1: Yolo")
dash_duo.wait_for_text_to_equal("#out2", "2: Yolo")
190 changes: 190 additions & 0 deletions tests/integration/renderer/test_request_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import json

import dash_html_components as html
import dash_core_components as dcc
from dash import Dash
from dash.dependencies import Output, Input


def test_rdrh001_request_hooks(dash_duo):
app = Dash(__name__)

app.index_string = """<!DOCTYPE html>
<html>
<head>
{%metas%}
<title>{%title%}</title>
{%favicon%}
{%css%}
</head>
<body>
<div>Testing custom DashRenderer</div>
{%app_entry%}
<footer>
{%config%}
{%scripts%}
<script id="_dash-renderer" type"application/json">
const renderer = new DashRenderer({
request_pre: (payload) => {
var output = document.getElementById('output-pre')
var outputPayload = document.getElementById('output-pre-payload')
if(output) {
output.innerHTML = 'request_pre changed this text!';
}
if(outputPayload) {
outputPayload.innerHTML = JSON.stringify(payload);
}
},
request_post: (payload, response) => {
var output = document.getElementById('output-post')
var outputPayload = document.getElementById('output-post-payload')
var outputResponse = document.getElementById('output-post-response')
if(output) {
output.innerHTML = 'request_post changed this text!';
}
if(outputPayload) {
outputPayload.innerHTML = JSON.stringify(payload);
}
if(outputResponse) {
outputResponse.innerHTML = JSON.stringify(response);
}
}
})
</script>
</footer>
<div>With request hooks</div>
</body>
</html>"""

app.layout = html.Div(
[
dcc.Input(id="input", value="initial value"),
html.Div(
html.Div(
[
html.Div(id="output-1"),
html.Div(id="output-pre"),
html.Div(id="output-pre-payload"),
html.Div(id="output-post"),
html.Div(id="output-post-payload"),
html.Div(id="output-post-response"),
]
)
),
]
)

@app.callback(Output("output-1", "children"), [Input("input", "value")])
def update_output(value):
return value

dash_duo.start_server(app)

_in = dash_duo.find_element("#input")
dash_duo.clear_input(_in)

_in.send_keys("fire request hooks")

dash_duo.wait_for_text_to_equal("#output-1", "fire request hooks")
dash_duo.wait_for_text_to_equal("#output-pre", "request_pre changed this text!")
dash_duo.wait_for_text_to_equal("#output-post", "request_post changed this text!")

assert json.loads(dash_duo.find_element("#output-pre-payload").text) == {
"output": "output-1.children",
"outputs": {"id": "output-1", "property": "children"},
"changedPropIds": ["input.value"],
"inputs": [{"id": "input", "property": "value", "value": "fire request hooks"}],
}

assert json.loads(dash_duo.find_element("#output-post-payload").text) == {
"output": "output-1.children",
"outputs": {"id": "output-1", "property": "children"},
"changedPropIds": ["input.value"],
"inputs": [{"id": "input", "property": "value", "value": "fire request hooks"}],
}

assert json.loads(dash_duo.find_element("#output-post-response").text) == {
"output-1": {"children": "fire request hooks"}
}

dash_duo.percy_snapshot(name="request-hooks render")


def test_rdrh002_with_custom_renderer_interpolated(dash_duo):

renderer = """
<script id="_dash-renderer" type="application/javascript">
console.log('firing up a custom renderer!')
const renderer = new DashRenderer({
request_pre: () => {
var output = document.getElementById('output-pre')
if(output) {
output.innerHTML = 'request_pre was here!';
}
},
request_post: () => {
var output = document.getElementById('output-post')
if(output) {
output.innerHTML = 'request_post!!!';
}
}
})
</script>
"""

class CustomDash(Dash):
def interpolate_index(self, **kwargs):
return """<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="custom-header">My custom header</div>
{app_entry}
{config}
{scripts}
{renderer}
<div id="custom-footer">My custom footer</div>
</body>
</html>""".format(
app_entry=kwargs["app_entry"],
config=kwargs["config"],
scripts=kwargs["scripts"],
renderer=renderer,
)

app = CustomDash()

app.layout = html.Div(
[
dcc.Input(id="input", value="initial value"),
html.Div(
html.Div(
[
html.Div(id="output-1"),
html.Div(id="output-pre"),
html.Div(id="output-post"),
]
)
),
]
)

@app.callback(Output("output-1", "children"), [Input("input", "value")])
def update_output(value):
return value

dash_duo.start_server(app)

input1 = dash_duo.find_element("#input")
dash_duo.clear_input(input1)

input1.send_keys("fire request hooks")

dash_duo.wait_for_text_to_equal("#output-1", "fire request hooks")
assert dash_duo.find_element("#output-pre").text == "request_pre was here!"
assert dash_duo.find_element("#output-post").text == "request_post!!!"

dash_duo.percy_snapshot(name="request-hooks interpolated")
Loading

0 comments on commit d9a8d9c

Please sign in to comment.