Skip to content

Commit

Permalink
Merge pull request #2898 from aGitForEveryone/running-non-existent-co…
Browse files Browse the repository at this point in the history
…mponent

Allowing defining potentially non existent components in the running keyword
  • Loading branch information
T4rk1n authored Jun 25, 2024
2 parents b13d9b8 + 62b2f1d commit bbd013c
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).

## Fixed

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

Expand Down
27 changes: 23 additions & 4 deletions dash/dash-renderer/src/actions/callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
} from '../types/callbacks';
import {isMultiValued, stringifyId, isMultiOutputProp} from './dependencies';
import {urlBase} from './utils';
import {getCSRFHeader} from '.';
import {getCSRFHeader, dispatchError} from '.';
import {createAction, Action} from 'redux-actions';
import {addHttpHeaders} from '../actions';
import {notifyObservers, updateProps} from './index';
Expand Down Expand Up @@ -330,10 +330,29 @@ async function handleClientside(
return result;
}

function updateComponent(component_id: any, props: any) {
function updateComponent(component_id: any, props: any, cb: ICallbackPayload) {
return function (dispatch: any, getState: any) {
const paths = getState().paths;
const {paths, config} = getState();
const componentPath = getPath(paths, component_id);
if (!componentPath) {
if (!config.suppress_callback_exceptions) {
dispatchError(dispatch)(
'ID running component not found in layout',
[
'Component defined in running keyword not found in layout.',
`Component id: "${stringifyId(component_id)}"`,
'This ID was used in the callback(s) for Output(s):',
`${cb.output}`,
'You can suppress this exception by setting',
'`suppress_callback_exceptions=True`.'
]
);
}
// We need to stop further processing because functions further on
// can't operate on an 'undefined' object, and they will throw an
// error.
return;
}
dispatch(
updateProps({
props,
Expand Down Expand Up @@ -381,7 +400,7 @@ function sideUpdate(outputs: SideUpdateOutput, cb: ICallbackPayload) {
return acc;
}, [] as any[])
.forEach(([id, idProps]) => {
dispatch(updateComponent(id, idProps));
dispatch(updateComponent(id, idProps, cb));
});
};
}
Expand Down
92 changes: 92 additions & 0 deletions tests/integration/callbacks/test_basic_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,3 +823,95 @@ def on_click(_):

dash_duo.wait_for_text_to_equal("#output", "done")
dash_duo.wait_for_text_to_equal("#running", "off")


def test_cbsc020_callback_running_non_existing_component(dash_duo):
lock = Lock()
app = Dash(__name__, suppress_callback_exceptions=True)

app.layout = html.Div(
[
html.Button("start", id="start"),
html.Div(id="output"),
]
)

@app.callback(
Output("output", "children"),
Input("start", "n_clicks"),
running=[
[
Output("non_existent_component", "children"),
html.B("on", id="content"),
"off",
]
],
prevent_initial_call=True,
)
def on_click(_):
with lock:
pass
return "done"

dash_duo.start_server(app)
with lock:
dash_duo.find_element("#start").click()

dash_duo.wait_for_text_to_equal("#output", "done")


def test_cbsc021_callback_running_non_existing_component(dash_duo):
lock = Lock()
app = Dash(__name__)

app.layout = html.Div(
[
html.Button("start", id="start"),
html.Div(id="output"),
]
)

@app.callback(
Output("output", "children"),
Input("start", "n_clicks"),
running=[
[
Output("non_existent_component", "children"),
html.B("on", id="content"),
"off",
]
],
prevent_initial_call=True,
)
def on_click(_):
with lock:
pass
return "done"

dash_duo.start_server(
app,
debug=True,
use_reloader=False,
use_debugger=True,
dev_tools_hot_reload=False,
)
with lock:
dash_duo.find_element("#start").click()

dash_duo.wait_for_text_to_equal("#output", "done")
error_title = "ID running component not found in layout"
error_message = [
"Component defined in running keyword not found in layout.",
'Component id: "non_existent_component"',
"This ID was used in the callback(s) for Output(s):",
"output.children",
"You can suppress this exception by setting",
"`suppress_callback_exceptions=True`.",
]
# The error should show twice, once for trying to set running on and once for
# turning it off.
dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "2")
for error in dash_duo.find_elements(".dash-fe-error__title"):
assert error.text == error_title
for error_text in dash_duo.find_elements(".dash-backend-error"):
assert all(line in error_text for line in error_message)

0 comments on commit bbd013c

Please sign in to comment.