Skip to content

Commit

Permalink
Add construction of callback context variables (#254)
Browse files Browse the repository at this point in the history
* Add construction of callback context variables

* Only add callback context for extended callbacks

* Add setting of dash global variable

* Add access and reporting of callback context in demo app

* Update test

* Add docs for callback_context

* Add request to documentation of extended callbacks

Co-authored-by: Mark Gibbs <mark@gibbs.consulting>
  • Loading branch information
delsim and Mark Gibbs committed Jun 20, 2020
1 parent 844fe40 commit ad1017e
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 6 deletions.
6 changes: 3 additions & 3 deletions demo/demo/plotly_apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ def callback_show_timeseries(internal_state_string, state_uid, **kwargs):
dash.dependencies.Input('dropdown-color', 'value'),
])
def multiple_callbacks_one(button_clicks, color_choice):
return ("Output 1: %s %s" % (button_clicks, color_choice),
return ("Output 1: %s %s %s" % (button_clicks, color_choice, dash.callback_context.triggered),
"Output 2: %s %s" % (color_choice, button_clicks),
"Output 3: %s %s" % (button_clicks, color_choice),
)
Expand Down Expand Up @@ -373,7 +373,7 @@ def multiple_callbacks_one(button_clicks, color_choice):
def multiple_callbacks_two(button_clicks, color_choice, **kwargs):
if color_choice == 'green':
raise PreventUpdate
return ["Output 1: %s %s" % (button_clicks, color_choice),
"Output 2: %s %s" % (button_clicks, color_choice),
return ["Output 1: %s %s %s" % (button_clicks, color_choice, dash.callback_context.triggered),
"Output 2: %s %s %s" % (button_clicks, color_choice, kwargs['callback_context'].triggered),
"Output 3: %s %s [%s]" % (button_clicks, color_choice, kwargs)
]
1 change: 1 addition & 0 deletions dev_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pytz
redis
sphinx
sphinx-autobuild
sphinx_rtd_theme
twine
whitenoise

79 changes: 77 additions & 2 deletions django_plotly_dash/dash_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@
import json
import inspect

import dash
from dash import Dash
from dash._utils import split_callback_id
from dash._utils import split_callback_id, inputs_to_dict

from flask import Flask

from django.urls import reverse
Expand All @@ -44,6 +46,54 @@
from .util import serve_locally as serve_locally_setting
from .util import stateless_app_lookup_hook

try:
from dataclasses import dataclass
from typing import Dict, List

@dataclass(frozen-True)
class CallbackContext:
inputs_list : List
inputs: Dict
states_list: List
states: Dict
outputs_list: List
outputs: Dict
triggered: List

except:
# Not got python 3.7 or dataclasses yet
class CallbackContext:
def __init__(self, **kwargs):
self._args = kwargs

@property
def inputs_list(self):
return self._args['inputs_list']

@property
def inputs(self):
return self._args['inputs']

@property
def states_list(self):
return self._args['states_list']

@property
def states(self):
return self._args['states']

@property
def outputs(self):
return self._args['outputs']

@property
def outputs_list(self):
return self._args['outputs_list']
@property
def triggered(self):
return self._args['triggered']


uid_counter = 0

usable_apps = {}
Expand Down Expand Up @@ -89,6 +139,7 @@ def get_local_stateless_by_name(name):

return sa


class Holder:
'Helper class for holding configuration options'
def __init__(self):
Expand All @@ -100,6 +151,7 @@ def append_script(self, script):
'Add extra script file name to component package'
self.items.append(script)


class DjangoDash:
'''
Wrapper class that provides Dash functionality in a form that can be served by Django
Expand Down Expand Up @@ -511,8 +563,31 @@ def dispatch(self):
def dispatch_with_args(self, body, argMap):
'Perform callback dispatching, with enhanced arguments and recording of response'
inputs = body.get('inputs', [])
state = body.get('state', [])
input_values = inputs_to_dict(inputs)
states = body.get('state', [])
output = body['output']
outputs_list = body.get('outputs') or split_callback_id(output)
changed_props = body.get('changedPropIds', [])
triggered_inputs = [{"prop_id": x, "value": input_values.get(x)} for x in changed_props]

callback_context_info = {
'inputs_list': inputs,
'inputs': input_values,
'states_list': states,
'states': inputs_to_dict(states),
'outputs_list': outputs_list,
'outputs': outputs_list,
'triggered': triggered_inputs,
}

callback_context = CallbackContext(**callback_context_info)

# Overload dash global variable
dash.callback_context = callback_context

# Add context to arg map, if extended callbacks in use
if len(argMap) > 0:
argMap['callback_context'] = callback_context

outputs = []
try:
Expand Down
2 changes: 1 addition & 1 deletion django_plotly_dash/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def test_injection_updating_multiple_callbacks(client):
resp_detail = resp['response']
assert 'output-two' in resp_detail
assert 'children' in resp_detail['output-two']
assert resp_detail['output-two']['children'] == "Output 2: 10 purple-ish yellow with a hint of greeny orange"
assert resp_detail['output-two']['children'] == "Output 2: 10 purple-ish yellow with a hint of greeny orange []"


@pytest.mark.django_db
Expand Down
11 changes: 11 additions & 0 deletions docs/extended_callbacks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,27 @@ For example, the ``plotly_apps.py`` example contains this dash application:
The additional arguments, which are reported as the ``kwargs`` content in this example, include

:callback_context: The ``Dash`` callback context. See the `documentation <https://dash.plotly.com/advanced-callbacks`_ on the content of
this variable. This variable is provided as an argument to the expanded callback as well as
the ``dash.callback_context`` global variable.
:dash_app: For stateful applications, the ``DashApp`` model instance
:dash_app_id: The application identifier. For stateless applications, this is the (slugified) name given to the ``DjangoDash`` constructor.
For stateful applications, it is the (slugified) unique identifier for the associated model instance.
:request: The Django request object.
:session_state: A dictionary of information, unique to this user session. Any changes made to its content during the
callback are persisted as part of the Django session framework.
:user: The Django User instance.

The ``DashApp`` model instance can also be configured to persist itself on any change. This is discussed
in the :ref:`models_and_state` section.

The ``callback_context`` argument is provided in addition to the ``dash.callback_context`` global variable. As a rule, the use of
global variables should generally be avoided. The context provided by ``django-plotly-dash`` is not the same as the one
provided by the underlying ``Dash`` library, although its property values are the same and code that uses the content of this
variable should work unchanged. The use of
this global variable in any asychronous or multithreaded application is not
supported, and the use of the callback argument is strongly recommended for all use cases.


.. _using_session_state:
Using session state
Expand Down

0 comments on commit ad1017e

Please sign in to comment.