Skip to content

Commit

Permalink
Merge pull request #608 from plotly/callback-context
Browse files Browse the repository at this point in the history
Callback context
  • Loading branch information
T4rk1n authored Feb 21, 2019
2 parents f44d606 + 0e90fc7 commit 1249ffb
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .circleci/requirements/dev-requirements-py37.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ dash_core_components==0.43.1
dash_html_components==0.13.5
dash-flow-example==0.0.5
dash-dangerously-set-inner-html
dash_renderer==0.18.0
-e git://github.com/plotly/dash-renderer.git@master#egg=dash_renderer
percy
selenium
mock
Expand Down
2 changes: 1 addition & 1 deletion .circleci/requirements/dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ dash_core_components==0.43.1
dash_html_components==0.13.5
dash_flow_example==0.0.5
dash-dangerously-set-inner-html
dash_renderer==0.18.0
-e git://github.com/plotly/dash-renderer.git@master#egg=dash_renderer
percy
selenium
mock
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

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

## [0.37.0] - 2019-02-11
## Fixed
Expand Down
3 changes: 3 additions & 0 deletions dash/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
from . import exceptions # noqa: F401
from . import resources # noqa: F401
from .version import __version__ # noqa: F401
from ._callback_context import CallbackContext as _CallbackContext

callback_context = _CallbackContext()
35 changes: 35 additions & 0 deletions dash/_callback_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import functools
import flask

from . import exceptions


def has_context(func):
@functools.wraps(func)
def assert_context(*args, **kwargs):
if not flask.has_request_context():
raise exceptions.MissingCallbackContextException(
'dash.callback.{} is only available from a callback!'.format(
getattr(func, '__name__')
)
)
return func(*args, **kwargs)
return assert_context


# pylint: disable=no-init
class CallbackContext:
@property
@has_context
def inputs(self):
return getattr(flask.g, 'input_values', {})

@property
@has_context
def states(self):
return getattr(flask.g, 'state_values', {})

@property
@has_context
def triggered(self):
return getattr(flask.g, 'triggered_inputs', [])
15 changes: 15 additions & 0 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,21 @@ def dispatch(self):

target_id = '{}.{}'.format(output['id'], output['property'])
args = []

flask.g.input_values = input_values = {
'{}.{}'.format(x['id'], x['property']): x.get('value')
for x in inputs
}
flask.g.state_values = {
'{}.{}'.format(x['id'], x['property']): x.get('value')
for x in state
}
changed_props = body.get('changedPropIds')
flask.g.triggered_inputs = [
{'prop_id': x, 'value': input_values[x]}
for x in changed_props
] if changed_props else []

for component_registration in self.callback_map[target_id]['inputs']:
args.append([
c.get('value', None) for c in inputs if
Expand Down
4 changes: 4 additions & 0 deletions dash/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,7 @@ class ResourceException(DashException):

class SameInputOutputException(CallbackException):
pass


class MissingCallbackContextException(CallbackException):
pass
46 changes: 45 additions & 1 deletion tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import dash

from dash.dependencies import Input, Output
from dash.exceptions import PreventUpdate, CallbackException
from dash.exceptions import (
PreventUpdate, CallbackException, MissingCallbackContextException
)
from .IntegrationTests import IntegrationTests
from .utils import assert_clean_console, invincible, wait_for

Expand Down Expand Up @@ -571,3 +573,45 @@ def failure(children):
'Same output and input: input-output.children',
context.exception.args[0]
)

def test_callback_context(self):
app = dash.Dash(__name__)

btns = ['btn-{}'.format(x) for x in range(1, 6)]

app.layout = html.Div([
html.Div([
html.Button(x, id=x) for x in btns
]),
html.Div(id='output'),
])

@app.callback(Output('output', 'children'),
[Input(x, 'n_clicks') for x in btns])
def on_click(*args):
if not dash.callback_context.triggered:
raise PreventUpdate
trigger = dash.callback_context.triggered[0]
return 'Just clicked {} for the {} time!'.format(
trigger['prop_id'].split('.')[0], trigger['value']
)

self.startServer(app)

btn_elements = [
self.wait_for_element_by_id(x) for x in btns
]

for i in range(1, 5):
for j, btn in enumerate(btns):
btn_elements[j].click()
self.wait_for_text_to_equal(
'#output',
'Just clicked {} for the {} time!'.format(
btn, i
)
)

def test_no_callback_context(self):
with self.assertRaises(MissingCallbackContextException):
no_context = dash.callback_context.inputs

0 comments on commit 1249ffb

Please sign in to comment.