From 9a98038e464c0d3f8c979d2e8796d0f661c507dc Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 13 Jul 2019 09:45:57 -0400 Subject: [PATCH 1/6] fix #791: don't try to use Component.keys() --- dash/dash.py | 10 +++--- tests/integration/test_integration.py | 49 +++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index deb28d76fc..51bdbf3611 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -849,6 +849,9 @@ 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): + all_ids = [k for k in layout] + if layout_id: + all_ids.append(layout_id) raise exceptions.NonExistentIdException(''' Attempting to assign a callback to the component with the id "{0}" but no @@ -860,12 +863,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).replace(' ', '')) component = ( layout if layout_id == arg_id else layout[arg_id] diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index b17825fa7b..0e00633df1 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 @@ -20,7 +18,7 @@ from dash.exceptions import ( PreventUpdate, DuplicateCallbackOutput, CallbackException, MissingCallbackContextException, InvalidCallbackReturnValue, - IncorrectTypeException + IncorrectTypeException, NonExistentIdException ) from dash.testing.wait import until @@ -898,3 +896,48 @@ def test_inin022_no_callback_context(): 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] + + # the right way + @app.callback( + Output('inner-div', 'children'), + [Input('inner-input', 'value')] + ) + def h(a): + return a From 740f8ce38e0143b062afc9eca8f613393f5c7e9f Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 13 Jul 2019 09:52:50 -0400 Subject: [PATCH 2/6] convert "replace" to "dedent" in multiline error messages --- dash/dash.py | 54 +++++++++++++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 51bdbf3611..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'), @@ -852,7 +853,7 @@ def _validate_callback(self, output, inputs, state): all_ids = [k for k in layout] if layout_id: all_ids.append(layout_id) - raise exceptions.NonExistentIdException(''' + 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 @@ -863,7 +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, all_ids).replace(' ', '')) + ''').format(arg_id, all_ids)) component = ( layout if layout_id == arg_id else layout[arg_id] @@ -873,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 @@ -951,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 @@ -982,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. @@ -994,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" + @@ -1012,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 ( @@ -1226,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. @@ -1234,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 From aa4eac52b332c55ad91be91dc4812f0a2a726739 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 13 Jul 2019 10:03:59 -0400 Subject: [PATCH 3/6] changelog for callback error reporting fix --- dash/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) 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 From 1cb18fab89244df736ebb236f7b30019f180432a Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 13 Jul 2019 13:53:38 -0400 Subject: [PATCH 4/6] add a 3x retry to browser starting --- dash/testing/browser.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) 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 = ( From 782af07aa2394805a569c4bd625f22aefa8b671d Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 15 Jul 2019 07:50:49 -0400 Subject: [PATCH 5/6] black test_integration --- tests/integration/test_integration.py | 786 +++++++++++++------------- 1 file changed, 407 insertions(+), 379 deletions(-) diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 0e00633df1..0299c34d99 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -16,87 +16,98 @@ from dash.dependencies import Input, Output, State from dash.exceptions import ( - PreventUpdate, DuplicateCallbackOutput, CallbackException, - MissingCallbackContextException, InvalidCallbackReturnValue, - IncorrectTypeException, NonExistentIdException + 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() @@ -107,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: @@ -128,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): @@ -169,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 = """ @@ -301,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 = """ @@ -334,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: @@ -393,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] ) @@ -461,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 = """ @@ -599,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 """ @@ -671,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() @@ -738,183 +772,182 @@ 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') + 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'] + 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')] - ) + + @app.callback(Output("nuh-uh", "children"), [Input("inner-input", "value")]) def f(a): return a @@ -923,10 +956,8 @@ def f(a): assert component_id in err.value.args[0] with pytest.raises(NonExistentIdException) as err: - @app.callback( - Output('inner-div', 'children'), - [Input('yeah-no', 'value')] - ) + + @app.callback(Output("inner-div", "children"), [Input("yeah-no", "value")]) def g(a): return a @@ -935,9 +966,6 @@ def g(a): assert component_id in err.value.args[0] # the right way - @app.callback( - Output('inner-div', 'children'), - [Input('inner-input', 'value')] - ) + @app.callback(Output("inner-div", "children"), [Input("inner-input", "value")]) def h(a): return a From fbec9dcd866a847e6299d3130bbf8d8cdfce8619 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 15 Jul 2019 07:53:59 -0400 Subject: [PATCH 6/6] test nonexistent id with multi-output --- tests/integration/test_integration.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 0299c34d99..57bc9eb039 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -965,6 +965,15 @@ def g(a): 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):