diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e36bb5fb8..4ab9ca977b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Fix missing indentation for generated metadata.json [#600](https://github.com/plotly/dash/issues/600) - Fix missing component prop docstring error [#598](https://github.com/plotly/dash/issues/598) - Moved `__repr__` to base component instead of being generated. [#492](https://github.com/plotly/dash/pull/492) +- Raise exception when same input & output are used in a callback [#605](https://github.com/plotly/dash/pull/605) ## Added - Added components libraries js/css distribution to hot reload watch. [#603](https://github.com/plotly/dash/pull/603) diff --git a/dash/dash.py b/dash/dash.py index 246e5086e4..42dc2bb3d9 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -638,6 +638,12 @@ def _validate_callback(self, output, inputs, state): # pylint: disable=too-many-branches layout = self._cached_layout or self._layout_value() + for i in inputs: + if output == i: + raise exceptions.SameInputOutputException( + 'Same output and input: {}'.format(output) + ) + if (layout is None and not self.config.first('suppress_callback_exceptions', 'supress_callback_exceptions')): diff --git a/dash/dependencies.py b/dash/dependencies.py index f8ef127dcc..3f946f1ebf 100644 --- a/dash/dependencies.py +++ b/dash/dependencies.py @@ -1,19 +1,34 @@ -# pylint: disable=too-few-public-methods -class Output: +class DashDependency: def __init__(self, component_id, component_property): self.component_id = component_id self.component_property = component_property + def __str__(self): + return '{}.{}'.format( + self.component_id, + self.component_property + ) + + def __repr__(self): + return '<{} `{}`>'.format(self.__class__.__name__, self) + + def __eq__(self, other): + return isinstance(other, DashDependency) and str(self) == str(other) + + def __hash__(self): + return hash(str(self)) + # pylint: disable=too-few-public-methods -class Input: - def __init__(self, component_id, component_property): - self.component_id = component_id - self.component_property = component_property +class Output(DashDependency): + """Output of a callback.""" # pylint: disable=too-few-public-methods -class State: - def __init__(self, component_id, component_property): - self.component_id = component_id - self.component_property = component_property +class Input(DashDependency): + """Input of callback trigger an update when it is updated.""" + + +# pylint: disable=too-few-public-methods +class State(DashDependency): + """Use the value of a state in a callback but don't trigger updates.""" diff --git a/dash/exceptions.py b/dash/exceptions.py index 8ad30d52c6..2b98bbbc18 100644 --- a/dash/exceptions.py +++ b/dash/exceptions.py @@ -76,3 +76,7 @@ class DependencyException(DashException): class ResourceException(DashException): pass + + +class SameInputOutputException(CallbackException): + pass diff --git a/tests/IntegrationTests.py b/tests/IntegrationTests.py index 8f22b6282f..3ef54de174 100644 --- a/tests/IntegrationTests.py +++ b/tests/IntegrationTests.py @@ -56,9 +56,10 @@ def setUp(s): pass def tearDown(s): - time.sleep(2) - s.server_process.terminate() - time.sleep(2) + if hasattr(s, 'server_process'): + time.sleep(2) + s.server_process.terminate() + time.sleep(2) def startServer(s, dash): def run(): diff --git a/tests/test_integration.py b/tests/test_integration.py index 19bf13bf60..3624795941 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -12,7 +12,7 @@ import dash from dash.dependencies import Input, Output -from dash.exceptions import PreventUpdate +from dash.exceptions import PreventUpdate, CallbackException from .IntegrationTests import IntegrationTests from .utils import assert_clean_console, invincible, wait_for @@ -553,3 +553,21 @@ def update_output(value): time.sleep(1) self.wait_for_element_by_css_selector('#inserted-input') + + def test_output_input_invalid_callback(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + html.Div('child', id='input-output'), + html.Div(id='out') + ]) + + with self.assertRaises(CallbackException) as context: + @app.callback(Output('input-output', 'children'), + [Input('input-output', 'children')]) + def failure(children): + pass + + self.assertEqual( + 'Same output and input: input-output.children', + context.exception.args[0] + )