Skip to content

Commit 5b2ced7

Browse files
authored
Merge pull request #1220 from plotly/missing-outputs
Missing pattern-matching outputs
2 parents 3f932f6 + 005790f commit 5b2ced7

File tree

3 files changed

+314
-73
lines changed

3 files changed

+314
-73
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
88
- [#1078](https://github.com/plotly/dash/pull/1078) Permit usage of arbitrary file extensions for assets within component libraries
99

1010
### Fixed
11+
- [#1220](https://github.com/plotly/dash/pull/1220) Fixes [#1216](https://github.com/plotly/dash/issues/1216), a set of related issues about pattern-matching callbacks with `ALL` wildcards in their `Output` which would fail if no components matched the pattern.
1112
- [#1212](https://github.com/plotly/dash/pull/1212) Fixes [#1200](https://github.com/plotly/dash/issues/1200) - prior to Dash 1.11, if none of the inputs to a callback were on the page, it was not an error. This was, and is now again, treated as though the callback raised PreventUpdate. The one exception to this is with pattern-matching callbacks, when every Input uses a multi-value wildcard (ALL or ALLSMALLER), and every Output is on the page. In that case the callback fires as usual.
1213
- [#1201](https://github.com/plotly/dash/pull/1201) Fixes [#1193](https://github.com/plotly/dash/issues/1193) - prior to Dash 1.11, you could use `flask.has_request_context() == False` inside an `app.layout` function to provide a special layout containing all IDs for validation purposes in a multi-page app. Dash 1.11 broke this when we moved most of this validation into the renderer. This change makes it work again.
1314

dash-renderer/src/actions/dependencies.js

Lines changed: 70 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
dissoc,
1111
equals,
1212
evolve,
13+
findIndex,
1314
flatten,
1415
forEachObjIndexed,
1516
includes,
@@ -396,12 +397,12 @@ function findInOutOverlap(outputs, inputs, head, dispatchError) {
396397
}
397398

398399
function findMismatchedWildcards(outputs, inputs, state, head, dispatchError) {
399-
const {anyKeys: out0AnyKeys} = findWildcardKeys(outputs[0].id);
400-
outputs.forEach((out, outi) => {
401-
if (outi && !equals(findWildcardKeys(out.id).anyKeys, out0AnyKeys)) {
400+
const {matchKeys: out0MatchKeys} = findWildcardKeys(outputs[0].id);
401+
outputs.forEach((out, i) => {
402+
if (i && !equals(findWildcardKeys(out.id).matchKeys, out0MatchKeys)) {
402403
dispatchError('Mismatched `MATCH` wildcards across `Output`s', [
403404
head,
404-
`Output ${outi} (${combineIdAndProp(out)})`,
405+
`Output ${i} (${combineIdAndProp(out)})`,
405406
'does not have MATCH wildcards on the same keys as',
406407
`Output 0 (${combineIdAndProp(outputs[0])}).`,
407408
'MATCH wildcards must be on the same keys for all Outputs.',
@@ -414,9 +415,9 @@ function findMismatchedWildcards(outputs, inputs, state, head, dispatchError) {
414415
[state, 'State'],
415416
].forEach(([args, cls]) => {
416417
args.forEach((arg, i) => {
417-
const {anyKeys, allsmallerKeys} = findWildcardKeys(arg.id);
418-
const allWildcardKeys = anyKeys.concat(allsmallerKeys);
419-
const diff = difference(allWildcardKeys, out0AnyKeys);
418+
const {matchKeys, allsmallerKeys} = findWildcardKeys(arg.id);
419+
const allWildcardKeys = matchKeys.concat(allsmallerKeys);
420+
const diff = difference(allWildcardKeys, out0MatchKeys);
420421
if (diff.length) {
421422
diff.sort();
422423
dispatchError('`Input` / `State` wildcards not in `Output`s', [
@@ -639,7 +640,7 @@ export function computeGraphs(dependencies, dispatchError) {
639640
* {[id]: {[prop]: [callback, ...]}}
640641
* where callbacks are the matching specs from the original
641642
* dependenciesRequest, but with outputs parsed to look like inputs,
642-
* and a list anyKeys added if the outputs have MATCH wildcards.
643+
* and a list matchKeys added if the outputs have MATCH wildcards.
643644
* For outputMap there should only ever be one callback per id/prop
644645
* but for inputMap there may be many.
645646
*
@@ -785,9 +786,10 @@ export function computeGraphs(dependencies, dispatchError) {
785786
// Also collect MATCH keys in the output (all outputs must share these)
786787
// and ALL keys in the first output (need not be shared but we'll use
787788
// the first output for calculations) for later convenience.
788-
const {anyKeys, hasALL} = findWildcardKeys(outputs[0].id);
789+
const {matchKeys} = findWildcardKeys(outputs[0].id);
790+
const firstSingleOutput = findIndex(o => !isMultiValued(o.id), outputs);
789791
const finalDependency = mergeRight(
790-
{hasALL, anyKeys, outputs},
792+
{matchKeys, firstSingleOutput, outputs},
791793
dependency
792794
);
793795

@@ -820,23 +822,20 @@ export function computeGraphs(dependencies, dispatchError) {
820822
}
821823

822824
function findWildcardKeys(id) {
823-
const anyKeys = [];
825+
const matchKeys = [];
824826
const allsmallerKeys = [];
825-
let hasALL = false;
826827
if (typeof id === 'object') {
827828
forEachObjIndexed((val, key) => {
828829
if (val === MATCH) {
829-
anyKeys.push(key);
830+
matchKeys.push(key);
830831
} else if (val === ALLSMALLER) {
831832
allsmallerKeys.push(key);
832-
} else if (val === ALL) {
833-
hasALL = true;
834833
}
835834
}, id);
836-
anyKeys.sort();
835+
matchKeys.sort();
837836
allsmallerKeys.sort();
838837
}
839-
return {anyKeys, allsmallerKeys, hasALL};
838+
return {matchKeys, allsmallerKeys};
840839
}
841840

842841
/*
@@ -1048,31 +1047,6 @@ function getCallbackByOutput(graphs, paths, id, prop) {
10481047
return makeResolvedCallback(callback, resolve, anyVals);
10491048
}
10501049

1051-
/*
1052-
* If there are ALL keys we need to reduce a set of outputs resolved
1053-
* from an input to one item per combination of MATCH values.
1054-
* That will give one result per callback invocation.
1055-
*/
1056-
function reduceALLOuts(outs, anyKeys, hasALL) {
1057-
if (!hasALL) {
1058-
return outs;
1059-
}
1060-
if (!anyKeys.length) {
1061-
// If there's ALL but no MATCH, there's only one invocation
1062-
// of the callback, so just base it off the first output.
1063-
return [outs[0]];
1064-
}
1065-
const anySeen = {};
1066-
return outs.filter(i => {
1067-
const matchKeys = JSON.stringify(props(anyKeys, i.id));
1068-
if (!anySeen[matchKeys]) {
1069-
anySeen[matchKeys] = 1;
1070-
return true;
1071-
}
1072-
return false;
1073-
});
1074-
}
1075-
10761050
function addResolvedFromOutputs(callback, outPattern, outs, matches) {
10771051
const out0Keys = Object.keys(outPattern.id).sort();
10781052
const out0PatternVals = props(out0Keys, outPattern.id);
@@ -1088,6 +1062,51 @@ function addResolvedFromOutputs(callback, outPattern, outs, matches) {
10881062
});
10891063
}
10901064

1065+
function addAllResolvedFromOutputs(resolve, paths, matches) {
1066+
return callback => {
1067+
const {matchKeys, firstSingleOutput, outputs} = callback;
1068+
if (matchKeys.length) {
1069+
const singleOutPattern = outputs[firstSingleOutput];
1070+
if (singleOutPattern) {
1071+
addResolvedFromOutputs(
1072+
callback,
1073+
singleOutPattern,
1074+
resolve(paths)(singleOutPattern),
1075+
matches
1076+
);
1077+
} else {
1078+
/*
1079+
* If every output has ALL we need to reduce resolved set
1080+
* to one item per combination of MATCH values.
1081+
* That will give one result per callback invocation.
1082+
*/
1083+
const anySeen = {};
1084+
outputs.forEach(outPattern => {
1085+
const outSet = resolve(paths)(outPattern).filter(i => {
1086+
const matchStr = JSON.stringify(props(matchKeys, i.id));
1087+
if (!anySeen[matchStr]) {
1088+
anySeen[matchStr] = 1;
1089+
return true;
1090+
}
1091+
return false;
1092+
});
1093+
addResolvedFromOutputs(
1094+
callback,
1095+
outPattern,
1096+
outSet,
1097+
matches
1098+
);
1099+
});
1100+
}
1101+
} else {
1102+
const cb = makeResolvedCallback(callback, resolve, '');
1103+
if (flatten(cb.getOutputs(paths)).length) {
1104+
matches.push(cb);
1105+
}
1106+
}
1107+
};
1108+
}
1109+
10911110
/*
10921111
* For a given id and prop find all callbacks it's an input of.
10931112
*
@@ -1111,21 +1130,9 @@ export function getCallbacksByInput(graphs, paths, id, prop, changeType) {
11111130
return [];
11121131
}
11131132

1114-
const baseResolve = resolveDeps();
1115-
callbacks.forEach(callback => {
1116-
const {anyKeys, hasALL} = callback;
1117-
if (anyKeys) {
1118-
const out0Pattern = callback.outputs[0];
1119-
const out0Set = reduceALLOuts(
1120-
baseResolve(paths)(out0Pattern),
1121-
anyKeys,
1122-
hasALL
1123-
);
1124-
addResolvedFromOutputs(callback, out0Pattern, out0Set, matches);
1125-
} else {
1126-
matches.push(makeResolvedCallback(callback, baseResolve, ''));
1127-
}
1128-
});
1133+
callbacks.forEach(
1134+
addAllResolvedFromOutputs(resolveDeps(), paths, matches)
1135+
);
11291136
} else {
11301137
// wildcard version
11311138
const keys = Object.keys(id).sort();
@@ -1137,23 +1144,13 @@ export function getCallbacksByInput(graphs, paths, id, prop, changeType) {
11371144
}
11381145
patterns.forEach(pattern => {
11391146
if (idMatch(keys, vals, pattern.values)) {
1140-
const resolve = resolveDeps(keys, vals, pattern.values);
1141-
pattern.callbacks.forEach(callback => {
1142-
const out0Pattern = callback.outputs[0];
1143-
const {anyKeys, hasALL} = callback;
1144-
const out0Set = reduceALLOuts(
1145-
resolve(paths)(out0Pattern),
1146-
anyKeys,
1147-
hasALL
1148-
);
1149-
1150-
addResolvedFromOutputs(
1151-
callback,
1152-
out0Pattern,
1153-
out0Set,
1147+
pattern.callbacks.forEach(
1148+
addAllResolvedFromOutputs(
1149+
resolveDeps(keys, vals, pattern.values),
1150+
paths,
11541151
matches
1155-
);
1156-
});
1152+
)
1153+
);
11571154
}
11581155
});
11591156
}

0 commit comments

Comments
 (0)