Skip to content

Commit 97f0fdc

Browse files
authored
Merge pull request #2649 from plotly/feat/dynamic-callback-creation
dynamic callback creation
2 parents d9b8d17 + e7b2088 commit 97f0fdc

File tree

9 files changed

+122
-40
lines changed

9 files changed

+122
-40
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ This project adheres to [Semantic Versioning](https://semver.org/).
1212

1313
- [#2635](https://github.com/plotly/dash/pull/2635) Get proper app module name, remove need to give `__name__` to Dash constructor.
1414

15+
## Added
16+
17+
- [#2649](https://github.com/plotly/dash/pull/2649) Add `_allow_dynamic_callbacks`, register new callbacks inside other callbacks.
18+
**WARNING: dynamic callback creation can be dangerous, use at you own risk. It is not intended for use in a production app, multi-user or multiprocess use as it only works for a single user.**
19+
1520
## [2.13.0] 2023-08-28
1621
## Changed
1722

dash/_callback.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ def clientside_callback(clientside_function, *args, **kwargs):
210210
)
211211

212212

213+
# pylint: disable=too-many-arguments
213214
def insert_callback(
214215
callback_list,
215216
callback_map,
@@ -222,6 +223,7 @@ def insert_callback(
222223
prevent_initial_call,
223224
long=None,
224225
manager=None,
226+
dynamic_creator=False,
225227
):
226228
if prevent_initial_call is None:
227229
prevent_initial_call = config_prevent_initial_callbacks
@@ -243,6 +245,7 @@ def insert_callback(
243245
and {
244246
"interval": long["interval"],
245247
},
248+
"dynamic_creator": dynamic_creator,
246249
}
247250

248251
callback_map[callback_id] = {
@@ -282,6 +285,7 @@ def register_callback( # pylint: disable=R0914
282285

283286
long = _kwargs.get("long")
284287
manager = _kwargs.get("manager")
288+
allow_dynamic_callbacks = _kwargs.get("_allow_dynamic_callbacks")
285289

286290
output_indices = make_grouping_by_index(output, list(range(grouping_len(output))))
287291
callback_id = insert_callback(
@@ -296,6 +300,7 @@ def register_callback( # pylint: disable=R0914
296300
prevent_initial_call,
297301
long=long,
298302
manager=manager,
303+
dynamic_creator=allow_dynamic_callbacks,
299304
)
300305

301306
# pylint: disable=too-many-locals

dash/dash-renderer/src/TreeContainer.js

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
validateComponent
3636
} from './utils/TreeContainer';
3737
import {DashContext} from './APIController.react';
38+
import {batch} from 'react-redux';
3839

3940
const NOT_LOADING = {
4041
is_loading: false
@@ -130,48 +131,50 @@ class BaseTreeContainer extends Component {
130131
}
131132

132133
setProps(newProps) {
133-
const {
134-
_dashprivate_graphs,
135-
_dashprivate_dispatch,
136-
_dashprivate_path,
137-
_dashprivate_layout
138-
} = this.props;
134+
const {_dashprivate_dispatch, _dashprivate_path, _dashprivate_layout} =
135+
this.props;
139136

140137
const oldProps = this.getLayoutProps();
141138
const {id} = oldProps;
142139
const changedProps = pickBy(
143140
(val, key) => !equals(val, oldProps[key]),
144141
newProps
145142
);
146-
if (!isEmpty(changedProps)) {
147-
// Identify the modified props that are required for callbacks
148-
const watchedKeys = getWatchedKeys(
149-
id,
150-
keys(changedProps),
151-
_dashprivate_graphs
152-
);
153143

154-
// setProps here is triggered by the UI - record these changes
155-
// for persistence
156-
recordUiEdit(_dashprivate_layout, newProps, _dashprivate_dispatch);
157-
158-
// Only dispatch changes to Dash if a watched prop changed
159-
if (watchedKeys.length) {
160-
_dashprivate_dispatch(
161-
notifyObservers({
162-
id,
163-
props: pick(watchedKeys, changedProps)
164-
})
144+
if (!isEmpty(changedProps)) {
145+
_dashprivate_dispatch((dispatch, getState) => {
146+
const {graphs} = getState();
147+
// Identify the modified props that are required for callbacks
148+
const watchedKeys = getWatchedKeys(
149+
id,
150+
keys(changedProps),
151+
graphs
165152
);
166-
}
167153

168-
// Always update this component's props
169-
_dashprivate_dispatch(
170-
updateProps({
171-
props: changedProps,
172-
itempath: _dashprivate_path
173-
})
174-
);
154+
batch(() => {
155+
// setProps here is triggered by the UI - record these changes
156+
// for persistence
157+
recordUiEdit(_dashprivate_layout, newProps, dispatch);
158+
159+
// Only dispatch changes to Dash if a watched prop changed
160+
if (watchedKeys.length) {
161+
dispatch(
162+
notifyObservers({
163+
id,
164+
props: pick(watchedKeys, changedProps)
165+
})
166+
);
167+
}
168+
169+
// Always update this component's props
170+
dispatch(
171+
updateProps({
172+
props: changedProps,
173+
itempath: _dashprivate_path
174+
})
175+
);
176+
});
177+
});
175178
}
176179
}
177180

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import {CallbackJobPayload} from '../reducers/callbackJobs';
4343
import {handlePatch, isPatch} from './patch';
4444
import {getPath} from './paths';
4545

46+
import {requestDependencies} from './requestDependencies';
47+
4648
export const addBlockedCallbacks = createAction<IBlockedCallback[]>(
4749
CallbackActionType.AddBlocked
4850
);
@@ -584,7 +586,8 @@ export function executeCallback(
584586
dispatch: any,
585587
getState: any
586588
): IExecutingCallback {
587-
const {output, inputs, state, clientside_function, long} = cb.callback;
589+
const {output, inputs, state, clientside_function, long, dynamic_creator} =
590+
cb.callback;
588591
try {
589592
const inVals = fillVals(paths, layout, cb, inputs, 'Input', true);
590593

@@ -728,6 +731,13 @@ export function executeCallback(
728731
}
729732
});
730733

734+
if (dynamic_creator) {
735+
setTimeout(
736+
() => dispatch(requestDependencies()),
737+
0
738+
);
739+
}
740+
731741
return {data, payload};
732742
} catch (res: any) {
733743
lastError = res;

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,18 @@ import {
1818
} from 'ramda';
1919
import {
2020
ICallback,
21-
ICallbackProperty,
2221
ICallbackDefinition,
23-
ILayoutCallbackProperty,
24-
ICallbackTemplate
22+
ICallbackProperty,
23+
ICallbackTemplate,
24+
ILayoutCallbackProperty
2525
} from '../types/callbacks';
2626
import {
2727
addAllResolvedFromOutputs,
28-
splitIdAndProp,
29-
stringifyId,
3028
getUnfilteredLayoutCallbacks,
29+
idMatch,
3130
isMultiValued,
32-
idMatch
31+
splitIdAndProp,
32+
stringifyId
3333
} from './dependencies';
3434
import {getPath} from './paths';
3535

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {batch} from 'react-redux';
2+
import {setGraphs} from './index';
3+
import apiThunk from './api';
4+
5+
export function requestDependencies() {
6+
return (dispatch: any) => {
7+
batch(() => {
8+
dispatch(setGraphs({}));
9+
dispatch(
10+
apiThunk('_dash-dependencies', 'GET', 'dependenciesRequest')
11+
);
12+
});
13+
};
14+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface ICallbackDefinition {
1212
prevent_initial_call: boolean;
1313
state: ICallbackProperty[];
1414
long?: LongCallbackInfo;
15+
dynamic_creator?: boolean;
1516
}
1617

1718
export interface ICallbackProperty {

requires-ci.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ flask-talisman==1.0.0
99
isort==4.3.21;python_version<"3.7"
1010
mimesis
1111
mock==4.0.3
12-
numpy
12+
numpy<=1.25.2
1313
orjson==3.5.4;python_version<"3.7"
1414
orjson==3.6.7;python_version>="3.7"
1515
openpyxl;python_version>="3.8"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from dash import html, Dash, Input, Output
2+
3+
4+
# WARNING: dynamic callback creation can be dangerous, use at you own risk.
5+
# It is not intended for use in a production app, multi-user
6+
# or multiprocess use as it only works for a single user.
7+
def test_dync001_dynamic_callback(dash_duo):
8+
app = Dash()
9+
10+
app.layout = html.Div(
11+
[
12+
html.Div(id="output"),
13+
html.Button("create", id="create"),
14+
html.Div("initial", id="output-2"),
15+
html.Button("dynamic", id="dynamic"),
16+
]
17+
)
18+
19+
@app.callback(
20+
Output("output", "children"),
21+
Input("create", "n_clicks"),
22+
_allow_dynamic_callbacks=True,
23+
prevent_initial_call=True,
24+
)
25+
def on_click(n_clicks):
26+
@app.callback(
27+
Output("output-2", "children"),
28+
Input("dynamic", "n_clicks"),
29+
prevent_initial_call=True,
30+
)
31+
def on_click2(n_clicks2):
32+
return f"Dynamic clicks {n_clicks2}"
33+
34+
return f"creator {n_clicks}"
35+
36+
dash_duo.start_server(app)
37+
38+
dash_duo.wait_for_element("#dynamic").click()
39+
dash_duo.wait_for_element("#create").click()
40+
dash_duo.wait_for_text_to_equal("#output", "creator 1")
41+
dash_duo.wait_for_text_to_equal("#output-2", "initial")
42+
43+
dash_duo.wait_for_element("#dynamic").click()
44+
dash_duo.wait_for_text_to_equal("#output-2", "Dynamic clicks 2")

0 commit comments

Comments
 (0)