Skip to content

Commit 870c661

Browse files
authored
Merge pull request #2789 from plotly/load-inside-callback
Add library loading capacity to `_allow_dynamic_callbacks`.
2 parents 0ef64b9 + 025107a commit 870c661

File tree

8 files changed

+101
-14
lines changed

8 files changed

+101
-14
lines changed

CHANGELOG.md

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

99
- [#2881](https://github.com/plotly/dash/pull/2881) Add outputs_list to window.dash_clientside.callback_context. Fixes [#2877](https://github.com/plotly/dash/issues/2877).
1010
- [#2936](https://github.com/plotly/dash/pull/2936) Adds support for TypeScript 5.5+.
11+
- [#2789](https://github.com/plotly/dash/pull/2789) Add library loading capacity to `_allow_dynamic_callbacks`
1112

1213
## Fixed
1314

@@ -44,6 +45,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
4445
- `target_components` specifies components/props triggering the loading spinner
4546
- `custom_spinner` enables using a custom component for loading messages instead of built-in spinners
4647
- `display` overrides the loading status with options for "show," "hide," or "auto"
48+
4749
- [#2822](https://github.com/plotly/dash/pull/2822) Support no output callbacks. Fixes [#1549](https://github.com/plotly/dash/issues/1549)
4850
- [#2822](https://github.com/plotly/dash/pull/2822) Add global set_props. Fixes [#2803](https://github.com/plotly/dash/issues/2803)
4951

dash/_callback.py

+17
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
handle_grouped_callback_args,
1111
Output,
1212
)
13+
from .development.base_component import ComponentRegistry
1314
from .exceptions import (
1415
InvalidCallbackReturnValue,
1516
PreventUpdate,
1617
WildcardInLongCallback,
1718
MissingLongCallbackManagerError,
1819
LongCallbackError,
20+
ImportedInsideCallbackError,
1921
)
2022

2123
from ._grouping import (
@@ -354,11 +356,14 @@ def wrap_func(func):
354356
def add_context(*args, **kwargs):
355357
output_spec = kwargs.pop("outputs_list")
356358
app_callback_manager = kwargs.pop("long_callback_manager", None)
359+
357360
callback_ctx = kwargs.pop(
358361
"callback_context", AttributeDict({"updated_props": {}})
359362
)
363+
app = kwargs.pop("app", None)
360364
callback_manager = long and long.get("manager", app_callback_manager)
361365
error_handler = on_error or kwargs.pop("app_on_error", None)
366+
original_packages = set(ComponentRegistry.registry)
362367

363368
if has_output:
364369
_validate.validate_output_spec(insert_output, output_spec, Output)
@@ -557,6 +562,18 @@ def add_context(*args, **kwargs):
557562

558563
response["response"] = component_ids
559564

565+
if len(ComponentRegistry.registry) != len(original_packages):
566+
diff_packages = list(
567+
set(ComponentRegistry.registry).difference(original_packages)
568+
)
569+
if not allow_dynamic_callbacks:
570+
raise ImportedInsideCallbackError(
571+
f"Component librar{'y' if len(diff_packages) == 1 else 'ies'} was imported during callback.\n"
572+
"You can set `_allow_dynamic_callbacks` to allow for development purpose only."
573+
)
574+
dist = app.get_dist(diff_packages)
575+
response["dist"] = dist
576+
560577
try:
561578
jsonResponse = to_json(response)
562579
except TypeError:

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

+12-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ import {handlePatch, isPatch} from './patch';
4545
import {getPath} from './paths';
4646

4747
import {requestDependencies} from './requestDependencies';
48+
49+
import {loadLibrary} from '../utils/libraries';
50+
4851
import {parsePMCId} from './patternMatching';
4952
import {replacePMC} from './patternMatching';
5053

@@ -576,8 +579,15 @@ function handleServerside(
576579
}
577580

578581
if (!long || data.response !== undefined) {
579-
completeJob();
580-
finishLine(data);
582+
if (data.dist) {
583+
Promise.all(data.dist.map(loadLibrary)).then(() => {
584+
completeJob();
585+
finishLine(data);
586+
});
587+
} else {
588+
completeJob();
589+
finishLine(data);
590+
}
581591
} else {
582592
// Poll chain.
583593
setTimeout(

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

+1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export type CallbackResponseData = {
105105
running?: CallbackResponse;
106106
runningOff?: CallbackResponse;
107107
cancel?: ICallbackProperty[];
108+
dist?: any;
108109
sideUpdate?: any;
109110
};
110111

+34
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

+3-12
Original file line numberDiff line numberDiff line change
@@ -789,15 +789,14 @@ def serve_reload_hash(self):
789789
}
790790
)
791791

792-
def serve_dist(self):
793-
libraries = flask.request.get_json()
792+
def get_dist(self, libraries):
794793
dists = []
795794
for dist_type in ("_js_dist", "_css_dist"):
796795
resources = ComponentRegistry.get_resources(dist_type, libraries)
797796
srcs = self._collect_and_register_resources(resources, False)
798797
for src in srcs:
799798
dists.append(dict(type=dist_type, url=src))
800-
return flask.jsonify(dists)
799+
return dists
801800

802801
def _collect_and_register_resources(self, resources, include_async=True):
803802
# now needs the app context.
@@ -1254,8 +1253,6 @@ def long_callback(
12541253
def dispatch(self):
12551254
body = flask.request.get_json()
12561255

1257-
nlibs = len(ComponentRegistry.registry)
1258-
12591256
g = AttributeDict({})
12601257

12611258
g.inputs_list = inputs = body.get( # pylint: disable=assigning-non-slot
@@ -1285,7 +1282,6 @@ def dispatch(self):
12851282

12861283
try:
12871284
cb = self.callback_map[output]
1288-
_allow_dynamic = cb.get("allow_dynamic_callbacks", False)
12891285
func = cb["callback"]
12901286
g.background_callback_manager = (
12911287
cb.get("manager") or self._background_manager
@@ -1361,16 +1357,11 @@ def dispatch(self):
13611357
outputs_list=outputs_list,
13621358
long_callback_manager=self._background_manager,
13631359
callback_context=g,
1360+
app=self,
13641361
app_on_error=self._on_error,
13651362
)
13661363
)
13671364
)
1368-
1369-
if not _allow_dynamic and nlibs != len(ComponentRegistry.registry):
1370-
print(
1371-
"Warning: component library imported during callback, move to top-level for full support.",
1372-
file=sys.stderr,
1373-
)
13741365
return response
13751366

13761367
def _setup_server(self):

dash/exceptions.py

+4
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

+28
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)