Skip to content

Commit 1249ffb

Browse files
authored
Merge pull request #608 from plotly/callback-context
Callback context
2 parents f44d606 + 0e90fc7 commit 1249ffb

File tree

8 files changed

+107
-3
lines changed

8 files changed

+107
-3
lines changed

.circleci/requirements/dev-requirements-py37.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ dash_core_components==0.43.1
22
dash_html_components==0.13.5
33
dash-flow-example==0.0.5
44
dash-dangerously-set-inner-html
5-
dash_renderer==0.18.0
5+
-e git://github.com/plotly/dash-renderer.git@master#egg=dash_renderer
66
percy
77
selenium
88
mock

.circleci/requirements/dev-requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ dash_core_components==0.43.1
22
dash_html_components==0.13.5
33
dash_flow_example==0.0.5
44
dash-dangerously-set-inner-html
5-
dash_renderer==0.18.0
5+
-e git://github.com/plotly/dash-renderer.git@master#egg=dash_renderer
66
percy
77
selenium
88
mock

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
## Added
99
- Added components libraries js/css distribution to hot reload watch. [#603](https://github.com/plotly/dash/pull/603)
10+
- Callback context [#608](https://github.com/plotly/dash/pull/608)
11+
- Know which inputs fired in a callback `dash.callback.triggered`
12+
- Input/State values by name `dash.callback.states.get('btn.n_clicks')`
1013

1114
## [0.37.0] - 2019-02-11
1215
## Fixed

dash/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
from . import exceptions # noqa: F401
55
from . import resources # noqa: F401
66
from .version import __version__ # noqa: F401
7+
from ._callback_context import CallbackContext as _CallbackContext
8+
9+
callback_context = _CallbackContext()

dash/_callback_context.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import functools
2+
import flask
3+
4+
from . import exceptions
5+
6+
7+
def has_context(func):
8+
@functools.wraps(func)
9+
def assert_context(*args, **kwargs):
10+
if not flask.has_request_context():
11+
raise exceptions.MissingCallbackContextException(
12+
'dash.callback.{} is only available from a callback!'.format(
13+
getattr(func, '__name__')
14+
)
15+
)
16+
return func(*args, **kwargs)
17+
return assert_context
18+
19+
20+
# pylint: disable=no-init
21+
class CallbackContext:
22+
@property
23+
@has_context
24+
def inputs(self):
25+
return getattr(flask.g, 'input_values', {})
26+
27+
@property
28+
@has_context
29+
def states(self):
30+
return getattr(flask.g, 'state_values', {})
31+
32+
@property
33+
@has_context
34+
def triggered(self):
35+
return getattr(flask.g, 'triggered_inputs', [])

dash/dash.py

+15
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,21 @@ def dispatch(self):
949949

950950
target_id = '{}.{}'.format(output['id'], output['property'])
951951
args = []
952+
953+
flask.g.input_values = input_values = {
954+
'{}.{}'.format(x['id'], x['property']): x.get('value')
955+
for x in inputs
956+
}
957+
flask.g.state_values = {
958+
'{}.{}'.format(x['id'], x['property']): x.get('value')
959+
for x in state
960+
}
961+
changed_props = body.get('changedPropIds')
962+
flask.g.triggered_inputs = [
963+
{'prop_id': x, 'value': input_values[x]}
964+
for x in changed_props
965+
] if changed_props else []
966+
952967
for component_registration in self.callback_map[target_id]['inputs']:
953968
args.append([
954969
c.get('value', None) for c in inputs if

dash/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,7 @@ class ResourceException(DashException):
8080

8181
class SameInputOutputException(CallbackException):
8282
pass
83+
84+
85+
class MissingCallbackContextException(CallbackException):
86+
pass

tests/test_integration.py

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

1414
from dash.dependencies import Input, Output
15-
from dash.exceptions import PreventUpdate, CallbackException
15+
from dash.exceptions import (
16+
PreventUpdate, CallbackException, MissingCallbackContextException
17+
)
1618
from .IntegrationTests import IntegrationTests
1719
from .utils import assert_clean_console, invincible, wait_for
1820

@@ -571,3 +573,45 @@ def failure(children):
571573
'Same output and input: input-output.children',
572574
context.exception.args[0]
573575
)
576+
577+
def test_callback_context(self):
578+
app = dash.Dash(__name__)
579+
580+
btns = ['btn-{}'.format(x) for x in range(1, 6)]
581+
582+
app.layout = html.Div([
583+
html.Div([
584+
html.Button(x, id=x) for x in btns
585+
]),
586+
html.Div(id='output'),
587+
])
588+
589+
@app.callback(Output('output', 'children'),
590+
[Input(x, 'n_clicks') for x in btns])
591+
def on_click(*args):
592+
if not dash.callback_context.triggered:
593+
raise PreventUpdate
594+
trigger = dash.callback_context.triggered[0]
595+
return 'Just clicked {} for the {} time!'.format(
596+
trigger['prop_id'].split('.')[0], trigger['value']
597+
)
598+
599+
self.startServer(app)
600+
601+
btn_elements = [
602+
self.wait_for_element_by_id(x) for x in btns
603+
]
604+
605+
for i in range(1, 5):
606+
for j, btn in enumerate(btns):
607+
btn_elements[j].click()
608+
self.wait_for_text_to_equal(
609+
'#output',
610+
'Just clicked {} for the {} time!'.format(
611+
btn, i
612+
)
613+
)
614+
615+
def test_no_callback_context(self):
616+
with self.assertRaises(MissingCallbackContextException):
617+
no_context = dash.callback_context.inputs

0 commit comments

Comments
 (0)