Skip to content

Commit d26c9d3

Browse files
committed
Add hooks for index modification and adding script and stylesheet resources.
1 parent d405c72 commit d26c9d3

File tree

5 files changed

+123
-36
lines changed

5 files changed

+123
-36
lines changed

dash/_hooks.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import flask as _f
55

66
from .exceptions import HookError
7-
7+
from .resources import ResourceType
88

99
if _t.TYPE_CHECKING:
1010
from .dash import Dash
@@ -38,7 +38,12 @@ def __init__(self) -> None:
3838
"routes": [],
3939
"error": [],
4040
"callback": [],
41+
"script": [],
42+
"stylesheet": [],
43+
"index": [],
4144
}
45+
self._js_dist = []
46+
self._css_dist = []
4247
self._finals = {}
4348

4449
def add_hook(
@@ -59,7 +64,7 @@ def add_hook(
5964
hks.append(_Hook(func, priority=priority, data=data))
6065
self._ns[hook] = sorted(hks, reverse=True, key=lambda h: h.priority)
6166

62-
def get_hooks(self, hook: str):
67+
def get_hooks(self, hook: str) -> _t.List[_Hook]:
6368
final = self._finals.get(hook, None)
6469
if final:
6570
final = [final]
@@ -140,6 +145,28 @@ def wrap(func):
140145

141146
return wrap
142147

148+
def script(self, distribution: _t.List[ResourceType]):
149+
"""Add js scripts to the page."""
150+
self._js_dist.extend(distribution)
151+
152+
def stylesheet(self, distribution: _t.List[ResourceType]):
153+
"""Add stylesheets to the page."""
154+
self._css_dist.extend(distribution)
155+
156+
def index(self, priority=None, final=False):
157+
"""Modify the index of the apps."""
158+
159+
def wrap(func):
160+
self.add_hook(
161+
"index",
162+
func,
163+
priority=priority,
164+
final=final,
165+
)
166+
return func
167+
168+
return wrap
169+
143170

144171
hooks = _Hooks()
145172

dash/dash.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -921,9 +921,13 @@ def _relative_url_path(relative_package_path="", namespace=""):
921921

922922
return srcs
923923

924+
# pylint: disable=protected-access
924925
def _generate_css_dist_html(self):
925926
external_links = self.config.external_stylesheets
926-
links = self._collect_and_register_resources(self.css.get_all_css())
927+
links = self._collect_and_register_resources(
928+
self.css.get_all_css()
929+
+ self.css._resources._filter_resources(self._hooks.hooks._css_dist)
930+
)
927931

928932
return "\n".join(
929933
[
@@ -972,6 +976,9 @@ def _generate_scripts_html(self):
972976
+ self.scripts._resources._filter_resources(
973977
dash_table._js_dist, dev_bundles=dev
974978
)
979+
+ self.scripts._resources._filter_resources(
980+
self._hooks.hooks._js_dist, dev_bundles=dev
981+
)
975982
)
976983
)
977984

@@ -1095,6 +1102,9 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument
10951102
renderer=renderer,
10961103
)
10971104

1105+
for hook in self._hooks.get_hooks("index"):
1106+
index = hook(index)
1107+
10981108
checks = (
10991109
_re_index_entry_id,
11001110
_re_index_config_id,

dash/resources.py

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,53 @@
22
import warnings
33
import os
44

5+
import typing as _t
6+
import typing_extensions as _tx
7+
8+
59
from .development.base_component import ComponentRegistry
610
from . import exceptions
711

812

13+
# ResourceType has `async` key, use the init form to be able to provide it.
14+
ResourceType = _tx.TypedDict(
15+
"ResourceType",
16+
{
17+
"namespace": str,
18+
"async": _t.Union[bool, _t.Literal["eager", "lazy"]],
19+
"dynamic": bool,
20+
"relative_package_path": str,
21+
"external_url": str,
22+
"dev_package_path": str,
23+
"absolute_path": str,
24+
"asset_path": str,
25+
"external_only": bool,
26+
"filepath": str,
27+
},
28+
total=False,
29+
)
30+
31+
32+
# pylint: disable=too-few-public-methods
33+
class ResourceConfig:
34+
def __init__(self, serve_locally, eager_loading):
35+
self.eager_loading = eager_loading
36+
self.serve_locally = serve_locally
37+
38+
939
class Resources:
10-
def __init__(self, resource_name):
11-
self._resources = []
40+
def __init__(self, resource_name: str, config: ResourceConfig):
41+
self._resources: _t.List[ResourceType] = []
1242
self.resource_name = resource_name
43+
self.config = config
1344

14-
def append_resource(self, resource):
45+
def append_resource(self, resource: ResourceType):
1546
self._resources.append(resource)
1647

1748
# pylint: disable=too-many-branches
18-
def _filter_resources(self, all_resources, dev_bundles=False):
49+
def _filter_resources(
50+
self, all_resources: _t.List[ResourceType], dev_bundles=False
51+
):
1952
filtered_resources = []
2053
for s in all_resources:
2154
filtered_resource = {}
@@ -45,7 +78,9 @@ def _filter_resources(self, all_resources, dev_bundles=False):
4578
)
4679
if "namespace" in s:
4780
filtered_resource["namespace"] = s["namespace"]
48-
if "external_url" in s and not self.config.serve_locally:
81+
if "external_url" in s and (
82+
s.get("external_only") or not self.config.serve_locally
83+
):
4984
filtered_resource["external_url"] = s["external_url"]
5085
elif "dev_package_path" in s and dev_bundles:
5186
filtered_resource["relative_package_path"] = s["dev_package_path"]
@@ -54,14 +89,14 @@ def _filter_resources(self, all_resources, dev_bundles=False):
5489
elif "absolute_path" in s:
5590
filtered_resource["absolute_path"] = s["absolute_path"]
5691
elif "asset_path" in s:
57-
info = os.stat(s["filepath"])
92+
info = os.stat(s["filepath"]) # type: ignore
5893
filtered_resource["asset_path"] = s["asset_path"]
5994
filtered_resource["ts"] = info.st_mtime
6095
elif self.config.serve_locally:
6196
warnings.warn(
6297
(
6398
"You have set your config to `serve_locally=True` but "
64-
f"A local version of {s['external_url']} is not available.\n"
99+
f"A local version of {s['external_url']} is not available.\n" # type: ignore
65100
"If you added this file with "
66101
"`app.scripts.append_script` "
67102
"or `app.css.append_css`, use `external_scripts` "
@@ -95,32 +130,25 @@ def get_library_resources(self, libraries, dev_bundles=False):
95130
return self._filter_resources(all_resources, dev_bundles)
96131

97132

98-
# pylint: disable=too-few-public-methods
99-
class _Config:
100-
def __init__(self, serve_locally, eager_loading):
101-
self.eager_loading = eager_loading
102-
self.serve_locally = serve_locally
103-
104-
105133
class Css:
106-
def __init__(self, serve_locally):
107-
self._resources = Resources("_css_dist")
108-
self._resources.config = self.config = _Config(serve_locally, True)
134+
def __init__(self, serve_locally: bool):
135+
self.config = ResourceConfig(serve_locally, True)
136+
self._resources = Resources("_css_dist", self.config)
109137

110-
def append_css(self, stylesheet):
138+
def append_css(self, stylesheet: ResourceType):
111139
self._resources.append_resource(stylesheet)
112140

113141
def get_all_css(self):
114142
return self._resources.get_all_resources()
115143

116-
def get_library_css(self, libraries):
144+
def get_library_css(self, libraries: _t.List[str]):
117145
return self._resources.get_library_resources(libraries)
118146

119147

120148
class Scripts:
121149
def __init__(self, serve_locally, eager):
122-
self._resources = Resources("_js_dist")
123-
self._resources.config = self.config = _Config(serve_locally, eager)
150+
self.config = ResourceConfig(serve_locally, eager)
151+
self._resources = Resources("_js_dist", self.config)
124152

125153
def append_script(self, script):
126154
self._resources.append_resource(script)

tests/integration/test_hooks.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,34 @@ def hook4(layout):
130130
dash_duo.wait_for_text_to_equal("#body > div:nth-child(3)", "first")
131131
dash_duo.wait_for_text_to_equal("#body > div:nth-child(4)", "second")
132132
dash_duo.wait_for_text_to_equal("#body > div:nth-child(5)", "third")
133+
134+
135+
def test_hook007_hook_index(hook_cleanup, dash_duo):
136+
@hooks.index()
137+
def hook_index(index: str):
138+
body = "<body>"
139+
ib = index.find(body) + len(body)
140+
injected = '<div id="hooked">Hooked</div>'
141+
new_index = index[ib:] + injected + index[: ib + 1]
142+
return new_index
143+
144+
app = Dash()
145+
app.layout = html.Div(["index"])
146+
147+
dash_duo.start_server(app)
148+
dash_duo.wait_for_text_to_equal("#hooked", "Hooked")
149+
150+
151+
def test_hook008_hook_distributions(hook_cleanup, dash_duo):
152+
js_uri = "https://example.com/none.js"
153+
css_uri = "https://example.com/none.css"
154+
hooks.script([{"external_url": js_uri, "external_only": True}])
155+
hooks.stylesheet([{"external_url": css_uri, "external_only": True}])
156+
157+
app = Dash()
158+
app.layout = html.Div("distribute")
159+
160+
dash_duo.start_server(app)
161+
162+
assert dash_duo.find_element(f'script[src="{js_uri}"]')
163+
assert dash_duo.find_element(f'link[href="{css_uri}"]')

tests/unit/library/test_async_resources.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
1-
from dash.resources import Resources
2-
3-
4-
class obj(object):
5-
def __init__(self, dict):
6-
self.__dict__ = dict
1+
from dash.resources import Resources, ResourceConfig
72

83

94
def test_resources_eager():
10-
11-
resource = Resources("js_test")
12-
resource.config = obj({"eager_loading": True, "serve_locally": False})
5+
resource = Resources("js_test", ResourceConfig(True, False))
136

147
filtered = resource._filter_resources(
158
[
@@ -32,9 +25,7 @@ def test_resources_eager():
3225

3326

3427
def test_resources_lazy():
35-
36-
resource = Resources("js_test")
37-
resource.config = obj({"eager_loading": False, "serve_locally": False})
28+
resource = Resources("js_test", ResourceConfig(False, False))
3829

3930
filtered = resource._filter_resources(
4031
[

0 commit comments

Comments
 (0)