Skip to content

Commit 3f932f6

Browse files
authored
Merge pull request #1212 from plotly/missing-inputs
Missing inputs
2 parents 680423e + b44bd54 commit 3f932f6

File tree

6 files changed

+603
-52
lines changed

6 files changed

+603
-52
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+
- [#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.
1112
- [#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.
1213

1314
## [1.11.0] - 2020-04-10

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ $ cd dash-renderer
1919
$ npm run build # or `renderer build`
2020
# install dash-renderer for development
2121
$ pip install -e .
22+
# build and install components used in tests
23+
$ npm run setup-tests
2224
# you should see both dash and dash-renderer are pointed to local source repos
2325
$ pip list | grep dash
2426
```

dash-renderer/src/actions/index.js

Lines changed: 111 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -136,51 +136,44 @@ function moveHistory(changeType) {
136136
}
137137

138138
function unwrapIfNotMulti(paths, idProps, spec, anyVals, depType) {
139+
let msg = '';
140+
139141
if (isMultiValued(spec)) {
140-
return idProps;
142+
return [idProps, msg];
141143
}
144+
142145
if (idProps.length !== 1) {
143146
if (!idProps.length) {
144-
if (typeof spec.id === 'string') {
145-
throw new ReferenceError(
146-
'A nonexistent object was used in an `' +
147-
depType +
148-
'` of a Dash callback. The id of this object is `' +
149-
spec.id +
150-
'` and the property is `' +
151-
spec.property +
152-
'`. The string ids in the current layout are: [' +
153-
keys(paths.strs).join(', ') +
154-
']'
155-
);
156-
}
157-
// TODO: unwrapped list of wildcard ids?
158-
// eslint-disable-next-line no-console
159-
console.log(paths.objs);
160-
throw new ReferenceError(
147+
const isStr = typeof spec.id === 'string';
148+
msg =
161149
'A nonexistent object was used in an `' +
162-
depType +
163-
'` of a Dash callback. The id of this object is ' +
164-
JSON.stringify(spec.id) +
165-
(anyVals ? ' with MATCH values ' + anyVals : '') +
166-
' and the property is `' +
167-
spec.property +
168-
'`. The wildcard ids currently available are logged above.'
169-
);
170-
}
171-
throw new ReferenceError(
172-
'Multiple objects were found for an `' +
150+
depType +
151+
'` of a Dash callback. The id of this object is ' +
152+
(isStr
153+
? '`' + spec.id + '`'
154+
: JSON.stringify(spec.id) +
155+
(anyVals ? ' with MATCH values ' + anyVals : '')) +
156+
' and the property is `' +
157+
spec.property +
158+
(isStr
159+
? '`. The string ids in the current layout are: [' +
160+
keys(paths.strs).join(', ') +
161+
']'
162+
: '`. The wildcard ids currently available are logged above.');
163+
} else {
164+
msg =
165+
'Multiple objects were found for an `' +
173166
depType +
174167
'` of a callback that only takes one value. The id spec is ' +
175168
JSON.stringify(spec.id) +
176169
(anyVals ? ' with MATCH values ' + anyVals : '') +
177170
' and the property is `' +
178171
spec.property +
179172
'`. The objects we found are: ' +
180-
JSON.stringify(map(pick(['id', 'property']), idProps))
181-
);
173+
JSON.stringify(map(pick(['id', 'property']), idProps));
174+
}
182175
}
183-
return idProps[0];
176+
return [idProps[0], msg];
184177
}
185178

