Skip to content

Commit 669d034

Browse files
authored
Merge pull request #605 from plotly/fix-wrong-callback
Raise exception when same input & output are used in a callback
2 parents ac84f2e + 356ff07 commit 669d034

File tree

6 files changed

+59
-14
lines changed

6 files changed

+59
-14
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- Fix missing indentation for generated metadata.json [#600](https://github.com/plotly/dash/issues/600)
44
- Fix missing component prop docstring error [#598](https://github.com/plotly/dash/issues/598)
55
- Moved `__repr__` to base component instead of being generated. [#492](https://github.com/plotly/dash/pull/492)
6+
- Raise exception when same input & output are used in a callback [#605](https://github.com/plotly/dash/pull/605)
67

78
## Added
89
- Added components libraries js/css distribution to hot reload watch. [#603](https://github.com/plotly/dash/pull/603)

Diff for: dash/dash.py

+6
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,12 @@ def _validate_callback(self, output, inputs, state):
638638
# pylint: disable=too-many-branches
639639
layout = self._cached_layout or self._layout_value()
640640

641+
for i in inputs:
642+
if output == i:
643+
raise exceptions.SameInputOutputException(
644+
'Same output and input: {}'.format(output)
645+
)
646+
641647
if (layout is None and
642648
not self.config.first('suppress_callback_exceptions',
643649
'supress_callback_exceptions')):

Diff for: dash/dependencies.py

+25-10
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
1-
# pylint: disable=too-few-public-methods
2-
class Output:
1+
class DashDependency:
32
def __init__(self, component_id, component_property):
43
self.component_id = component_id
54
self.component_property = component_property
65

6+
def __str__(self):
7+
return '{}.{}'.format(
8+
self.component_id,
9+
self.component_property
10+
)
11+
12+
def __repr__(self):
13+
return '<{} `{}`>'.format(self.__class__.__name__, self)
14+
15+
def __eq__(self, other):
16+
return isinstance(other, DashDependency) and str(self) == str(other)
17+
18+
def __hash__(self):
19+
return hash(str(self))
20+
721

822
# pylint: disable=too-few-public-methods
9-
class Input:
10-
def __init__(self, component_id, component_property):
11-
self.component_id = component_id
12-
self.component_property = component_property
23+
class Output(DashDependency):
24+
"""Output of a callback."""
1325

1426

1527
# pylint: disable=too-few-public-methods
16-
class State:
17-
def __init__(self, component_id, component_property):
18-
self.component_id = component_id
19-
self.component_property = component_property
28+
class Input(DashDependency):
29+
"""Input of callback trigger an update when it is updated."""
30+
31+
32+
# pylint: disable=too-few-public-methods
33+
class State(DashDependency):
34+
"""Use the value of a state in a callback but don't trigger updates."""

Diff for: dash/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,7 @@ class DependencyException(DashException):
7676

7777
class ResourceException(DashException):
7878
pass
79+
80+
81+
class SameInputOutputException(CallbackException):
82+
pass

Diff for: tests/IntegrationTests.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ def setUp(s):
5656
pass
5757

5858
def tearDown(s):
59-
time.sleep(2)
60-
s.server_process.terminate()
61-
time.sleep(2)
59+
if hasattr(s, 'server_process'):
60+
time.sleep(2)
61+
s.server_process.terminate()
62+
time.sleep(2)
6263

6364
def startServer(s, dash):
6465
def run():

Diff for: tests/test_integration.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import dash
1313

1414
from dash.dependencies import Input, Output
15-
from dash.exceptions import PreventUpdate
15+
from dash.exceptions import PreventUpdate, CallbackException
1616
from .IntegrationTests import IntegrationTests
1717
from .utils import assert_clean_console, invincible, wait_for
1818

@@ -553,3 +553,21 @@ def update_output(value):
553553
time.sleep(1)
554554

555555
self.wait_for_element_by_css_selector('#inserted-input')
556+
557+
def test_output_input_invalid_callback(self):
558+
app = dash.Dash(__name__)
559+
app.layout = html.Div([
560+
html.Div('child', id='input-output'),
561+
html.Div(id='out')
562+
])
563+
564+
with self.assertRaises(CallbackException) as context:
565+
@app.callback(Output('input-output', 'children'),
566+
[Input('input-output', 'children')])
567+
def failure(children):
568+
pass
569+
570+
self.assertEqual(
571+
'Same output and input: input-output.children',
572+
context.exception.args[0]
573+
)

0 commit comments

Comments
 (0)