Skip to content

Commit 3488bbe

Browse files
committed
Support Arbitrary callbacks
- Allow no output callback - Add global set_props - Fix side update pattern ids
1 parent f7f8fb4 commit 3488bbe

File tree

13 files changed

+204
-65
lines changed

13 files changed

+204
-65
lines changed

.pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ max-public-methods=40
426426
max-returns=6
427427

428428
# Maximum number of statements in function / method body
429-
max-statements=50
429+
max-statements=75
430430

431431
# Minimum number of public methods for a class (see R0903).
432432
min-public-methods=2

dash/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from . import html # noqa: F401,E402
1919
from . import dash_table # noqa: F401,E402
2020
from .version import __version__ # noqa: F401,E402
21-
from ._callback_context import callback_context # noqa: F401,E402
21+
from ._callback_context import callback_context, set_props # noqa: F401,E402
2222
from ._callback import callback, clientside_callback # noqa: F401,E402
2323
from ._get_app import get_app # noqa: F401,E402
2424
from ._get_paths import ( # noqa: F401,E402

dash/_callback.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ def insert_callback(
226226
manager=None,
227227
running=None,
228228
dynamic_creator=False,
229+
no_output=False,
229230
):
230231
if prevent_initial_call is None:
231232
prevent_initial_call = config_prevent_initial_callbacks
@@ -234,7 +235,7 @@ def insert_callback(
234235
output, prevent_initial_call, config_prevent_initial_callbacks
235236
)
236237

237-
callback_id = create_callback_id(output, inputs)
238+
callback_id = create_callback_id(output, inputs, no_output)
238239
callback_spec = {
239240
"output": callback_id,
240241
"inputs": [c.to_dict() for c in inputs],
@@ -248,6 +249,7 @@ def insert_callback(
248249
"interval": long["interval"],
249250
},
250251
"dynamic_creator": dynamic_creator,
252+
"no_output": no_output,
251253
}
252254
if running:
253255
callback_spec["running"] = running
@@ -262,6 +264,7 @@ def insert_callback(
262264
"raw_inputs": inputs,
263265
"manager": manager,
264266
"allow_dynamic_callbacks": dynamic_creator,
267+
"no_output": no_output,
265268
}
266269
callback_list.append(callback_spec)
267270

@@ -283,10 +286,12 @@ def register_callback( # pylint: disable=R0914
283286
# Insert callback with scalar (non-multi) Output
284287
insert_output = output
285288
multi = False
289+
no_output = False
286290
else:
287291
# Insert callback as multi Output
288292
insert_output = flatten_grouping(output)
289293
multi = True
294+
no_output = len(output) == 0
290295

291296
long = _kwargs.get("long")
292297
manager = _kwargs.get("manager")
@@ -315,6 +320,7 @@ def register_callback( # pylint: disable=R0914
315320
manager=manager,
316321
dynamic_creator=allow_dynamic_callbacks,
317322
running=running,
323+
no_output=no_output,
318324
)
319325

320326
# pylint: disable=too-many-locals
@@ -333,7 +339,8 @@ def add_context(*args, **kwargs):
333339
app_callback_manager = kwargs.pop("long_callback_manager", None)
334340
callback_ctx = kwargs.pop("callback_context", {})
335341
callback_manager = long and long.get("manager", app_callback_manager)
336-
_validate.validate_output_spec(insert_output, output_spec, Output)
342+
if not no_output:
343+
_validate.validate_output_spec(insert_output, output_spec, Output)
337344

338345
context_value.set(callback_ctx)
339346

@@ -443,6 +450,9 @@ def add_context(*args, **kwargs):
443450
NoUpdate() if NoUpdate.is_no_update(r) else r
444451
for r in output_value
445452
]
453+
updated_props = callback_manager.get_updated_props(cache_key)
454+
if len(updated_props) > 0:
455+
response["sideUpdate"] = updated_props
446456

447457
if output_value is callback_manager.UNDEFINED:
448458
return to_json(response)
@@ -452,7 +462,10 @@ def add_context(*args, **kwargs):
452462
if NoUpdate.is_no_update(output_value):
453463
raise PreventUpdate
454464

455-
if not multi:
465+
if no_output:
466+
output_value = []
467+
flat_output_values = []
468+
elif not multi:
456469
output_value, output_spec = [output_value], [output_spec]
457470
flat_output_values = output_value
458471
else:
@@ -464,23 +477,30 @@ def add_context(*args, **kwargs):
464477
# Flatten grouping and validate grouping structure
465478
flat_output_values = flatten_grouping(output_value, output)
466479

467-
_validate.validate_multi_return(
468-
output_spec, flat_output_values, callback_id
469-
)
470-
471480
component_ids = collections.defaultdict(dict)
472481
has_update = False
473-
for val, spec in zip(flat_output_values, output_spec):
474-
if isinstance(val, NoUpdate):
475-
continue
476-
for vali, speci in (
477-
zip(val, spec) if isinstance(spec, list) else [[val, spec]]
478-
):
479-
if not isinstance(vali, NoUpdate):
480-
has_update = True
481-
id_str = stringify_id(speci["id"])
482-
prop = clean_property_name(speci["property"])
483-
component_ids[id_str][prop] = vali
482+
if not no_output:
483+
_validate.validate_multi_return(
484+
output_spec, flat_output_values, callback_id
485+
)
486+
487+
for val, spec in zip(flat_output_values, output_spec):
488+
if isinstance(val, NoUpdate):
489+
continue
490+
for vali, speci in (
491+
zip(val, spec) if isinstance(spec, list) else [[val, spec]]
492+
):
493+
if not isinstance(vali, NoUpdate):
494+
has_update = True
495+
id_str = stringify_id(speci["id"])
496+
prop = clean_property_name(speci["property"])
497+
component_ids[id_str][prop] = vali
498+
499+
if not long:
500+
side_update = dict(callback_ctx.updated_props)
501+
if len(side_update) > 0:
502+
has_update = True
503+
response["sideUpdate"] = side_update
484504

485505
if not has_update:
486506
raise PreventUpdate

dash/_callback_context.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import warnings
33
import json
44
import contextvars
5+
import typing
56

67
import flask
78

@@ -247,5 +248,20 @@ def using_outputs_grouping(self):
247248
def timing_information(self):
248249
return getattr(flask.g, "timing_information", {})
249250

251+
@has_context
252+
def set_props(self, component_id: typing.Union[str, dict], props: dict):
253+
ctx_value = _get_context_value()
254+
if isinstance(component_id, dict):
255+
ctx_value.updated_props[json.dumps(component_id)] = props
256+
else:
257+
ctx_value.updated_props[component_id] = props
258+
250259

251260
callback_context = CallbackContext()
261+
262+
263+
def set_props(component_id: typing.Union[str, dict], props: dict):
264+
"""
265+
Set the props for a component not included in the callback outputs.
266+
"""
267+
callback_context.set_props(component_id, props)

dash/_utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def first(self, *names):
131131
return next(iter(self), {})
132132

133133

134-
def create_callback_id(output, inputs):
134+
def create_callback_id(output, inputs, no_output=False):
135135
# A single dot within a dict id key or value is OK
136136
# but in case of multiple dots together escape each dot
137137
# with `\` so we don't mistake it for multi-outputs
@@ -149,6 +149,12 @@ def _concat(x):
149149
_id += f"@{hashed_inputs}"
150150
return _id
151151

152+
if no_output:
153+
# No output will hash the inputs.
154+
return hashlib.sha256(
155+
".".join(str(x) for x in inputs).encode("utf-8")
156+
).hexdigest()
157+
152158
if isinstance(output, (list, tuple)):
153159
return ".." + "...".join(_concat(x) for x in output) + ".."
154160

dash/dash-renderer/src/actions/callbacks.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -324,19 +324,37 @@ async function handleClientside(
324324
return result;
325325
}
326326

327-
function sideUpdate(outputs: any, dispatch: any, paths: any) {
328-
toPairs(outputs).forEach(([id, value]) => {
329-
const [componentId, propName] = id.split('.');
330-
const componentPath = paths.strs[componentId];
327+
function updateComponent(component_id: any, props: any) {
328+
return function (dispatch: any, getState: any) {
329+
const paths = getState().paths;
330+
const componentPath = getPath(paths, component_id);
331331
dispatch(
332332
updateProps({
333-
props: {[propName]: value},
333+
props,
334334
itempath: componentPath
335335
})
336336
);
337-
dispatch(
338-
notifyObservers({id: componentId, props: {[propName]: value}})
339-
);
337+
dispatch(notifyObservers({id: component_id, props}));
338+
};
339+
}
340+
341+
function sideUpdate(outputs: any, dispatch: any) {
342+
toPairs(outputs).forEach(([id, value]) => {
343+
let componentId, propName;
344+
if (id.includes('.')) {
345+
[componentId, propName] = id.split('.');
346+
if (componentId.startsWith('{')) {
347+
componentId = JSON.parse(componentId);
348+
}
349+
dispatch(updateComponent(componentId, {[propName]: value}));
350+
} else {
351+
if (id.startsWith('{')) {
352+
componentId = JSON.parse(id);
353+
} else {
354+
componentId = id;
355+
}
356+
dispatch(updateComponent(componentId, value));
357+
}
340358
});
341359
}
342360

@@ -345,7 +363,6 @@ function handleServerside(
345363
hooks: any,
346364
config: any,
347365
payload: any,
348-
paths: any,
349366
long: LongCallbackInfo | undefined,
350367
additionalArgs: [string, string, boolean?][] | undefined,
351368
getState: any,
@@ -365,7 +382,7 @@ function handleServerside(
365382
let moreArgs = additionalArgs;
366383

367384
if (running) {
368-
sideUpdate(running.running, dispatch, paths);
385+
sideUpdate(running.running, dispatch);
369386
runningOff = running.runningOff;
370387
}
371388

@@ -475,10 +492,10 @@ function handleServerside(
475492
dispatch(removeCallbackJob({jobId: job}));
476493
}
477494
if (runningOff) {
478-
sideUpdate(runningOff, dispatch, paths);
495+
sideUpdate(runningOff, dispatch);
479496
}
480497
if (progressDefault) {
481-
sideUpdate(progressDefault, dispatch, paths);
498+
sideUpdate(progressDefault, dispatch);
482499
}
483500
};
484501

@@ -500,8 +517,12 @@ function handleServerside(
500517
job = data.job;
501518
}
502519

520+
if (data.sideUpdate) {
521+
sideUpdate(data.sideUpdate, dispatch);
522+
}
523+
503524
if (data.progress) {
504-
sideUpdate(data.progress, dispatch, paths);
525+
sideUpdate(data.progress, dispatch);
505526
}
506527
if (!progressDefault && data.progressDefault) {
507528
progressDefault = data.progressDefault;
@@ -696,11 +717,7 @@ export function executeCallback(
696717
if (inter.length) {
697718
additionalArgs.push(['cancelJob', job.jobId]);
698719
if (job.progressDefault) {
699-
sideUpdate(
700-
job.progressDefault,
701-
dispatch,
702-
paths
703-
);
720+
sideUpdate(job.progressDefault, dispatch);
704721
}
705722
}
706723
}
@@ -713,7 +730,6 @@ export function executeCallback(
713730
hooks,
714731
newConfig,
715732
payload,
716-
paths,
717733
long,
718734
additionalArgs.length ? additionalArgs : undefined,
719735
getState,

0 commit comments

Comments
 (0)