Skip to content

Fix #1383 prevent_initial_call positional arg regression #1384

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to fix https://app.circleci.com/pipelines/github/plotly/dash/1693/workflows/002acae1-f322-4389-928c-f2c14deaab1b/jobs/27184

Seems like there's something weird going on in the latest release (tonight!) of cryptography, which is pulled in by requests - see eg pyca/cryptography#5412

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):
Copy link
Collaborator Author

@alexcjohnson alexcjohnson Aug 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL - I thought this was going to be broken:

flat_args += arg if isinstance(arg, (list, tuple)) else [arg]

but turns out even though a = [] + () is an error, a = []; a += () is fine https://python-forum.io/Thread-concatenating-list-with-tuple

Anyway now it's tested :)

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