Repository: plotly/dash

Issue:
[BUG] error reporting callback definition errors
From https://community.plot.ly/t/multiple-callback-error/24981

Probably came from removing the `MutableMapping` mixin https://github.com/plotly/dash/pull/753

This app has bad IDs in the second callback:
```py
import dash
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output

series = [str(i) for i in range(20)]

all_options = {
    'group_1': series[1:5],
    'group_2': series[6:10],
    'group_3': series[11:-1],
}

app = dash.Dash()

app.layout = html.Div(children=[
    html.P([
        html.Label('Choose a group:'),
        dcc.Dropdown(
            id='groups_dropdown',
            options=[{'label': k, 'value': k} for k in all_options.keys()],
            value='group_1'
        )
    ], style=dict(width='400px')),

    html.P([
        html.Label('Choose a series:'),
        dcc.Dropdown(id='series_dropdown')
    ], style=dict(width='400px')),
])


@app.callback(Output('series_dropdown', 'options'),
              [Input('groups_dropdown', 'value')])
def set_series_options(selected_group):
    return [{'label': i, 'value': i} for i in all_options[selected_group]]


@app.callback(Output('series-dropdown', 'value'),
              [Input('series-dropdown', 'options')])
def set_series_value(available_options):
    return available_options[0]['value']


if __name__ == '__main__':
    app.run_server(debug=True)
```

But the error is broken:
```
Traceback (most recent call last):
  File "f.py", line 40, in <module>
    [Input('series-dropdown', 'options')])
  File "/Users/alex/plotly/dash/dash/dash.py", line 1156, in callback
    self._validate_callback(output, inputs, state)
  File "/Users/alex/plotly/dash/dash/dash.py", line 865, in _validate_callback
    list(layout.keys()) + (
AttributeError: 'Div' object has no attribute 'keys'
```


Discussion:
Hi,
I am having the same issue. These are the libraries versions if it can help:
```
dash
dash-html-components==0.13.4
dash-core-components==0.42.1
dash-daq
dash-table==3.1.11
```
Does this depend only on the new version of dash? Or maybe the other components are related?
@shoegazerstella if the issue is what I think it is, it's specific to dash 1.0.0 via #753 - I expect we'll be able to fix this for the next release.
Hi, I'm running into the same problem. I'd like to follow up if this has fixed yet? Thank you.

Resolving patch:
diff --git a/dash/dash.py b/dash/dash.py
--- 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
 


Test patch:
diff --git a/dash/testing/browser.py b/dash/testing/browser.py
--- 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
--- 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(
-        '<div ' + ' '.join(
-            '{}="{!s}"'.format(k, v) for k, v in attrs.items() if v is not None
-        ) + '></div>',
-        'lxml'
+        "<div "
+        + " ".join('{}="{!s}"'.format(k, v) for k, v in attrs.items() if v is not None)
+        + "></div>",
+        "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(
+                """
             <h1>No Props Component</h1>
-        ''')
-    ])
+        """
+            )
+        ]
+    )
 
     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 = """
     <!DOCTYPE html>
     <html>
         <head>
@@ -303,24 +318,24 @@ def test_inin008_index_customization(dash_duo):
             </script>
         </body>
     </html>
-    '''
+    """
 
-    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 = """
         <!DOCTYPE html>
         <html>
             <head>
@@ -336,57 +351,58 @@ def will_raise():
                 </footer>
             </body>
         </html>
-        '''
+        """
 
     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 = """
     <!DOCTYPE html>
     <html>
         <head>
@@ -601,40 +629,44 @@ def test_inin014_with_custom_renderer(dash_duo):
             <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-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 = """
         <script id="_dash-renderer" type="application/javascript">
             console.log('firing up a custom renderer!')
             const renderer = new DashRenderer({
@@ -652,12 +684,11 @@ def test_inin015_with_custom_renderer_interpolated(dash_duo):
                 }
             })
         </script>
-    '''
+    """
 
     class CustomDash(Dash):
-
         def interpolate_index(self, **kwargs):
-            return '''
+            return """
             <!DOCTYPE html>
             <html>
                 <head>
@@ -673,66 +704,67 @@ def interpolate_index(self, **kwargs):
                     <div id="custom-footer">My custom footer</div>
                 </body>
             </html>
-            '''.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