Skip to content

Commit ad1017e

Browse files
delsimMark Gibbs
andauthored
Add construction of callback context variables (#254)
* 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>
1 parent 844fe40 commit ad1017e

File tree

5 files changed

+93
-6
lines changed

5 files changed

+93
-6
lines changed

demo/demo/plotly_apps.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ def callback_show_timeseries(internal_state_string, state_uid, **kwargs):
342342
dash.dependencies.Input('dropdown-color', 'value'),
343343
])
344344
def multiple_callbacks_one(button_clicks, color_choice):
345-
return ("Output 1: %s %s" % (button_clicks, color_choice),
345+
return ("Output 1: %s %s %s" % (button_clicks, color_choice, dash.callback_context.triggered),
346346
"Output 2: %s %s" % (color_choice, button_clicks),
347347
"Output 3: %s %s" % (button_clicks, color_choice),
348348
)
@@ -373,7 +373,7 @@ def multiple_callbacks_one(button_clicks, color_choice):
373373
def multiple_callbacks_two(button_clicks, color_choice, **kwargs):
374374
if color_choice == 'green':
375375
raise PreventUpdate
376-
return ["Output 1: %s %s" % (button_clicks, color_choice),
377-
"Output 2: %s %s" % (button_clicks, color_choice),
376+
return ["Output 1: %s %s %s" % (button_clicks, color_choice, dash.callback_context.triggered),
377+
"Output 2: %s %s %s" % (button_clicks, color_choice, kwargs['callback_context'].triggered),
378378
"Output 3: %s %s [%s]" % (button_clicks, color_choice, kwargs)
379379
]

dev_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pytz
1919
redis
2020
sphinx
2121
sphinx-autobuild
22+
sphinx_rtd_theme
2223
twine
2324
whitenoise
2425

django_plotly_dash/dash_wrapper.py

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
import json
2929
import inspect
3030

31+
import dash
3132
from dash import Dash
32-
from dash._utils import split_callback_id
33+
from dash._utils import split_callback_id, inputs_to_dict
34+
3335
from flask import Flask
3436

3537
from django.urls import reverse
@@ -44,6 +46,54 @@
4446
from .util import serve_locally as serve_locally_setting
4547
from .util import stateless_app_lookup_hook
4648

49+
try:
50+
from dataclasses import dataclass
51+
from typing import Dict, List
52+
53+
@dataclass(frozen-True)
54+
class CallbackContext:
55+
inputs_list : List
56+
inputs: Dict
57+
states_list: List
58+
states: Dict
59+
outputs_list: List
60+
outputs: Dict
61+
triggered: List
62+
63+
except:
64+
# Not got python 3.7 or dataclasses yet
65+
class CallbackContext:
66+
def __init__(self, **kwargs):
67+
self._args = kwargs
68+
69+
@property
70+
def inputs_list(self):
71+
return self._args['inputs_list']
72+
73+
@property
74+
def inputs(self):
75+
return self._args['inputs']
76+
77+
@property
78+
def states_list(self):
79+
return self._args['states_list']
80+
81+
@property
82+
def states(self):
83+
return self._args['states']
84+
85+
@property
86+
def outputs(self):
87+
return self._args['outputs']
88+
89+
@property
90+
def outputs_list(self):
91+
return self._args['outputs_list']
92+
@property
93+
def triggered(self):
94+
return self._args['triggered']
95+
96+
4797
uid_counter = 0
4898

4999
usable_apps = {}
@@ -89,6 +139,7 @@ def get_local_stateless_by_name(name):
89139

90140
return sa
91141

142+
92143
class Holder:
93144
'Helper class for holding configuration options'
94145
def __init__(self):
@@ -100,6 +151,7 @@ def append_script(self, script):
100151
'Add extra script file name to component package'
101152
self.items.append(script)
102153

154+
103155
class DjangoDash:
104156
'''
105157
Wrapper class that provides Dash functionality in a form that can be served by Django
@@ -511,8 +563,31 @@ def dispatch(self):
511563
def dispatch_with_args(self, body, argMap):
512564
'Perform callback dispatching, with enhanced arguments and recording of response'
513565
inputs = body.get('inputs', [])
514-
state = body.get('state', [])
566+
input_values = inputs_to_dict(inputs)
567+
states = body.get('state', [])
515568
output = body['output']
569+
outputs_list = body.get('outputs') or split_callback_id(output)
570+
changed_props = body.get('changedPropIds', [])
571+
triggered_inputs = [{"prop_id": x, "value": input_values.get(x)} for x in changed_props]
572+
573+
callback_context_info = {
574+
'inputs_list': inputs,
575+
'inputs': input_values,
576+
'states_list': states,
577+
'states': inputs_to_dict(states),
578+
'outputs_list': outputs_list,
579+
'outputs': outputs_list,
580+
'triggered': triggered_inputs,
581+
}
582+
583+
callback_context = CallbackContext(**callback_context_info)
584+
585+
# Overload dash global variable
586+
dash.callback_context = callback_context
587+
588+
# Add context to arg map, if extended callbacks in use
589+
if len(argMap) > 0:
590+
argMap['callback_context'] = callback_context
516591

517592
outputs = []
518593
try:

django_plotly_dash/tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def test_injection_updating_multiple_callbacks(client):
198198
resp_detail = resp['response']
199199
assert 'output-two' in resp_detail
200200
assert 'children' in resp_detail['output-two']
201-
assert resp_detail['output-two']['children'] == "Output 2: 10 purple-ish yellow with a hint of greeny orange"
201+
assert resp_detail['output-two']['children'] == "Output 2: 10 purple-ish yellow with a hint of greeny orange []"
202202

203203

204204
@pytest.mark.django_db

docs/extended_callbacks.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,27 @@ For example, the ``plotly_apps.py`` example contains this dash application:
3939
4040
The additional arguments, which are reported as the ``kwargs`` content in this example, include
4141

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

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

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

5364
.. _using_session_state:
5465
Using session state

0 commit comments

Comments
 (0)