Skip to content

Commit b2a97b4

Browse files
committed
add no_update for partial or non-exception update rejection
1 parent fb0db30 commit b2a97b4

File tree

3 files changed

+65
-8
lines changed

3 files changed

+65
-8
lines changed

dash/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .dash import Dash # noqa: F401
1+
from .dash import Dash, no_update # noqa: F401
22
from . import dependencies # noqa: F401
33
from . import development # noqa: F401
44
from . import exceptions # noqa: F401

dash/dash.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@
7474
_re_renderer_scripts_id = re.compile(r'id="_dash-renderer')
7575

7676

77+
class _NoUpdate(object):
78+
# pylint: disable=too-few-public-methods
79+
pass
80+
81+
82+
# Singleton signal to not update an output, alternative to PreventUpdate
83+
no_update = _NoUpdate()
84+
85+
7786
# pylint: disable=too-many-instance-attributes
7887
# pylint: disable=too-many-arguments, too-many-locals
7988
class Dash(object):
@@ -990,15 +999,25 @@ def add_context(*args, **kwargs):
990999
)
9911000

9921001
component_ids = collections.defaultdict(dict)
1002+
has_update = False
9931003
for i, o in enumerate(output):
994-
component_ids[o.component_id][o.component_property] =\
995-
output_value[i]
1004+
val = output_value[i]
1005+
if val is not no_update:
1006+
has_update = True
1007+
o_id, o_prop = o.component_id, o.component_property
1008+
component_ids[o_id][o_prop] = val
1009+
1010+
if not has_update:
1011+
raise exceptions.PreventUpdate
9961012

9971013
response = {
9981014
'response': component_ids,
9991015
'multi': True
10001016
}
10011017
else:
1018+
if output_value is no_update:
1019+
raise exceptions.PreventUpdate
1020+
10021021
response = {
10031022
'response': {
10041023
'props': {

tests/test_integration.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import dash_html_components as html
1414
import dash_core_components as dcc
1515

16-
from dash import Dash, callback_context
16+
from dash import Dash, callback_context, no_update
1717

1818
from dash.dependencies import Input, Output, State
1919
from dash.exceptions import (
@@ -150,7 +150,10 @@ def update_text(data):
150150
assert_clean_console(self)
151151

152152
def test_aborted_callback(self):
153-
"""Raising PreventUpdate prevents update and triggering dependencies"""
153+
"""
154+
Raising PreventUpdate OR returning no_update
155+
prevents update and triggering dependencies
156+
"""
154157

155158
initial_input = 'initial input'
156159
initial_output = 'initial output'
@@ -168,6 +171,8 @@ def test_aborted_callback(self):
168171
@app.callback(Output('output1', 'children'), [Input('input', 'value')])
169172
def callback1(value):
170173
callback1_count.value += 1
174+
if callback1_count.value > 2:
175+
return no_update
171176
raise PreventUpdate("testing callback does not update")
172177
return value
173178

@@ -180,12 +185,12 @@ def callback2(value):
180185

181186
input_ = self.wait_for_element_by_id('input')
182187
input_.clear()
183-
input_.send_keys('x')
188+
input_.send_keys('xyz')
184189
output1 = self.wait_for_element_by_id('output1')
185190
output2 = self.wait_for_element_by_id('output2')
186191

187-
# callback1 runs twice (initial page load and through send_keys)
188-
self.assertEqual(callback1_count.value, 2)
192+
# callback1 runs 4x (initial page load and 3x through send_keys)
193+
self.assertEqual(callback1_count.value, 4)
189194

190195
# callback2 is never triggered, even on initial load
191196
self.assertEqual(callback2_count.value, 0)
@@ -656,6 +661,39 @@ def overlapping_multi_output(n_clicks):
656661

657662
self.assertGreater(int(output2.text), t)
658663

664+
def test_multi_output_no_update(self):
665+
app = Dash(__name__)
666+
app.scripts.config.serve_locally = True
667+
668+
app.layout = html.Div([
669+
html.Button('B', 'btn'),
670+
html.P('initial1', 'n1'),
671+
html.P('initial2', 'n2'),
672+
html.P('initial3', 'n3')
673+
])
674+
675+
@app.callback([Output('n1', 'children'),
676+
Output('n2', 'children'),
677+
Output('n3', 'children')],
678+
[Input('btn', 'n_clicks')])
679+
def show_clicks(n):
680+
# partial or complete cancelation of updates via no_update
681+
return [
682+
no_update if n > 4 else n,
683+
no_update if n > 2 else n,
684+
no_update
685+
]
686+
687+
self.startServer(app)
688+
689+
btn = self.wait_for_element_by_id('btn')
690+
for _ in range(10):
691+
btn.click()
692+
693+
self.wait_for_text_to_equal('#n1', '4')
694+
self.wait_for_text_to_equal('#n2', '2')
695+
self.wait_for_text_to_equal('#n3', 'initial3')
696+
659697
def test_with_custom_renderer(self):
660698
app = Dash(__name__)
661699

0 commit comments

Comments
 (0)