Skip to content

Commit eae8914

Browse files
authored
Merge pull request #1718 from plotly/dash-dot-callback-2
`dash.callback`
2 parents 5c2f05e + b572091 commit eae8914

File tree

5 files changed

+367
-164
lines changed

5 files changed

+367
-164
lines changed

dash/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@
2020
from . import dash_table # noqa: F401,E402
2121
from .version import __version__ # noqa: F401,E402
2222
from ._callback_context import callback_context # noqa: F401,E402
23+
from ._callback import callback, clientside_callback # noqa: F401,E402

dash/_callback.py

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import collections
2+
from functools import wraps
3+
4+
from .dependencies import (
5+
handle_callback_args,
6+
handle_grouped_callback_args,
7+
Output,
8+
)
9+
from .exceptions import PreventUpdate
10+
11+
from ._grouping import (
12+
flatten_grouping,
13+
make_grouping_by_index,
14+
grouping_len,
15+
)
16+
from ._utils import (
17+
create_callback_id,
18+
stringify_id,
19+
to_json,
20+
)
21+
22+
from . import _validate
23+
24+
25+
class NoUpdate(object):
26+
# pylint: disable=too-few-public-methods
27+
pass
28+
29+
30+
GLOBAL_CALLBACK_LIST = []
31+
GLOBAL_CALLBACK_MAP = {}
32+
GLOBAL_INLINE_SCRIPTS = []
33+
34+
35+
def callback(*_args, **_kwargs):
36+
"""
37+
Normally used as a decorator, `@dash.callback` provides a server-side
38+
callback relating the values of one or more `Output` items to one or
39+
more `Input` items which will trigger the callback when they change,
40+
and optionally `State` items which provide additional information but
41+
do not trigger the callback directly.
42+
43+
`@dash.callback` is an alternative to `@app.callback` (where `app = dash.Dash()`)
44+
introduced in Dash 2.0.
45+
It allows you to register callbacks without defining or importing the `app`
46+
object. The call signature is identical and it can be used instead of `app.callback`
47+
in all cases.
48+
49+
The last, optional argument `prevent_initial_call` causes the callback
50+
not to fire when its outputs are first added to the page. Defaults to
51+
`False` and unlike `app.callback` is not configurable at the app level.
52+
"""
53+
return register_callback(
54+
GLOBAL_CALLBACK_LIST,
55+
GLOBAL_CALLBACK_MAP,
56+
False,
57+
*_args,
58+
**_kwargs,
59+
)
60+
61+
62+
def clientside_callback(clientside_function, *args, **kwargs):
63+
return register_clientside_callback(
64+
GLOBAL_CALLBACK_LIST,
65+
GLOBAL_CALLBACK_MAP,
66+
False,
67+
GLOBAL_INLINE_SCRIPTS,
68+
clientside_function,
69+
*args,
70+
**kwargs,
71+
)
72+
73+
74+
def insert_callback(
75+
callback_list,
76+
callback_map,
77+
config_prevent_initial_callbacks,
78+
output,
79+
outputs_indices,
80+
inputs,
81+
state,
82+
inputs_state_indices,
83+
prevent_initial_call,
84+
):
85+
if prevent_initial_call is None:
86+
prevent_initial_call = config_prevent_initial_callbacks
87+
88+
callback_id = create_callback_id(output)
89+
callback_spec = {
90+
"output": callback_id,
91+
"inputs": [c.to_dict() for c in inputs],
92+
"state": [c.to_dict() for c in state],
93+
"clientside_function": None,
94+
"prevent_initial_call": prevent_initial_call,
95+
}
96+
callback_map[callback_id] = {
97+
"inputs": callback_spec["inputs"],
98+
"state": callback_spec["state"],
99+
"outputs_indices": outputs_indices,
100+
"inputs_state_indices": inputs_state_indices,
101+
}
102+
callback_list.append(callback_spec)
103+
104+
return callback_id
105+
106+
107+
def register_callback(
108+
callback_list, callback_map, config_prevent_initial_callbacks, *_args, **_kwargs
109+
):
110+
(
111+
output,
112+
flat_inputs,
113+
flat_state,
114+
inputs_state_indices,
115+
prevent_initial_call,
116+
) = handle_grouped_callback_args(_args, _kwargs)
117+
if isinstance(output, Output):
118+
# Insert callback with scalar (non-multi) Output
119+
insert_output = output
120+
multi = False
121+
else:
122+
# Insert callback as multi Output
123+
insert_output = flatten_grouping(output)
124+
multi = True
125+
126+
output_indices = make_grouping_by_index(output, list(range(grouping_len(output))))
127+
callback_id = insert_callback(
128+
callback_list,
129+
callback_map,
130+
config_prevent_initial_callbacks,
131+
insert_output,
132+
output_indices,
133+
flat_inputs,
134+
flat_state,
135+
inputs_state_indices,
136+
prevent_initial_call,
137+
)
138+
139+
# pylint: disable=too-many-locals
140+
def wrap_func(func):
141+
@wraps(func)
142+
def add_context(*args, **kwargs):
143+
output_spec = kwargs.pop("outputs_list")
144+
_validate.validate_output_spec(insert_output, output_spec, Output)
145+
146+
func_args, func_kwargs = _validate.validate_and_group_input_args(
147+
args, inputs_state_indices
148+
)
149+
150+
# don't touch the comment on the next line - used by debugger
151+
output_value = func(*func_args, **func_kwargs) # %% callback invoked %%
152+
153+
if isinstance(output_value, NoUpdate):
154+
raise PreventUpdate
155+
156+
if not multi:
157+
output_value, output_spec = [output_value], [output_spec]
158+
flat_output_values = output_value
159+
else:
160+
if isinstance(output_value, (list, tuple)):
161+
# For multi-output, allow top-level collection to be
162+
# list or tuple
163+
output_value = list(output_value)
164+
165+
# Flatten grouping and validate grouping structure
166+
flat_output_values = flatten_grouping(output_value, output)
167+
168+
_validate.validate_multi_return(
169+
output_spec, flat_output_values, callback_id
170+
)
171+
172+
component_ids = collections.defaultdict(dict)
173+
has_update = False
174+
for val, spec in zip(flat_output_values, output_spec):
175+
if isinstance(val, NoUpdate):
176+
continue
177+
for vali, speci in (
178+
zip(val, spec) if isinstance(spec, list) else [[val, spec]]
179+
):
180+
if not isinstance(vali, NoUpdate):
181+
has_update = True
182+
id_str = stringify_id(speci["id"])
183+
component_ids[id_str][speci["property"]] = vali
184+
185+
if not has_update:
186+
raise PreventUpdate
187+
188+
response = {"response": component_ids, "multi": True}
189+
190+
try:
191+
jsonResponse = to_json(response)
192+
except TypeError:
193+
_validate.fail_callback_output(output_value, output)
194+
195+
return jsonResponse
196+
197+
callback_map[callback_id]["callback"] = add_context
198+
199+
return add_context
200+
201+
return wrap_func
202+
203+
204+
_inline_clientside_template = """
205+
var clientside = window.dash_clientside = window.dash_clientside || {{}};
206+
var ns = clientside["{namespace}"] = clientside["{namespace}"] || {{}};
207+
ns["{function_name}"] = {clientside_function};
208+
"""
209+
210+
211+
def register_clientside_callback(
212+
callback_list,
213+
callback_map,
214+
config_prevent_initial_callbacks,
215+
inline_scripts,
216+
clientside_function,
217+
*args,
218+
**kwargs
219+
):
220+
output, inputs, state, prevent_initial_call = handle_callback_args(args, kwargs)
221+
insert_callback(
222+
callback_list,
223+
callback_map,
224+
config_prevent_initial_callbacks,
225+
output,
226+
None,
227+
inputs,
228+
state,
229+
None,
230+
prevent_initial_call,
231+
)
232+
233+
# If JS source is explicitly given, create a namespace and function
234+
# name, then inject the code.
235+
if isinstance(clientside_function, str):
236+
237+
out0 = output
238+
if isinstance(output, (list, tuple)):
239+
out0 = output[0]
240+
241+
namespace = "_dashprivate_{}".format(out0.component_id)
242+
function_name = "{}".format(out0.component_property)
243+
244+
inline_scripts.append(
245+
_inline_clientside_template.format(
246+
namespace=namespace.replace('"', '\\"'),
247+
function_name=function_name.replace('"', '\\"'),
248+
clientside_function=clientside_function,
249+
)
250+
)
251+
252+
# Callback is stored in an external asset.
253+
else:
254+
namespace = clientside_function.namespace
255+
function_name = clientside_function.function_name
256+
257+
callback_list[-1]["clientside_function"] = {
258+
"namespace": namespace,
259+
"function_name": function_name,
260+
}

0 commit comments

Comments
 (0)