Skip to content

Support for callbacks with no outputs #2682

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

Closed
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
13 changes: 11 additions & 2 deletions dash/_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def callback(
cancel=None,
manager=None,
cache_args_to_ignore=None,
force_no_output=False,
**_kwargs,
):
"""
Expand Down Expand Up @@ -183,6 +184,7 @@ def callback(
*_args,
**_kwargs,
long=long_spec,
force_no_output=force_no_output,
manager=manager,
)

Expand Down Expand Up @@ -224,6 +226,7 @@ def insert_callback(
long=None,
manager=None,
dynamic_creator=False,
force_no_output=False
):
if prevent_initial_call is None:
prevent_initial_call = config_prevent_initial_callbacks
Expand All @@ -246,6 +249,7 @@ def insert_callback(
"interval": long["interval"],
},
"dynamic_creator": dynamic_creator,
"force_no_output":force_no_output,
}

callback_map[callback_id] = {
Expand Down Expand Up @@ -286,6 +290,7 @@ def register_callback( # pylint: disable=R0914
long = _kwargs.get("long")
manager = _kwargs.get("manager")
allow_dynamic_callbacks = _kwargs.get("_allow_dynamic_callbacks")
force_no_output = _kwargs.get("force_no_output")

output_indices = make_grouping_by_index(output, list(range(grouping_len(output))))
callback_id = insert_callback(
Expand All @@ -301,6 +306,7 @@ def register_callback( # pylint: disable=R0914
long=long,
manager=manager,
dynamic_creator=allow_dynamic_callbacks,
force_no_output=force_no_output
)

# pylint: disable=too-many-locals
Expand Down Expand Up @@ -454,8 +460,11 @@ def add_context(*args, **kwargs):
output_value = list(output_value)

# Flatten grouping and validate grouping structure
flat_output_values = flatten_grouping(output_value, output)

if len(output):
flat_output_values = flatten_grouping(output_value, output)
else:
# for no output callback
flat_output_values = []
_validate.validate_multi_return(
output_spec, flat_output_values, callback_id
)
Expand Down
6 changes: 6 additions & 0 deletions dash/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ def _concat(x):
return _id

if isinstance(output, (list, tuple)):
if len(output) == 0:
if not hashed_inputs:
hashed_inputs = hashlib.md5(
".".join(str(x) for x in inputs).encode("utf-8")
).hexdigest()
return ".."+hashed_inputs+".."
return ".." + "...".join(_concat(x) for x in output) + ".."

return _concat(output)
Expand Down
105,782 changes: 46,295 additions & 59,487 deletions dash/dash-renderer/build/dash_renderer.dev.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dash/dash-renderer/build/dash_renderer.min.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion dash/dash-renderer/src/actions/callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ export function executeCallback(
dispatch: any,
getState: any
): IExecutingCallback {
const {output, inputs, state, clientside_function, long, dynamic_creator} =
const {output, inputs, state, clientside_function, long, dynamic_creator, force_no_output} =
cb.callback;
try {
const inVals = fillVals(paths, layout, cb, inputs, 'Input', true);
Expand Down Expand Up @@ -636,6 +636,7 @@ export function executeCallback(
outputs: isMultiOutputProp(output) ? outputs : outputs[0],
inputs: inVals,
changedPropIds: keys(cb.changedPropIds),
force_no_output: force_no_output,
state: cb.callback.state.length
? fillVals(paths, layout, cb, state, 'State')
: undefined
Expand Down
42 changes: 25 additions & 17 deletions dash/dash-renderer/src/actions/dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,16 @@ function validateDependencies(parsedDependencies, dispatchError) {
const outObjs = [];

parsedDependencies.forEach(dep => {
const {inputs, outputs, state} = dep;
const {inputs, outputs, state, force_no_output} = dep;
let hasOutputs = true;
if (outputs.length === 1 && !outputs[0].id && !outputs[0].property) {
hasOutputs = false;
dispatchError('A callback is missing Outputs', [
'Please provide an output for this callback:',
JSON.stringify(dep, null, 2)
]);
if (!force_no_output) {
dispatchError('A callback is missing Outputs', [
'Please provide an output for this callback:',
JSON.stringify(dep, null, 2)
]);
}
}

const head =
Expand Down Expand Up @@ -234,16 +236,18 @@ function validateDependencies(parsedDependencies, dispatchError) {
]);
}
args.forEach((idProp, i) => {
validateArg(idProp, head, cls, i, dispatchError);
validateArg(idProp, head, cls, i, dispatchError, force_no_output);
});
});

findDuplicateOutputs(outputs, head, dispatchError, outStrs, outObjs);
findMismatchedWildcards(outputs, inputs, state, head, dispatchError);
if (!force_no_output) {
findMismatchedWildcards(outputs, inputs, state, head, dispatchError);
}
});
}

function validateArg({id, property}, head, cls, i, dispatchError) {
function validateArg({id, property}, head, cls, i, dispatchError, force_no_output) {
if (typeof property !== 'string' || !property) {
dispatchError('Callback property error', [
head,
Expand All @@ -253,7 +257,7 @@ function validateArg({id, property}, head, cls, i, dispatchError) {
}

if (typeof id === 'object') {
if (isEmpty(id)) {
if (isEmpty(id) && !force_no_output) {
dispatchError('Callback item missing ID', [
head,
`${cls}[${i}].id = {}`,
Expand Down Expand Up @@ -290,7 +294,7 @@ function validateArg({id, property}, head, cls, i, dispatchError) {
}
}, id);
} else if (typeof id === 'string') {
if (!id) {
if (!id && !force_no_output) {
dispatchError('Callback item missing ID', [
head,
`${cls}[${i}].id = "${id}"`,
Expand Down Expand Up @@ -605,12 +609,16 @@ export function computeGraphs(dependencies, dispatchError) {

const fixIds = map(evolve({id: parseIfWildcard}));
const parsedDependencies = map(dep => {
const {output} = dep;
const {output, force_no_output} = dep;
const out = evolve({inputs: fixIds, state: fixIds}, dep);
out.outputs = map(
outi => assoc('out', true, splitIdAndProp(outi)),
isMultiOutputProp(output) ? parseMultipleOutputs(output) : [output]
);
if(!force_no_output){
out.outputs = map(
outi => assoc('out', true, splitIdAndProp(outi)),
isMultiOutputProp(output) ? parseMultipleOutputs(output) : [output]
);
} else {
out.outputs = [];
}
return out;
}, dependencies);

Expand Down Expand Up @@ -809,7 +817,7 @@ export function computeGraphs(dependencies, dispatchError) {
// Also collect MATCH keys in the output (all outputs must share these)
// and ALL keys in the first output (need not be shared but we'll use
// the first output for calculations) for later convenience.
const {matchKeys} = findWildcardKeys(outputs[0].id);
const {matchKeys} = findWildcardKeys(outputs.length ? outputs[0].id : undefined);
const firstSingleOutput = findIndex(o => !isMultiValued(o.id), outputs);
const finalDependency = mergeRight(
{matchKeys, firstSingleOutput, outputs},
Expand Down Expand Up @@ -1091,7 +1099,7 @@ export function addAllResolvedFromOutputs(resolve, paths, matches) {
}
} else {
const cb = makeResolvedCallback(callback, resolve, '');
if (flatten(cb.getOutputs(paths)).length) {
if (flatten(cb.getOutputs(paths)).length || callback.force_no_output) {
matches.push(cb);
}
}
Expand Down
2 changes: 2 additions & 0 deletions dash/dash-renderer/src/types/callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ICallbackDefinition {
state: ICallbackProperty[];
long?: LongCallbackInfo;
dynamic_creator?: boolean;
force_no_output: boolean;
}

export interface ICallbackProperty {
Expand Down Expand Up @@ -74,6 +75,7 @@ export interface ICallbackPayload {
output: string;
outputs: any[];
state?: any[] | null;
force_no_output: boolean;
}

export type CallbackResult = {
Expand Down
2 changes: 1 addition & 1 deletion dash/dash-renderer/webpack.base.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const defaults = {
{
test: /\.ts(x?)$/,
exclude: /node_modules/,
use: ['babel-loader', 'ts-loader'],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I wasn't aware this could be omitted. Sounds to me as though it's safer to keep it though, to keep type checking. Per https://webpack.js.org/guides/typescript/#loader:

Note that if you're already using babel-loader to transpile your code, you can use @babel/preset-typescript and let Babel handle both your JavaScript and TypeScript files instead of using an additional loader. Keep in mind that, contrary to ts-loader, the underlying @babel/plugin-transform-typescript plugin does not perform any type checking.

use: ['babel-loader'],
},
{
test: /\.css$/,
Expand Down
31 changes: 19 additions & 12 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -1233,9 +1233,13 @@ def dispatch(self):
"state", []
)
output = body["output"]
outputs_list = body.get("outputs") or split_callback_id(output)
g.outputs_list = outputs_list # pylint: disable=assigning-non-slot


if body["force_no_output"]:
outputs_list = []
g.outputs_list = outputs_list
else:
outputs_list = body.get("outputs") or split_callback_id(output)
g.outputs_list = outputs_list # pylint: disable=assigning-non-slot
g.input_values = ( # pylint: disable=assigning-non-slot
input_values
) = inputs_to_dict(inputs)
Expand Down Expand Up @@ -1291,15 +1295,18 @@ def dispatch(self):
flat_outputs = [outputs_list]
else:
flat_outputs = outputs_list

outputs_grouping = map_grouping(
lambda ind: flat_outputs[ind], outputs_indices
)
g.outputs_grouping = outputs_grouping # pylint: disable=assigning-non-slot
g.using_outputs_grouping = ( # pylint: disable=assigning-non-slot
not isinstance(outputs_indices, int)
and outputs_indices != list(range(grouping_len(outputs_indices)))
)
if len(flat_outputs):
outputs_grouping = map_grouping(
lambda ind: flat_outputs[ind], outputs_indices
)
g.outputs_grouping = outputs_grouping # pylint: disable=assigning-non-slot
g.using_outputs_grouping = ( # pylint: disable=assigning-non-slot
not isinstance(outputs_indices, int)
and outputs_indices != list(range(grouping_len(outputs_indices)))
)
else:
g.outputs_grouping = []
g.using_outputs_grouping = []

except KeyError as missing_callback_function:
msg = f"Callback function not found for output '{output}', perhaps you forgot to prepend the '@'?"
Expand Down