Skip to content
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

Missing pattern-matching outputs #1220

Merged
merged 3 commits into from
Apr 30, 2020
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- [#1078](https://github.com/plotly/dash/pull/1078) Permit usage of arbitrary file extensions for assets within component libraries

### Fixed
- [#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.
- [#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.
- [#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.

Expand Down
143 changes: 70 additions & 73 deletions dash-renderer/src/actions/dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
dissoc,
equals,
evolve,
findIndex,
flatten,
forEachObjIndexed,
includes,
Expand Down Expand Up @@ -396,12 +397,12 @@ function findInOutOverlap(outputs, inputs, head, dispatchError) {
}

function findMismatchedWildcards(outputs, inputs, state, head, dispatchError) {
const {anyKeys: out0AnyKeys} = findWildcardKeys(outputs[0].id);
outputs.forEach((out, outi) => {
if (outi && !equals(findWildcardKeys(out.id).anyKeys, out0AnyKeys)) {
const {matchKeys: out0MatchKeys} = findWildcardKeys(outputs[0].id);
outputs.forEach((out, i) => {
if (i && !equals(findWildcardKeys(out.id).matchKeys, out0MatchKeys)) {
dispatchError('Mismatched `MATCH` wildcards across `Output`s', [
head,
`Output ${outi} (${combineIdAndProp(out)})`,
`Output ${i} (${combineIdAndProp(out)})`,
'does not have MATCH wildcards on the same keys as',
`Output 0 (${combineIdAndProp(outputs[0])}).`,
'MATCH wildcards must be on the same keys for all Outputs.',
Expand All @@ -414,9 +415,9 @@ function findMismatchedWildcards(outputs, inputs, state, head, dispatchError) {
[state, 'State'],
].forEach(([args, cls]) => {
args.forEach((arg, i) => {
const {anyKeys, allsmallerKeys} = findWildcardKeys(arg.id);
const allWildcardKeys = anyKeys.concat(allsmallerKeys);
const diff = difference(allWildcardKeys, out0AnyKeys);
const {matchKeys, allsmallerKeys} = findWildcardKeys(arg.id);
const allWildcardKeys = matchKeys.concat(allsmallerKeys);
const diff = difference(allWildcardKeys, out0MatchKeys);
if (diff.length) {
diff.sort();
dispatchError('`Input` / `State` wildcards not in `Output`s', [
Expand Down Expand Up @@ -639,7 +640,7 @@ export function computeGraphs(dependencies, dispatchError) {
* {[id]: {[prop]: [callback, ...]}}
* where callbacks are the matching specs from the original
* dependenciesRequest, but with outputs parsed to look like inputs,
* and a list anyKeys added if the outputs have MATCH wildcards.
* and a list matchKeys added if the outputs have MATCH wildcards.
* For outputMap there should only ever be one callback per id/prop
* but for inputMap there may be many.
*
Expand Down Expand Up @@ -785,9 +786,10 @@ 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 {anyKeys, hasALL} = findWildcardKeys(outputs[0].id);
const {matchKeys} = findWildcardKeys(outputs[0].id);
const firstSingleOutput = findIndex(o => !isMultiValued(o.id), outputs);
const finalDependency = mergeRight(
{hasALL, anyKeys, outputs},
{matchKeys, firstSingleOutput, outputs},
dependency
);

Expand Down Expand Up @@ -820,23 +822,20 @@ export function computeGraphs(dependencies, dispatchError) {
}

function findWildcardKeys(id) {
const anyKeys = [];
const matchKeys = [];
const allsmallerKeys = [];
let hasALL = false;
if (typeof id === 'object') {
forEachObjIndexed((val, key) => {
if (val === MATCH) {
anyKeys.push(key);
matchKeys.push(key);
} else if (val === ALLSMALLER) {
allsmallerKeys.push(key);
} else if (val === ALL) {
hasALL = true;
}
}, id);
anyKeys.sort();
matchKeys.sort();
allsmallerKeys.sort();
}
return {anyKeys, allsmallerKeys, hasALL};
return {matchKeys, allsmallerKeys};
}

/*
Expand Down Expand Up @@ -1048,31 +1047,6 @@ function getCallbackByOutput(graphs, paths, id, prop) {
return makeResolvedCallback(callback, resolve, anyVals);
}

/*
* If there are ALL keys we need to reduce a set of outputs resolved
* from an input to one item per combination of MATCH values.
* That will give one result per callback invocation.
*/
function reduceALLOuts(outs, anyKeys, hasALL) {
if (!hasALL) {
return outs;
}
if (!anyKeys.length) {
// If there's ALL but no MATCH, there's only one invocation
// of the callback, so just base it off the first output.
return [outs[0]];
}
const anySeen = {};
return outs.filter(i => {
const matchKeys = JSON.stringify(props(anyKeys, i.id));
if (!anySeen[matchKeys]) {
anySeen[matchKeys] = 1;
return true;
}
return false;
});
}

function addResolvedFromOutputs(callback, outPattern, outs, matches) {
const out0Keys = Object.keys(outPattern.id).sort();
const out0PatternVals = props(out0Keys, outPattern.id);
Expand All @@ -1088,6 +1062,51 @@ function addResolvedFromOutputs(callback, outPattern, outs, matches) {
});
}

function addAllResolvedFromOutputs(resolve, paths, matches) {
return callback => {
const {matchKeys, firstSingleOutput, outputs} = callback;
if (matchKeys.length) {
const singleOutPattern = outputs[firstSingleOutput];
if (singleOutPattern) {
addResolvedFromOutputs(
callback,
singleOutPattern,
resolve(paths)(singleOutPattern),
matches
);
} else {
/*
* If every output has ALL we need to reduce resolved set
* to one item per combination of MATCH values.
* That will give one result per callback invocation.
*/
const anySeen = {};
outputs.forEach(outPattern => {
const outSet = resolve(paths)(outPattern).filter(i => {
const matchStr = JSON.stringify(props(matchKeys, i.id));
if (!anySeen[matchStr]) {
anySeen[matchStr] = 1;
return true;
}
return false;
});
addResolvedFromOutputs(
callback,
outPattern,
outSet,
matches
);
});
}
} else {
const cb = makeResolvedCallback(callback, resolve, '');
if (flatten(cb.getOutputs(paths)).length) {
matches.push(cb);
}
}
};
}

/*
* For a given id and prop find all callbacks it's an input of.
*
Expand All @@ -1111,21 +1130,9 @@ export function getCallbacksByInput(graphs, paths, id, prop, changeType) {
return [];
}

const baseResolve = resolveDeps();
callbacks.forEach(callback => {
const {anyKeys, hasALL} = callback;
if (anyKeys) {
const out0Pattern = callback.outputs[0];
const out0Set = reduceALLOuts(
baseResolve(paths)(out0Pattern),
anyKeys,
hasALL
);
addResolvedFromOutputs(callback, out0Pattern, out0Set, matches);
} else {
matches.push(makeResolvedCallback(callback, baseResolve, ''));
}
});
callbacks.forEach(
addAllResolvedFromOutputs(resolveDeps(), paths, matches)
);
} else {
// wildcard version
const keys = Object.keys(id).sort();
Expand All @@ -1137,23 +1144,13 @@ export function getCallbacksByInput(graphs, paths, id, prop, changeType) {
}
patterns.forEach(pattern => {
if (idMatch(keys, vals, pattern.values)) {
const resolve = resolveDeps(keys, vals, pattern.values);
pattern.callbacks.forEach(callback => {
const out0Pattern = callback.outputs[0];
const {anyKeys, hasALL} = callback;
const out0Set = reduceALLOuts(
resolve(paths)(out0Pattern),
anyKeys,
hasALL
);

addResolvedFromOutputs(
callback,
out0Pattern,
out0Set,
pattern.callbacks.forEach(
addAllResolvedFromOutputs(
resolveDeps(keys, vals, pattern.values),
paths,
matches
);
});
)
);
}
});
}
Expand Down
Loading