diff --git a/dash/CHANGELOG.md b/dash/CHANGELOG.md
index b80865ac34..403d092b61 100644
--- a/dash/CHANGELOG.md
+++ b/dash/CHANGELOG.md
@@ -1,3 +1,6 @@
+## Unreleased
+### Fixed
+- [#821](https://github.com/plotly/dash/pull/821) Fix a bug with callback error reporting, [#791](https://github.com/plotly/dash/issues/791).
## [1.0.1] - 2019-07-09
### Changed
diff --git a/dash/dash.py b/dash/dash.py
index deb28d76fc..461bc7c4ab 100644
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -14,6 +14,7 @@
import pprint
from functools import wraps
+from textwrap import dedent
import flask
from flask import Flask, Response
@@ -808,13 +809,13 @@ def _validate_callback(self, output, inputs, state):
if (layout is None and not self.config.suppress_callback_exceptions):
# Without a layout, we can't do validation on the IDs and
# properties of the elements in the callback.
- raise exceptions.LayoutIsNotDefined('''
+ raise exceptions.LayoutIsNotDefined(dedent('''
Attempting to assign a callback to the application but
the `layout` property has not been assigned.
Assign the `layout` property before assigning callbacks.
Alternatively, suppress this warning by setting
`suppress_callback_exceptions=True`
- '''.replace(' ', ''))
+ '''))
outputs = output if is_multi else [output]
for args, obj, name in [(outputs, Output, 'Output'),
@@ -849,7 +850,10 @@ def _validate_callback(self, output, inputs, state):
arg_id = arg.component_id
arg_prop = getattr(arg, 'component_property', None)
if (arg_id not in layout and arg_id != layout_id):
- raise exceptions.NonExistentIdException('''
+ all_ids = [k for k in layout]
+ if layout_id:
+ all_ids.append(layout_id)
+ raise exceptions.NonExistentIdException(dedent('''
Attempting to assign a callback to the
component with the id "{0}" but no
components with id "{0}" exist in the
@@ -860,12 +864,7 @@ def _validate_callback(self, output, inputs, state):
(and therefore not in the initial layout), then
you can suppress this exception by setting
`suppress_callback_exceptions=True`.
- '''.format(
- arg_id,
- list(layout.keys()) + (
- [layout_id] if layout_id else []
- )
- ).replace(' ', ''))
+ ''').format(arg_id, all_ids))
component = (
layout if layout_id == arg_id else layout[arg_id]
@@ -875,34 +874,34 @@ def _validate_callback(self, output, inputs, state):
arg_prop not in component.available_properties and
not any(arg_prop.startswith(w) for w in
component.available_wildcard_properties)):
- raise exceptions.NonExistentPropException('''
+ raise exceptions.NonExistentPropException(dedent('''
Attempting to assign a callback with
the property "{0}" but the component
"{1}" doesn't have "{0}" as a property.\n
Here are the available properties in "{1}":
{2}
- '''.format(
+ ''').format(
arg_prop, arg_id, component.available_properties
- ).replace(' ', ''))
+ ))
if hasattr(arg, 'component_event'):
- raise exceptions.NonExistentEventException('''
+ raise exceptions.NonExistentEventException(dedent('''
Events have been removed.
Use the associated property instead.
- '''.replace(' ', ''))
+ '''))
if state and not inputs:
- raise exceptions.MissingInputsException('''
+ raise exceptions.MissingInputsException(dedent('''
This callback has {} `State` {}
but no `Input` elements.\n
Without `Input` elements, this callback
will never get called.\n
(Subscribing to input components will cause the
callback to be called whenever their values change.)
- '''.format(
+ ''').format(
len(state),
'elements' if len(state) > 1 else 'element'
- ).replace(' ', ''))
+ ))
for i in inputs:
bad = None
@@ -953,25 +952,22 @@ def duplicate_check():
return callback_id in callbacks
if duplicate_check():
if is_multi:
- msg = '''
+ msg = dedent('''
Multi output {} contains an `Output` object
that was already assigned.
Duplicates:
{}
- '''.format(
+ ''').format(
callback_id,
pprint.pformat(ns['duplicates'])
- ).replace(' ', '')
+ )
else:
- msg = '''
+ msg = dedent('''
You have already assigned a callback to the output
with ID "{}" and property "{}". An output can only have
a single callback function. Try combining your inputs and
callback functions together into one function.
- '''.format(
- output.component_id,
- output.component_property
- ).replace(' ', '')
+ ''').format(output.component_id, output.component_property)
raise exceptions.DuplicateCallbackOutput(msg)
@staticmethod
@@ -984,7 +980,7 @@ def _raise_invalid(bad_val, outer_val, path, index=None,
outer_id = "(id={:s})".format(outer_val.id) \
if getattr(outer_val, 'id', False) else ''
outer_type = type(outer_val).__name__
- raise exceptions.InvalidCallbackReturnValue('''
+ raise exceptions.InvalidCallbackReturnValue(dedent('''
The callback for `{output:s}`
returned a {object:s} having type `{type:s}`
which is not JSON serializable.
@@ -996,15 +992,15 @@ def _raise_invalid(bad_val, outer_val, path, index=None,
In general, Dash properties can only be
dash components, strings, dictionaries, numbers, None,
or lists of those.
- '''.format(
+ ''').format(
output=repr(output),
object='tree with one value' if not toplevel else 'value',
type=bad_type,
location_header=(
'The value in question is located at'
if not toplevel else
- '''The value in question is either the only value returned,
- or is in the top level of the returned list,'''
+ 'The value in question is either the only value returned,'
+ '\nor is in the top level of the returned list,'
),
location=(
"\n" +
@@ -1014,7 +1010,7 @@ def _raise_invalid(bad_val, outer_val, path, index=None,
+ "\n" + path + "\n"
) if not toplevel else '',
bad_val=bad_val
- ).replace(' ', ''))
+ ))
def _value_is_valid(val):
return (
@@ -1228,7 +1224,7 @@ def add_context(*args, **kwargs):
)
except TypeError:
self._validate_callback_output(output_value, output)
- raise exceptions.InvalidCallbackReturnValue('''
+ raise exceptions.InvalidCallbackReturnValue(dedent('''
The callback for property `{property:s}`
of component `{id:s}` returned a value
which is not JSON serializable.
@@ -1236,10 +1232,10 @@ def add_context(*args, **kwargs):
In general, Dash properties can only be
dash components, strings, dictionaries, numbers, None,
or lists of those.
- '''.format(
+ ''').format(
property=output.component_property,
id=output.component_id
- ).replace(' ', ''))
+ ))
return jsonResponse
diff --git a/dash/testing/browser.py b/dash/testing/browser.py
index b36c185381..cb12774d4d 100644
--- a/dash/testing/browser.py
+++ b/dash/testing/browser.py
@@ -69,7 +69,7 @@ def __exit__(self, exc_type, exc_val, traceback):
self.driver.quit()
self.percy_runner.finalize_build()
except WebDriverException:
- logger.exception("webdriver quit was not successfully")
+ logger.exception("webdriver quit was not successful")
except percy.errors.Error:
logger.exception("percy runner failed to finalize properly")
@@ -247,16 +247,22 @@ def open_new_tab(self, url=None):
)
def get_webdriver(self, remote):
- return (
- getattr(self, "_get_{}".format(self._browser))()
- if remote is None
- else webdriver.Remote(
- command_executor=remote,
- desired_capabilities=getattr(
- DesiredCapabilities, self._browser.upper()
- ),
- )
- )
+ # occasionally the browser fails to start - give it 3 tries
+ for i in reversed(range(3)):
+ try:
+ return (
+ getattr(self, "_get_{}".format(self._browser))()
+ if remote is None
+ else webdriver.Remote(
+ command_executor=remote,
+ desired_capabilities=getattr(
+ DesiredCapabilities, self._browser.upper()
+ ),
+ )
+ )
+ except WebDriverException:
+ if not i:
+ raise
def _get_wd_options(self):
options = (
diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py
index b17825fa7b..57bc9eb039 100644
--- a/tests/integration/test_integration.py
+++ b/tests/integration/test_integration.py
@@ -1,7 +1,5 @@
from multiprocessing import Value
import datetime
-import itertools
-import re
import time
import pytest
@@ -18,87 +16,98 @@
from dash.dependencies import Input, Output, State
from dash.exceptions import (
- PreventUpdate, DuplicateCallbackOutput, CallbackException,
- MissingCallbackContextException, InvalidCallbackReturnValue,
- IncorrectTypeException
+ PreventUpdate,
+ DuplicateCallbackOutput,
+ CallbackException,
+ MissingCallbackContextException,
+ InvalidCallbackReturnValue,
+ IncorrectTypeException,
+ NonExistentIdException,
)
from dash.testing.wait import until
def test_inin001_simple_callback(dash_duo):
app = Dash(__name__)
- app.layout = html.Div([
- dcc.Input(id='input', value='initial value'),
- html.Div(html.Div([1.5, None, 'string', html.Div(id='output-1')]))
- ])
+ app.layout = html.Div(
+ [
+ dcc.Input(id="input", value="initial value"),
+ html.Div(html.Div([1.5, None, "string", html.Div(id="output-1")])),
+ ]
+ )
- call_count = Value('i', 0)
+ call_count = Value("i", 0)
- @app.callback(Output('output-1', 'children'), [Input('input', 'value')])
+ @app.callback(Output("output-1", "children"), [Input("input", "value")])
def update_output(value):
call_count.value += 1
return value
dash_duo.start_server(app)
- dash_duo.wait_for_text_to_equal('#output-1', 'initial value')
- dash_duo.percy_snapshot(name='simple-callback-1')
+ dash_duo.wait_for_text_to_equal("#output-1", "initial value")
+ dash_duo.percy_snapshot(name="simple-callback-1")
- input1 = dash_duo.find_element('#input')
+ input1 = dash_duo.find_element("#input")
dash_duo.clear_input(input1)
- input1.send_keys('hello world')
+ input1.send_keys("hello world")
- dash_duo.wait_for_text_to_equal('#output-1', 'hello world')
- dash_duo.percy_snapshot(name='simple-callback-2')
+ dash_duo.wait_for_text_to_equal("#output-1", "hello world")
+ dash_duo.percy_snapshot(name="simple-callback-2")
# an initial call, one for clearing the input
# and one for each hello world character
- assert call_count.value == 2 + len('hello world')
+ assert call_count.value == 2 + len("hello world")
assert not dash_duo.get_logs()
def test_inin002_wildcard_callback(dash_duo):
app = Dash(__name__)
- app.layout = html.Div([
- dcc.Input(id='input', value='initial value'),
- html.Div(
- html.Div([
- 1.5,
- None,
- 'string',
- html.Div(id='output-1', **{'data-cb': 'initial value',
- 'aria-cb': 'initial value'})
- ])
- )
- ])
+ app.layout = html.Div(
+ [
+ dcc.Input(id="input", value="initial value"),
+ html.Div(
+ html.Div(
+ [
+ 1.5,
+ None,
+ "string",
+ html.Div(
+ id="output-1",
+ **{"data-cb": "initial value", "aria-cb": "initial value"}
+ ),
+ ]
+ )
+ ),
+ ]
+ )
- input_call_count = Value('i', 0)
+ input_call_count = Value("i", 0)
- @app.callback(Output('output-1', 'data-cb'), [Input('input', 'value')])
+ @app.callback(Output("output-1", "data-cb"), [Input("input", "value")])
def update_data(value):
input_call_count.value += 1
return value
- @app.callback(Output('output-1', 'children'),
- [Input('output-1', 'data-cb')])
+ @app.callback(Output("output-1", "children"), [Input("output-1", "data-cb")])
def update_text(data):
return data
dash_duo.start_server(app)
- dash_duo.wait_for_text_to_equal('#output-1', 'initial value')
- dash_duo.percy_snapshot(name='wildcard-callback-1')
+ dash_duo.wait_for_text_to_equal("#output-1", "initial value")
+ dash_duo.percy_snapshot(name="wildcard-callback-1")
- input1 = dash_duo.find_element('#input')
+ input1 = dash_duo.find_element("#input")
dash_duo.clear_input(input1)
- input1.send_keys('hello world')
+ input1.send_keys("hello world")
- dash_duo.wait_for_text_to_equal('#output-1', 'hello world')
- dash_duo.percy_snapshot(name='wildcard-callback-2')
+ dash_duo.wait_for_text_to_equal("#output-1", "hello world")
+ dash_duo.percy_snapshot(name="wildcard-callback-2")
# an initial call, one for clearing the input
# and one for each hello world character
- assert input_call_count.value == 2 + len('hello world')
+ assert input_call_count.value == 2 + len("hello world")
assert not dash_duo.get_logs()
@@ -109,20 +118,22 @@ def test_inin003_aborted_callback(dash_duo):
prevents update and triggering dependencies
"""
- initial_input = 'initial input'
- initial_output = 'initial output'
+ initial_input = "initial input"
+ initial_output = "initial output"
app = Dash(__name__)
- app.layout = html.Div([
- dcc.Input(id='input', value=initial_input),
- html.Div(initial_output, id='output1'),
- html.Div(initial_output, id='output2'),
- ])
+ app.layout = html.Div(
+ [
+ dcc.Input(id="input", value=initial_input),
+ html.Div(initial_output, id="output1"),
+ html.Div(initial_output, id="output2"),
+ ]
+ )
- callback1_count = Value('i', 0)
- callback2_count = Value('i', 0)
+ callback1_count = Value("i", 0)
+ callback2_count = Value("i", 0)
- @app.callback(Output('output1', 'children'), [Input('input', 'value')])
+ @app.callback(Output("output1", "children"), [Input("input", "value")])
def callback1(value):
callback1_count.value += 1
if callback1_count.value > 2:
@@ -130,35 +141,34 @@ def callback1(value):
raise PreventUpdate("testing callback does not update")
return value
- @app.callback(
- Output('output2', 'children'), [Input('output1', 'children')]
- )
+ @app.callback(Output("output2", "children"), [Input("output1", "children")])
def callback2(value):
callback2_count.value += 1
return value
dash_duo.start_server(app)
- input_ = dash_duo.find_element('#input')
- input_.send_keys('xyz')
- dash_duo.wait_for_text_to_equal('#input', 'initial inputxyz')
+ input_ = dash_duo.find_element("#input")
+ input_.send_keys("xyz")
+ dash_duo.wait_for_text_to_equal("#input", "initial inputxyz")
until(
lambda: callback1_count.value == 4,
timeout=3,
- msg="callback1 runs 4x (initial page load and 3x through send_keys)"
+ msg="callback1 runs 4x (initial page load and 3x through send_keys)",
)
- assert callback2_count.value == 0, \
- "callback2 is never triggered, even on initial load"
+ assert (
+ callback2_count.value == 0
+ ), "callback2 is never triggered, even on initial load"
# double check that output1 and output2 children were not updated
- assert dash_duo.find_element('#output1').text == initial_output
- assert dash_duo.find_element('#output2').text == initial_output
+ assert dash_duo.find_element("#output1").text == initial_output
+ assert dash_duo.find_element("#output2").text == initial_output
assert not dash_duo.get_logs()
- dash_duo.percy_snapshot(name='aborted')
+ dash_duo.percy_snapshot(name="aborted")
def test_inin004_wildcard_data_attributes(dash_duo):
@@ -171,107 +181,112 @@ def test_inin004_wildcard_data_attributes(dash_duo):
"data-number": 512,
"data-none": None,
"data-date": test_date,
- "aria-progress": 5
+ "aria-progress": 5,
}
- app.layout = html.Div([html.Div(**attrs)], id='data-element')
+ app.layout = html.Div([html.Div(**attrs)], id="data-element")
dash_duo.start_server(app)
- div = dash_duo.find_element('#data-element')
+ div = dash_duo.find_element("#data-element")
# attribute order is ill-defined - BeautifulSoup will sort them
- actual = BeautifulSoup(div.get_attribute('innerHTML'), 'lxml').decode()
+ actual = BeautifulSoup(div.get_attribute("innerHTML"), "lxml").decode()
expected = BeautifulSoup(
- '
',
- 'lxml'
+ "",
+ "lxml",
).decode()
- assert actual == expected, 'all attrs are included except None values'
+ assert actual == expected, "all attrs are included except None values"
assert not dash_duo.get_logs()
def test_inin005_no_props_component(dash_duo):
app = Dash()
- app.layout = html.Div([
- dash_dangerously_set_inner_html.DangerouslySetInnerHTML('''
+ app.layout = html.Div(
+ [
+ dash_dangerously_set_inner_html.DangerouslySetInnerHTML(
+ """
No Props Component
- ''')
- ])
+ """
+ )
+ ]
+ )
dash_duo.start_server(app)
assert not dash_duo.get_logs()
- dash_duo.percy_snapshot(name='no-props-component')
+ dash_duo.percy_snapshot(name="no-props-component")
def test_inin006_flow_component(dash_duo):
app = Dash()
- app.layout = html.Div([
- dash_flow_example.ExampleReactComponent(
- id='react',
- value='my-value',
- label='react component'
- ),
- dash_flow_example.ExampleFlowComponent(
- id='flow',
- value='my-value',
- label='flow component'
- ),
- html.Hr(),
- html.Div(id='output')
- ])
-
- @app.callback(Output('output', 'children'),
- [Input('react', 'value'), Input('flow', 'value')])
- def display_output(react_value, flow_value):
- return html.Div([
- 'You have entered {} and {}'.format(react_value, flow_value),
- html.Hr(),
- html.Label('Flow Component Docstring'),
- html.Pre(dash_flow_example.ExampleFlowComponent.__doc__),
+ app.layout = html.Div(
+ [
+ dash_flow_example.ExampleReactComponent(
+ id="react", value="my-value", label="react component"
+ ),
+ dash_flow_example.ExampleFlowComponent(
+ id="flow", value="my-value", label="flow component"
+ ),
html.Hr(),
- html.Label('React PropTypes Component Docstring'),
- html.Pre(dash_flow_example.ExampleReactComponent.__doc__),
- html.Div(id='waitfor')
- ])
+ html.Div(id="output"),
+ ]
+ )
+
+ @app.callback(
+ Output("output", "children"), [Input("react", "value"), Input("flow", "value")]
+ )
+ def display_output(react_value, flow_value):
+ return html.Div(
+ [
+ "You have entered {} and {}".format(react_value, flow_value),
+ html.Hr(),
+ html.Label("Flow Component Docstring"),
+ html.Pre(dash_flow_example.ExampleFlowComponent.__doc__),
+ html.Hr(),
+ html.Label("React PropTypes Component Docstring"),
+ html.Pre(dash_flow_example.ExampleReactComponent.__doc__),
+ html.Div(id="waitfor"),
+ ]
+ )
dash_duo.start_server(app)
- dash_duo.wait_for_element('#waitfor')
- dash_duo.percy_snapshot(name='flowtype')
+ dash_duo.wait_for_element("#waitfor")
+ dash_duo.percy_snapshot(name="flowtype")
def test_inin007_meta_tags(dash_duo):
metas = [
- {'name': 'description', 'content': 'my dash app'},
- {'name': 'custom', 'content': 'customized'},
+ {"name": "description", "content": "my dash app"},
+ {"name": "custom", "content": "customized"},
]
app = Dash(meta_tags=metas)
- app.layout = html.Div(id='content')
+ app.layout = html.Div(id="content")
dash_duo.start_server(app)
- meta = dash_duo.find_elements('meta')
+ meta = dash_duo.find_elements("meta")
# -2 for the meta charset and http-equiv.
- assert len(meta) == len(metas) + 2, 'Should have 2 extra meta tags'
+ assert len(meta) == len(metas) + 2, "Should have 2 extra meta tags"
for i in range(2, len(meta)):
meta_tag = meta[i]
meta_info = metas[i - 2]
- assert meta_tag.get_attribute('name') == meta_info['name']
- assert meta_tag.get_attribute('content') == meta_info['content']
+ assert meta_tag.get_attribute("name") == meta_info["name"]
+ assert meta_tag.get_attribute("content") == meta_info["content"]
def test_inin008_index_customization(dash_duo):
app = Dash()
- app.index_string = '''
+ app.index_string = """
@@ -303,24 +318,24 @@ def test_inin008_index_customization(dash_duo):
- '''
+ """
- app.layout = html.Div('Dash app', id='app')
+ app.layout = html.Div("Dash app", id="app")
dash_duo.start_server(app)
- assert dash_duo.find_element('#custom-header').text == 'My custom header'
- assert dash_duo.find_element('#custom-footer').text == 'My custom footer'
- assert dash_duo.wait_for_element('#add').text == 'Got added'
+ assert dash_duo.find_element("#custom-header").text == "My custom header"
+ assert dash_duo.find_element("#custom-footer").text == "My custom footer"
+ assert dash_duo.wait_for_element("#add").text == "Got added"
- dash_duo.percy_snapshot('custom-index')
+ dash_duo.percy_snapshot("custom-index")
def test_inin009_invalid_index_string(dash_duo):
app = Dash()
def will_raise():
- app.index_string = '''
+ app.index_string = """
@@ -336,57 +351,58 @@ def will_raise():
- '''
+ """
with pytest.raises(Exception) as err:
will_raise()
exc_msg = str(err.value)
- assert '{%app_entry%}' in exc_msg
- assert '{%config%}' in exc_msg
- assert '{%scripts%}' in exc_msg
+ assert "{%app_entry%}" in exc_msg
+ assert "{%config%}" in exc_msg
+ assert "{%scripts%}" in exc_msg
- app.layout = html.Div('Hello World', id='a')
+ app.layout = html.Div("Hello World", id="a")
dash_duo.start_server(app)
- assert dash_duo.find_element('#a').text == 'Hello World'
+ assert dash_duo.find_element("#a").text == "Hello World"
def test_inin010_func_layout_accepted(dash_duo):
app = Dash()
def create_layout():
- return html.Div('Hello World', id='a')
+ return html.Div("Hello World", id="a")
+
app.layout = create_layout
dash_duo.start_server(app)
- assert dash_duo.find_element('#a').text == 'Hello World'
+ assert dash_duo.find_element("#a").text == "Hello World"
def test_inin011_multi_output(dash_duo):
app = Dash(__name__)
- app.layout = html.Div([
- html.Button('OUTPUT', id='output-btn'),
-
- html.Table([
- html.Thead([
- html.Tr([html.Th('Output 1'), html.Th('Output 2')])
- ]),
- html.Tbody([
- html.Tr([html.Td(id='output1'), html.Td(id='output2')]),
- ])
- ]),
-
- html.Div(id='output3'),
- html.Div(id='output4'),
- html.Div(id='output5')
- ])
+ app.layout = html.Div(
+ [
+ html.Button("OUTPUT", id="output-btn"),
+ html.Table(
+ [
+ html.Thead([html.Tr([html.Th("Output 1"), html.Th("Output 2")])]),
+ html.Tbody(
+ [html.Tr([html.Td(id="output1"), html.Td(id="output2")])]
+ ),
+ ]
+ ),
+ html.Div(id="output3"),
+ html.Div(id="output4"),
+ html.Div(id="output5"),
+ ]
+ )
@app.callback(
- [Output('output1', 'children'), Output('output2', 'children')],
- [Input('output-btn', 'n_clicks')],
- [State('output-btn', 'n_clicks_timestamp')]
+ [Output("output1", "children"), Output("output2", "children")],
+ [Input("output-btn", "n_clicks")],
+ [State("output-btn", "n_clicks_timestamp")],
)
def on_click(n_clicks, n_clicks_timestamp):
if n_clicks is None:
@@ -395,67 +411,72 @@ def on_click(n_clicks, n_clicks_timestamp):
return n_clicks, n_clicks_timestamp
# Dummy callback for DuplicateCallbackOutput test.
- @app.callback(Output('output3', 'children'),
- [Input('output-btn', 'n_clicks')])
+ @app.callback(Output("output3", "children"), [Input("output-btn", "n_clicks")])
def dummy_callback(n_clicks):
if n_clicks is None:
raise PreventUpdate
- return 'Output 3: {}'.format(n_clicks)
+ return "Output 3: {}".format(n_clicks)
with pytest.raises(
DuplicateCallbackOutput,
- message="multi output can't be included in a single output"
+ message="multi output can't be included in a single output",
) as err:
- @app.callback(Output('output1', 'children'),
- [Input('output-btn', 'n_clicks')])
+
+ @app.callback(Output("output1", "children"), [Input("output-btn", "n_clicks")])
def on_click_duplicate(n_clicks):
if n_clicks is None:
raise PreventUpdate
- return 'something else'
+ return "something else"
- assert 'output1' in err.value.args[0]
+ assert "output1" in err.value.args[0]
with pytest.raises(
DuplicateCallbackOutput,
- message="multi output cannot contain a used single output"
+ message="multi output cannot contain a used single output",
) as err:
- @app.callback([Output('output3', 'children'),
- Output('output4', 'children')],
- [Input('output-btn', 'n_clicks')])
+
+ @app.callback(
+ [Output("output3", "children"), Output("output4", "children")],
+ [Input("output-btn", "n_clicks")],
+ )
def on_click_duplicate_multi(n_clicks):
if n_clicks is None:
raise PreventUpdate
- return 'something else'
+ return "something else"
- assert 'output3' in err.value.args[0]
+ assert "output3" in err.value.args[0]
with pytest.raises(
DuplicateCallbackOutput,
- message="same output cannot be used twice in one callback"
+ message="same output cannot be used twice in one callback",
) as err:
- @app.callback([Output('output5', 'children'),
- Output('output5', 'children')],
- [Input('output-btn', 'n_clicks')])
+
+ @app.callback(
+ [Output("output5", "children"), Output("output5", "children")],
+ [Input("output-btn", "n_clicks")],
+ )
def on_click_same_output(n_clicks):
return n_clicks
- assert 'output5' in err.value.args[0]
+ assert "output5" in err.value.args[0]
with pytest.raises(
DuplicateCallbackOutput,
- message="no part of an existing multi-output can be used in another"
+ message="no part of an existing multi-output can be used in another",
) as err:
- @app.callback([Output('output1', 'children'),
- Output('output5', 'children')],
- [Input('output-btn', 'n_clicks')])
+
+ @app.callback(
+ [Output("output1", "children"), Output("output5", "children")],
+ [Input("output-btn", "n_clicks")],
+ )
def overlapping_multi_output(n_clicks):
return n_clicks
assert (
- '{\'output1.children\'}' in err.value.args[0]
+ "{'output1.children'}" in err.value.args[0]
or "set(['output1.children'])" in err.value.args[0]
)
@@ -463,109 +484,116 @@ def overlapping_multi_output(n_clicks):
t = time.time()
- btn = dash_duo.find_element('#output-btn')
+ btn = dash_duo.find_element("#output-btn")
btn.click()
time.sleep(1)
- dash_duo.wait_for_text_to_equal('#output1', '1')
+ dash_duo.wait_for_text_to_equal("#output1", "1")
- assert int(dash_duo.find_element('#output2').text) > t
+ assert int(dash_duo.find_element("#output2").text) > t
def test_inin012_multi_output_no_update(dash_duo):
app = Dash(__name__)
- app.layout = html.Div([
- html.Button('B', 'btn'),
- html.P('initial1', 'n1'),
- html.P('initial2', 'n2'),
- html.P('initial3', 'n3')
- ])
-
- @app.callback([Output('n1', 'children'),
- Output('n2', 'children'),
- Output('n3', 'children')],
- [Input('btn', 'n_clicks')])
+ app.layout = html.Div(
+ [
+ html.Button("B", "btn"),
+ html.P("initial1", "n1"),
+ html.P("initial2", "n2"),
+ html.P("initial3", "n3"),
+ ]
+ )
+
+ @app.callback(
+ [Output("n1", "children"), Output("n2", "children"), Output("n3", "children")],
+ [Input("btn", "n_clicks")],
+ )
def show_clicks(n):
# partial or complete cancelation of updates via no_update
return [
no_update if n and n > 4 else n,
no_update if n and n > 2 else n,
- no_update
+ no_update,
]
dash_duo.start_server(app)
- dash_duo.multiple_click('#btn', 10)
+ dash_duo.multiple_click("#btn", 10)
- dash_duo.wait_for_text_to_equal('#n1', '4')
- dash_duo.wait_for_text_to_equal('#n2', '2')
- dash_duo.wait_for_text_to_equal('#n3', 'initial3')
+ dash_duo.wait_for_text_to_equal("#n1", "4")
+ dash_duo.wait_for_text_to_equal("#n2", "2")
+ dash_duo.wait_for_text_to_equal("#n3", "initial3")
def test_inin013_no_update_chains(dash_duo):
app = Dash(__name__)
- app.layout = html.Div([
- dcc.Input(id='a_in', value='a'),
- dcc.Input(id='b_in', value='b'),
- html.P('', id='a_out'),
- html.P('', id='a_out_short'),
- html.P('', id='b_out'),
- html.P('', id='ab_out')
- ])
-
- @app.callback([Output('a_out', 'children'),
- Output('a_out_short', 'children')],
- [Input('a_in', 'value')])
+ app.layout = html.Div(
+ [
+ dcc.Input(id="a_in", value="a"),
+ dcc.Input(id="b_in", value="b"),
+ html.P("", id="a_out"),
+ html.P("", id="a_out_short"),
+ html.P("", id="b_out"),
+ html.P("", id="ab_out"),
+ ]
+ )
+
+ @app.callback(
+ [Output("a_out", "children"), Output("a_out_short", "children")],
+ [Input("a_in", "value")],
+ )
def a_out(a):
return (a, a if len(a) < 3 else no_update)
- @app.callback(Output('b_out', 'children'), [Input('b_in', 'value')])
+ @app.callback(Output("b_out", "children"), [Input("b_in", "value")])
def b_out(b):
return b
- @app.callback(Output('ab_out', 'children'),
- [Input('a_out_short', 'children')],
- [State('b_out', 'children')])
+ @app.callback(
+ Output("ab_out", "children"),
+ [Input("a_out_short", "children")],
+ [State("b_out", "children")],
+ )
def ab_out(a, b):
- return a + ' ' + b
+ return a + " " + b
dash_duo.start_server(app)
- a_in = dash_duo.find_element('#a_in')
- b_in = dash_duo.find_element('#b_in')
-
- b_in.send_keys('b')
- a_in.send_keys('a')
- dash_duo.wait_for_text_to_equal('#a_out', 'aa')
- dash_duo.wait_for_text_to_equal('#b_out', 'bb')
- dash_duo.wait_for_text_to_equal('#a_out_short', 'aa')
- dash_duo.wait_for_text_to_equal('#ab_out', 'aa bb')
-
- b_in.send_keys('b')
- a_in.send_keys('a')
- dash_duo.wait_for_text_to_equal('#a_out', 'aaa')
- dash_duo.wait_for_text_to_equal('#b_out', 'bbb')
- dash_duo.wait_for_text_to_equal('#a_out_short', 'aa')
+ a_in = dash_duo.find_element("#a_in")
+ b_in = dash_duo.find_element("#b_in")
+
+ b_in.send_keys("b")
+ a_in.send_keys("a")
+ dash_duo.wait_for_text_to_equal("#a_out", "aa")
+ dash_duo.wait_for_text_to_equal("#b_out", "bb")
+ dash_duo.wait_for_text_to_equal("#a_out_short", "aa")
+ dash_duo.wait_for_text_to_equal("#ab_out", "aa bb")
+
+ b_in.send_keys("b")
+ a_in.send_keys("a")
+ dash_duo.wait_for_text_to_equal("#a_out", "aaa")
+ dash_duo.wait_for_text_to_equal("#b_out", "bbb")
+ dash_duo.wait_for_text_to_equal("#a_out_short", "aa")
# ab_out has not been triggered because a_out_short received no_update
- dash_duo.wait_for_text_to_equal('#ab_out', 'aa bb')
+ dash_duo.wait_for_text_to_equal("#ab_out", "aa bb")
- b_in.send_keys('b')
+ b_in.send_keys("b")
a_in.send_keys(Keys.END)
a_in.send_keys(Keys.BACKSPACE)
- dash_duo.wait_for_text_to_equal('#a_out', 'aa')
- dash_duo.wait_for_text_to_equal('#b_out', 'bbbb')
- dash_duo.wait_for_text_to_equal('#a_out_short', 'aa')
+ dash_duo.wait_for_text_to_equal("#a_out", "aa")
+ dash_duo.wait_for_text_to_equal("#b_out", "bbbb")
+ dash_duo.wait_for_text_to_equal("#a_out_short", "aa")
# now ab_out *is* triggered - a_out_short got a new value
# even though that value is the same as the last value it got
- dash_duo.wait_for_text_to_equal('#ab_out', 'aa bbbb')
+ dash_duo.wait_for_text_to_equal("#ab_out", "aa bbbb")
def test_inin014_with_custom_renderer(dash_duo):
app = Dash(__name__)
- app.index_string = '''
+ app.index_string = """
@@ -601,40 +629,44 @@ def test_inin014_with_custom_renderer(dash_duo):
With request hooks
- '''
-
- 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.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')])
+ @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')
+ input1 = dash_duo.find_element("#input")
dash_duo.clear_input(input1)
- input1.send_keys('fire request hooks')
+ 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!!!'
- assert dash_duo.find_element('#output-post').text == 'request_post ran!'
+ dash_duo.wait_for_text_to_equal("#output-1", "fire request hooks")
+ assert dash_duo.find_element("#output-pre").text == "request_pre!!!"
+ assert dash_duo.find_element("#output-post").text == "request_post ran!"
- dash_duo.percy_snapshot(name='request-hooks')
+ dash_duo.percy_snapshot(name="request-hooks")
def test_inin015_with_custom_renderer_interpolated(dash_duo):
- renderer = '''
+ renderer = """
- '''
+ """
class CustomDash(Dash):
-
def interpolate_index(self, **kwargs):
- return '''
+ return """
@@ -673,66 +704,67 @@ def interpolate_index(self, **kwargs):
- '''.format(
- app_entry=kwargs['app_entry'],
- config=kwargs['config'],
- scripts=kwargs['scripts'],
- renderer=renderer)
+ """.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.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')])
+ @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')
+ input1 = dash_duo.find_element("#input")
dash_duo.clear_input(input1)
- input1.send_keys('fire request hooks')
+ 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.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')
+ dash_duo.percy_snapshot(name="request-hooks interpolated")
def test_inin016_modified_response(dash_duo):
app = Dash(__name__)
- app.layout = html.Div([
- dcc.Input(id='input', value='ab'),
- html.Div(id='output')
- ])
+ app.layout = html.Div([dcc.Input(id="input", value="ab"), html.Div(id="output")])
- @app.callback(Output('output', 'children'), [Input('input', 'value')])
+ @app.callback(Output("output", "children"), [Input("input", "value")])
def update_output(value):
- callback_context.response.set_cookie(
- 'dash cookie', value + ' - cookie')
- return value + ' - output'
+ callback_context.response.set_cookie("dash cookie", value + " - cookie")
+ return value + " - output"
dash_duo.start_server(app)
- dash_duo.wait_for_text_to_equal('#output', 'ab - output')
- input1 = dash_duo.find_element('#input')
+ dash_duo.wait_for_text_to_equal("#output", "ab - output")
+ input1 = dash_duo.find_element("#input")
- input1.send_keys('cd')
+ input1.send_keys("cd")
- dash_duo.wait_for_text_to_equal('#output', 'abcd - output')
- cookie = dash_duo.driver.get_cookie('dash cookie')
+ dash_duo.wait_for_text_to_equal("#output", "abcd - output")
+ cookie = dash_duo.driver.get_cookie("dash cookie")
# cookie gets json encoded
- assert cookie['value'] == '"abcd - cookie"'
+ assert cookie["value"] == '"abcd - cookie"'
assert not dash_duo.get_logs()
@@ -740,161 +772,209 @@ def update_output(value):
def test_inin017_late_component_register(dash_duo):
app = Dash()
- app.layout = html.Div([
- html.Button('Click me to put a dcc ', id='btn-insert'),
- html.Div(id='output')
- ])
+ app.layout = html.Div(
+ [html.Button("Click me to put a dcc ", id="btn-insert"), html.Div(id="output")]
+ )
- @app.callback(Output('output', 'children'),
- [Input('btn-insert', 'n_clicks')])
+ @app.callback(Output("output", "children"), [Input("btn-insert", "n_clicks")])
def update_output(value):
if value is None:
raise PreventUpdate
- return dcc.Input(id='inserted-input')
+ return dcc.Input(id="inserted-input")
dash_duo.start_server(app)
- btn = dash_duo.find_element('#btn-insert')
+ btn = dash_duo.find_element("#btn-insert")
btn.click()
- dash_duo.find_element('#inserted-input')
+ dash_duo.find_element("#inserted-input")
def test_inin018_output_input_invalid_callback():
app = Dash(__name__)
- app.layout = html.Div([
- html.Div('child', id='input-output'),
- html.Div(id='out')
- ])
+ app.layout = html.Div([html.Div("child", id="input-output"), html.Div(id="out")])
with pytest.raises(CallbackException) as err:
- @app.callback(Output('input-output', 'children'),
- [Input('input-output', 'children')])
+
+ @app.callback(
+ Output("input-output", "children"), [Input("input-output", "children")]
+ )
def failure(children):
pass
- msg = 'Same output and input: input-output.children'
+ msg = "Same output and input: input-output.children"
assert err.value.args[0] == msg
# Multi output version.
with pytest.raises(CallbackException) as err:
- @app.callback([Output('out', 'children'),
- Output('input-output', 'children')],
- [Input('input-output', 'children')])
+
+ @app.callback(
+ [Output("out", "children"), Output("input-output", "children")],
+ [Input("input-output", "children")],
+ )
def failure2(children):
pass
- msg = 'Same output and input: input-output.children'
+ msg = "Same output and input: input-output.children"
assert err.value.args[0] == msg
def test_inin019_callback_dep_types():
app = Dash(__name__)
- app.layout = html.Div([
- html.Div('child', id='in'),
- html.Div('state', id='state'),
- html.Div(id='out')
- ])
+ app.layout = html.Div(
+ [html.Div("child", id="in"), html.Div("state", id="state"), html.Div(id="out")]
+ )
with pytest.raises(IncorrectTypeException, message="extra output nesting"):
- @app.callback([[Output('out', 'children')]],
- [Input('in', 'children')])
+
+ @app.callback([[Output("out", "children")]], [Input("in", "children")])
def f(i):
return i
with pytest.raises(IncorrectTypeException, message="un-nested input"):
- @app.callback(Output('out', 'children'),
- Input('in', 'children'))
+
+ @app.callback(Output("out", "children"), Input("in", "children"))
def f2(i):
return i
with pytest.raises(IncorrectTypeException, message="un-nested state"):
- @app.callback(Output('out', 'children'),
- [Input('in', 'children')],
- State('state', 'children'))
+
+ @app.callback(
+ Output("out", "children"),
+ [Input("in", "children")],
+ State("state", "children"),
+ )
def f3(i):
return i
# all OK with tuples
- @app.callback((Output('out', 'children'),),
- (Input('in', 'children'),),
- (State('state', 'children'),))
+ @app.callback(
+ (Output("out", "children"),),
+ (Input("in", "children"),),
+ (State("state", "children"),),
+ )
def f4(i):
return i
def test_inin020_callback_return_validation():
app = Dash(__name__)
- app.layout = html.Div([
- html.Div(id='a'),
- html.Div(id='b'),
- html.Div(id='c'),
- html.Div(id='d'),
- html.Div(id='e'),
- html.Div(id='f')
- ])
-
- @app.callback(Output('b', 'children'), [Input('a', 'children')])
+ app.layout = html.Div(
+ [
+ html.Div(id="a"),
+ html.Div(id="b"),
+ html.Div(id="c"),
+ html.Div(id="d"),
+ html.Div(id="e"),
+ html.Div(id="f"),
+ ]
+ )
+
+ @app.callback(Output("b", "children"), [Input("a", "children")])
def single(a):
return set([1])
with pytest.raises(InvalidCallbackReturnValue, message="not serializable"):
- single('aaa')
+ single("aaa")
- @app.callback([Output('c', 'children'), Output('d', 'children')],
- [Input('a', 'children')])
+ @app.callback(
+ [Output("c", "children"), Output("d", "children")], [Input("a", "children")]
+ )
def multi(a):
return [1, set([2])]
- with pytest.raises(
- InvalidCallbackReturnValue, message="nested non-serializable"
- ):
- multi('aaa')
+ with pytest.raises(InvalidCallbackReturnValue, message="nested non-serializable"):
+ multi("aaa")
- @app.callback([Output('e', 'children'), Output('f', 'children')],
- [Input('a', 'children')])
+ @app.callback(
+ [Output("e", "children"), Output("f", "children")], [Input("a", "children")]
+ )
def multi2(a):
- return ['abc']
+ return ["abc"]
- with pytest.raises(
- InvalidCallbackReturnValue, message="wrong-length list"
- ):
- multi2('aaa')
+ with pytest.raises(InvalidCallbackReturnValue, message="wrong-length list"):
+ multi2("aaa")
def test_inin021_callback_context(dash_duo):
app = Dash(__name__)
- btns = ['btn-{}'.format(x) for x in range(1, 6)]
+ btns = ["btn-{}".format(x) for x in range(1, 6)]
- app.layout = html.Div([
- html.Div([html.Button(btn, id=btn) for btn in btns]),
- html.Div(id='output'),
- ])
+ app.layout = html.Div(
+ [html.Div([html.Button(btn, id=btn) for btn in btns]), html.Div(id="output")]
+ )
- @app.callback(Output('output', 'children'),
- [Input(x, 'n_clicks') for x in btns])
+ @app.callback(Output("output", "children"), [Input(x, "n_clicks") for x in btns])
def on_click(*args):
if not callback_context.triggered:
raise PreventUpdate
trigger = callback_context.triggered[0]
- return 'Just clicked {} for the {} time!'.format(
- trigger['prop_id'].split('.')[0], trigger['value']
+ return "Just clicked {} for the {} time!".format(
+ trigger["prop_id"].split(".")[0], trigger["value"]
)
dash_duo.start_server(app)
for i in range(1, 5):
for btn in btns:
- dash_duo.find_element('#' + btn).click()
+ dash_duo.find_element("#" + btn).click()
dash_duo.wait_for_text_to_equal(
- '#output',
- 'Just clicked {} for the {} time!'.format(btn, i)
+ "#output", "Just clicked {} for the {} time!".format(btn, i)
)
def test_inin022_no_callback_context():
- for attr in ['inputs', 'states', 'triggered', 'response']:
+ for attr in ["inputs", "states", "triggered", "response"]:
with pytest.raises(MissingCallbackContextException):
getattr(callback_context, attr)
+
+
+def test_inin023_wrong_callback_id():
+ app = Dash(__name__)
+ app.layout = html.Div(
+ [
+ html.Div(
+ [html.Div(id="inner-div"), dcc.Input(id="inner-input")], id="outer-div"
+ ),
+ dcc.Input(id="outer-input"),
+ ],
+ id="main",
+ )
+
+ ids = ["main", "inner-div", "inner-input", "outer-div", "outer-input"]
+
+ with pytest.raises(NonExistentIdException) as err:
+
+ @app.callback(Output("nuh-uh", "children"), [Input("inner-input", "value")])
+ def f(a):
+ return a
+
+ assert '"nuh-uh"' in err.value.args[0]
+ for component_id in ids:
+ assert component_id in err.value.args[0]
+
+ with pytest.raises(NonExistentIdException) as err:
+
+ @app.callback(Output("inner-div", "children"), [Input("yeah-no", "value")])
+ def g(a):
+ return a
+
+ assert '"yeah-no"' in err.value.args[0]
+ for component_id in ids:
+ assert component_id in err.value.args[0]
+
+ with pytest.raises(NonExistentIdException) as err:
+
+ @app.callback(
+ [Output("inner-div", "children"), Output("nope", "children")],
+ [Input("inner-input", "value")],
+ )
+ def g2(a):
+ return [a, a]
+
+ # the right way
+ @app.callback(Output("inner-div", "children"), [Input("inner-input", "value")])
+ def h(a):
+ return a