Skip to content

Commit bbd013c

Browse files
authored
Merge pull request #2898 from aGitForEveryone/running-non-existent-component
Allowing defining potentially non existent components in the running keyword
2 parents b13d9b8 + 62b2f1d commit bbd013c

File tree

3 files changed

+116
-4
lines changed

3 files changed

+116
-4
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
1010

1111
## Fixed
1212

13+
- [#2898](https://github.com/plotly/dash/pull/2898) Fix error thrown when using non-existent components in callback running keyword. Fixes [#2897](https://github.com/plotly/dash/issues/2897).
1314
- [#2892](https://github.com/plotly/dash/pull/2860) Fix ensures dcc.Dropdown menu maxHeight option works with Datatable. Fixes [#2529](https://github.com/plotly/dash/issues/2529) [#2225](https://github.com/plotly/dash/issues/2225)
1415
- [#2896](https://github.com/plotly/dash/pull/2896) The tabIndex parameter of Div can accept number or string type. Fixes [#2891](https://github.com/plotly/dash/issues/2891)
1516

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

+23-4
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
} from '../types/callbacks';
3737
import {isMultiValued, stringifyId, isMultiOutputProp} from './dependencies';
3838
import {urlBase} from './utils';
39-
import {getCSRFHeader} from '.';
39+
import {getCSRFHeader, dispatchError} from '.';
4040
import {createAction, Action} from 'redux-actions';
4141
import {addHttpHeaders} from '../actions';
4242
import {notifyObservers, updateProps} from './index';
@@ -330,10 +330,29 @@ async function handleClientside(
330330
return result;
331331
}
332332

333-
function updateComponent(component_id: any, props: any) {
333+
function updateComponent(component_id: any, props: any, cb: ICallbackPayload) {
334334
return function (dispatch: any, getState: any) {
335-
const paths = getState().paths;
335+
const {paths, config} = getState();
336336
const componentPath = getPath(paths, component_id);
337+
if (!componentPath) {
338+
if (!config.suppress_callback_exceptions) {
339+
dispatchError(dispatch)(
340+
'ID running component not found in layout',
341+
[
342+
'Component defined in running keyword not found in layout.',
343+
`Component id: "${stringifyId(component_id)}"`,
344+
'This ID was used in the callback(s) for Output(s):',
345+
`${cb.output}`,
346+
'You can suppress this exception by setting',
347+
'`suppress_callback_exceptions=True`.'
348+
]
349+
);
350+
}
351+
// We need to stop further processing because functions further on
352+
// can't operate on an 'undefined' object, and they will throw an
353+
// error.
354+
return;
355+
}
337356
dispatch(
338357
updateProps({
339358
props,
@@ -381,7 +400,7 @@ function sideUpdate(outputs: SideUpdateOutput, cb: ICallbackPayload) {
381400
return acc;
382401
}, [] as any[])
383402
.forEach(([id, idProps]) => {
384-
dispatch(updateComponent(id, idProps));
403+
dispatch(updateComponent(id, idProps, cb));
385404
});
386405
};
387406
}

tests/integration/callbacks/test_basic_callback.py

+92
Original file line numberDiff line numberDiff line change
@@ -823,3 +823,95 @@ def on_click(_):
823823

824824
dash_duo.wait_for_text_to_equal("#output", "done")
825825
dash_duo.wait_for_text_to_equal("#running", "off")
826+
827+
828+
def test_cbsc020_callback_running_non_existing_component(dash_duo):
829+
lock = Lock()
830+
app = Dash(__name__, suppress_callback_exceptions=True)
831+
832+
app.layout = html.Div(
833+
[
834+
html.Button("start", id="start"),
835+
html.Div(id="output"),
836+
]
837+
)
838+
839+
@app.callback(
840+
Output("output", "children"),
841+
Input("start", "n_clicks"),
842+
running=[
843+
[
844+
Output("non_existent_component", "children"),
845+
html.B("on", id="content"),
846+
"off",
847+
]
848+
],
849+
prevent_initial_call=True,
850+
)
851+
def on_click(_):
852+
with lock:
853+
pass
854+
return "done"
855+
856+
dash_duo.start_server(app)
857+
with lock:
858+
dash_duo.find_element("#start").click()
859+
860+
dash_duo.wait_for_text_to_equal("#output", "done")
861+
862+
863+
def test_cbsc021_callback_running_non_existing_component(dash_duo):
864+
lock = Lock()
865+
app = Dash(__name__)
866+
867+
app.layout = html.Div(
868+
[
869+
html.Button("start", id="start"),
870+
html.Div(id="output"),
871+
]
872+
)
873+
874+
@app.callback(
875+
Output("output", "children"),
876+
Input("start", "n_clicks"),
877+
running=[
878+
[
879+
Output("non_existent_component", "children"),
880+
html.B("on", id="content"),
881+
"off",
882+
]
883+
],
884+
prevent_initial_call=True,
885+
)
886+
def on_click(_):
887+
with lock:
888+
pass
889+
return "done"
890+
891+
dash_duo.start_server(
892+
app,
893+
debug=True,
894+
use_reloader=False,
895+
use_debugger=True,
896+
dev_tools_hot_reload=False,
897+
)
898+
with lock:
899+
dash_duo.find_element("#start").click()
900+
901+
dash_duo.wait_for_text_to_equal("#output", "done")
902+
error_title = "ID running component not found in layout"
903+
error_message = [
904+
"Component defined in running keyword not found in layout.",
905+
'Component id: "non_existent_component"',
906+
"This ID was used in the callback(s) for Output(s):",
907+
"output.children",
908+
"You can suppress this exception by setting",
909+
"`suppress_callback_exceptions=True`.",
910+
]
911+
# The error should show twice, once for trying to set running on and once for
912+
# turning it off.
913+
dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "2")
914+
for error in dash_duo.find_elements(".dash-fe-error__title"):
915+
assert error.text == error_title
916+
for error_text in dash_duo.find_elements(".dash-backend-error"):
917+
assert all(line in error_text for line in error_message)

0 commit comments

Comments
 (0)