Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Callback context #608

Merged
merged 8 commits into from
Feb 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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