186179
function startCallbacks(callbacks) {
@@ -247,20 +240,51 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) {
247240

248241
let payload;
249242
try {
250-
const outputs = allOutputs.map((out, i) =>
251-
unwrapIfNotMulti(
243+
const inVals = fillVals(paths, layout, cb, inputs, 'Input', true);
244+
245+
const preventCallback = () => {
246+
removeCallbackFromPending();
247+
// no server call here; for performance purposes pretend this is
248+
// a clientside callback and defer fireNext for the end
249+
// of the currently-ready callbacks.
250+
hasClientSide = true;
251+
return null;
252+
};
253+
254+
if (inVals === null) {
255+
return preventCallback();
256+
}
257+
258+
const outputs = [];
259+
const outputErrors = [];
260+
allOutputs.forEach((out, i) => {
261+
const [outi, erri] = unwrapIfNotMulti(
252262
paths,
253263
map(pick(['id', 'property']), out),
254264
cb.callback.outputs[i],
255265
cb.anyVals,
256266
'Output'
257-
)
258-
);
267+
);
268+
outputs.push(outi);
269+
if (erri) {
270+
outputErrors.push(erri);
271+
}
272+
});
273+
if (outputErrors.length) {
274+
if (flatten(inVals).length) {
275+
refErr(outputErrors, paths);
276+
}
277+
// This case is all-empty multivalued wildcard inputs,
278+
// which we would normally fire the callback for, except
279+
// some outputs are missing. So instead we treat it like
280+
// regular missing inputs and just silently prevent it.
281+
return preventCallback();
282+
}
259283

260284
payload = {
261285
output,
262286
outputs: isMultiOutputProp(output) ? outputs : outputs[0],
263-
inputs: fillVals(paths, layout, cb, inputs, 'Input'),
287+
inputs: inVals,
264288
changedPropIds: keys(cb.changedPropIds),
265289
};
266290
if (cb.callback.state.length) {
@@ -360,14 +384,18 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) {
360384
updatePending(pendingCallbacks, without(updated, allPropIds));
361385
}
362386

363-
function handleError(err) {
387+
function removeCallbackFromPending() {
364388
const {pendingCallbacks} = getState();
365389
if (requestIsActive(pendingCallbacks, resolvedId, requestId)) {
366390
// Skip all prop updates from this callback, and remove
367391
// it from the pending list so callbacks it was blocking
368392
// that have other changed inputs will still fire.
369393
updatePending(pendingCallbacks, allPropIds);
370394
}
395+
}
396+
397+
function handleError(err) {
398+
removeCallbackFromPending();
371399
const outputs = payload
372400
? map(combineIdAndProp, flatten([payload.outputs])).join(', ')
373401
: output;
@@ -398,10 +426,13 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) {
398426
return hasClientSide ? fireNext().then(done) : done;
399427
}
400428

401-
function fillVals(paths, layout, cb, specs, depType) {
429+
function fillVals(paths, layout, cb, specs, depType, allowAllMissing) {
402430
const getter = depType === 'Input' ? cb.getInputs : cb.getState;
403-
return getter(paths).map((inputList, i) =>
404-
unwrapIfNotMulti(
431+
const errors = [];
432+
let emptyMultiValues = 0;
433+
434+
const inputVals = getter(paths).map((inputList, i) => {
435+
const [inputs, inputError] = unwrapIfNotMulti(
405436
paths,
406437
inputList.map(({id, property, path: path_}) => ({
407438
id,
@@ -411,8 +442,45 @@ function fillVals(paths, layout, cb, specs, depType) {
411442
specs[i],
412443
cb.anyVals,
413444
depType
414-
)
415-
);
445+
);
446+
if (isMultiValued(specs[i]) && !inputs.length) {
447+
emptyMultiValues++;
448+
}
449+
if (inputError) {
450+
errors.push(inputError);
451+
}
452+
return inputs;
453+
});
454+
455+
if (errors.length) {
456+
if (
457+
allowAllMissing &&
458+
errors.length + emptyMultiValues === inputVals.length
459+
) {
460+
// We have at least one non-multivalued input, but all simple and
461+
// multi-valued inputs are missing.
462+
// (if all inputs are multivalued and all missing we still return
463+
// them as normal, and fire the callback.)
464+
return null;
465+
}
466+
// If we get here we have some missing and some present inputs.
467+
// Or all missing in a context that doesn't allow this.
468+
// That's a real problem, so throw the first message as an error.
469+
refErr(errors, paths);
470+
}
471+
472+
return inputVals;
473+
}
474+
475+
function refErr(errors, paths) {
476+
const err = errors[0];
477+
if (err.indexOf('logged above') !== -1) {
478+
// Wildcard reference errors mention a list of wildcard specs logged
479+
// TODO: unwrapped list of wildcard ids?
480+
// eslint-disable-next-line no-console
481+
console.error(paths.objs);
482+
}
483+
throw new ReferenceError(err);
416484
}
417485

418486
function handleServerside(config, payload, hooks) {

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"format": "run-s private::format.*",
1919
"initialize": "run-s private::initialize.*",
2020
"lint": "run-s private::lint.*",
21-
"test.integration": "run-s private::test.setup-* private::test.integration-*",
21+
"setup-tests": "run-s private::test.setup-*",
22+
"test.integration": "run-s setup-tests private::test.integration-*",
2223
"test.unit": "run-s private::test.unit-**"
2324
},
2425
"devDependencies": {

0 commit comments

Comments
 (0)