Skip to content

Commit 8ff6cca

Browse files
committed
Add library loading capacity to _allow_dynamic_callbacks.
1 parent 78ebf0d commit 8ff6cca

File tree

7 files changed

+96
-14
lines changed

7 files changed

+96
-14
lines changed

dash/_callback.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
handle_grouped_callback_args,
1010
Output,
1111
)
12+
from .development.base_component import ComponentRegistry
1213
from .exceptions import (
1314
PreventUpdate,
1415
WildcardInLongCallback,
1516
MissingLongCallbackManagerError,
1617
LongCallbackError,
18+
ImportedInsideCallbackError,
1719
)
1820

1921
from ._grouping import (
@@ -332,7 +334,9 @@ def add_context(*args, **kwargs):
332334
output_spec = kwargs.pop("outputs_list")
333335
app_callback_manager = kwargs.pop("long_callback_manager", None)
334336
callback_ctx = kwargs.pop("callback_context", {})
337+
app = kwargs.pop("app", None)
335338
callback_manager = long and long.get("manager", app_callback_manager)
339+
original_packages = set(ComponentRegistry.registry)
336340
_validate.validate_output_spec(insert_output, output_spec, Output)
337341

338342
context_value.set(callback_ctx)
@@ -487,6 +491,18 @@ def add_context(*args, **kwargs):
487491

488492
response["response"] = component_ids
489493

494+
if len(ComponentRegistry.registry) != len(original_packages):
495+
diff_packages = list(
496+
set(ComponentRegistry.registry).difference(original_packages)
497+
)
498+
if not allow_dynamic_callbacks:
499+
raise ImportedInsideCallbackError(
500+
f"Component librar{'y' if len(diff_packages) == 1 else 'ies'} was imported during callback.\n"
501+
"You can set `_allow_dynamic_callbacks` to allow for development purpose only."
502+
)
503+
dist = app.get_dist(diff_packages)
504+
response["dist"] = dist
505+
490506
try:
491507
jsonResponse = to_json(response)
492508
except TypeError:

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {handlePatch, isPatch} from './patch';
4444
import {getPath} from './paths';
4545

4646
import {requestDependencies} from './requestDependencies';
47+
import {loadLibrary} from '../utils/libraries';
4748

4849
export const addBlockedCallbacks = createAction<IBlockedCallback[]>(
4950
CallbackActionType.AddBlocked
@@ -508,8 +509,15 @@ function handleServerside(
508509
}
509510

510511
if (!long || data.response !== undefined) {
511-
completeJob();
512-
finishLine(data);
512+
if (data.dist) {
513+
Promise.all(data.dist.map(loadLibrary)).then(() => {
514+
completeJob();
515+
finishLine(data);
516+
});
517+
} else {
518+
completeJob();
519+
finishLine(data);
520+
}
513521
} else {
514522
// Poll chain.
515523
setTimeout(

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,5 @@ export type CallbackResponseData = {
103103
running?: CallbackResponse;
104104
runningOff?: CallbackResponse;
105105
cancel?: ICallbackProperty[];
106+
dist?: any;
106107
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
type LibraryResource = {
2+
type: '_js_dist' | '_css_dist';
3+
url: string;
4+
};
5+
6+
export function loadLibrary(resource: LibraryResource) {
7+
let prom;
8+
const head = document.querySelector('head');
9+
if (resource.type === '_js_dist') {
10+
const element = document.createElement('script');
11+
element.src = resource.url;
12+
element.async = true;
13+
prom = new Promise<void>((resolve, reject) => {
14+
element.onload = () => {
15+
resolve();
16+
};
17+
element.onerror = error => reject(error);
18+
});
19+
20+
head?.appendChild(element);
21+
} else if (resource.type === '_css_dist') {
22+
const element = document.createElement('link');
23+
element.href = resource.url;
24+
element.rel = 'stylesheet';
25+
prom = new Promise<void>((resolve, reject) => {
26+
element.onload = () => {
27+
resolve();
28+
};
29+
element.onerror = error => reject(error);
30+
});
31+
head?.appendChild(element);
32+
}
33+
return prom;
34+
}

dash/dash.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -798,15 +798,14 @@ def serve_reload_hash(self):
798798
}
799799
)
800800

801-
def serve_dist(self):
802-
libraries = flask.request.get_json()
801+
def get_dist(self, libraries):
803802
dists = []
804803
for dist_type in ("_js_dist", "_css_dist"):
805804
resources = ComponentRegistry.get_resources(dist_type, libraries)
806805
srcs = self._collect_and_register_resources(resources, False)
807806
for src in srcs:
808807
dists.append(dict(type=dist_type, url=src))
809-
return flask.jsonify(dists)
808+
return dists
810809

811810
def _collect_and_register_resources(self, resources, include_async=True):
812811
# now needs the app context.
@@ -1262,8 +1261,6 @@ def long_callback(
12621261
def dispatch(self):
12631262
body = flask.request.get_json()
12641263

1265-
nlibs = len(ComponentRegistry.registry)
1266-
12671264
g = AttributeDict({})
12681265

12691266
g.inputs_list = inputs = body.get( # pylint: disable=assigning-non-slot
@@ -1293,7 +1290,6 @@ def dispatch(self):
12931290

12941291
try:
12951292
cb = self.callback_map[output]
1296-
_allow_dynamic = cb.get("allow_dynamic_callbacks", False)
12971293
func = cb["callback"]
12981294
g.background_callback_manager = (
12991295
cb.get("manager") or self._background_manager
@@ -1356,15 +1352,10 @@ def dispatch(self):
13561352
outputs_list=outputs_list,
13571353
long_callback_manager=self._background_manager,
13581354
callback_context=g,
1355+
app=self,
13591356
)
13601357
)
13611358
)
1362-
1363-
if not _allow_dynamic and nlibs != len(ComponentRegistry.registry):
1364-
print(
1365-
"Warning: component library imported during callback, move to top-level for full support.",
1366-
file=sys.stderr,
1367-
)
13681359
return response
13691360

13701361
def _setup_server(self):

dash/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,7 @@ class MissingLongCallbackManagerError(DashException):
9797

9898
class PageError(DashException):
9999
pass
100+
101+
102+
class ImportedInsideCallbackError(DashException):
103+
pass

tests/integration/callbacks/test_dynamic_callback.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,31 @@ def addition(_):
7373
dash_duo.wait_for_text_to_equal("#output", "add callbacks")
7474

7575
assert dash_duo.get_logs() == []
76+
77+
78+
def test_dyn003_dynamic_callback_import_library(dash_duo):
79+
app = Dash()
80+
app.layout = html.Div(
81+
[
82+
html.Button("insert", id="insert"),
83+
html.Div(id="output"),
84+
]
85+
)
86+
87+
@app.callback(
88+
Output("output", "children"),
89+
Input("insert", "n_clicks"),
90+
_allow_dynamic_callbacks=True,
91+
prevent_initial_call=True,
92+
)
93+
def on_click(_):
94+
import dash_test_components as dt
95+
96+
return dt.StyledComponent(
97+
value="inserted", id="inserted", style={"backgroundColor": "red"}
98+
)
99+
100+
dash_duo.start_server(app)
101+
102+
dash_duo.wait_for_element("#insert").click()
103+
dash_duo.wait_for_text_to_equal("#inserted", "inserted")

0 commit comments

Comments
 (0)