Skip to content

Commit 6f45057

Browse files
dfguerrerompre-commit-ci[bot]trungleducmartinRenou
authored
Custom page config hook (#1495)
* set draft of customlabextension * add get_page_config hook * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * remove url param * remove unused conf param * add path to conf page * use the hook to modify the default get_page function * make page_config_hook optional * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update voila/app.py Co-authored-by: Duc Trung Le <[email protected]> * add server extension hoooks * add page_config_hook documentation * document hook * Move hooks to VoilaConfiguration * Add test * Fix rebasing issue --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Duc Trung Le <[email protected]> Co-authored-by: martinRenou <[email protected]>
1 parent f8dd6a5 commit 6f45057

File tree

9 files changed

+248
-39
lines changed

9 files changed

+248
-39
lines changed

docs/customize.md

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,16 @@ There is a Voilà template cookiecutter available to give you a running start.
226226
This cookiecutter contains some docker configuration for live reloading of your template changes to make development easier.
227227
Please refer to the [cookiecutter repo](https://github.com/voila-dashboards/voila-template-cookiecutter) for more information on how to use the Voilà template cookiecutter.
228228

229-
### Accessing the tornado request (`prelaunch-hook`)
229+
### Customizing Voila with Hooks
230+
231+
Voila provides hooks that allow you to customize its behavior to fit your specific needs. These hooks enable you to inject custom functions at certain points during Voila's execution, giving you control over aspects like notebook execution and frontend configuration.
232+
233+
Currently, Voila supports the following hooks:
234+
235+
- prelaunch_hook: Access and modify the Tornado request and notebook before execution.
236+
- page_config_hook: Customize the page_config object, which controls the Voila frontend configuration.
237+
238+
#### Accessing the tornado request (`prelaunch-hook`)
230239

231240
In certain custom setups when you need to access the tornado request object in order to check for authentication cookies, access details about the request headers, or modify the notebook before rendering. You can leverage the `prelaunch-hook`, which lets you inject a function to inspect the notebook and the request prior to executing them.
232241

@@ -240,7 +249,7 @@ Because `prelaunch-hook` only runs after receiving a new request but before the
240249
The format of this hook should be:
241250

242251
```python
243-
def hook(req: tornado.web.RequestHandler,
252+
def prelaunch_hook(req: tornado.web.RequestHandler,
244253
notebook: nbformat.NotebookNode,
245254
cwd: str) -> Optional[nbformat.NotebookNode]:
246255
```
@@ -250,6 +259,39 @@ def hook(req: tornado.web.RequestHandler,
250259
- The last argument is the current working directory should you need to mutate anything on disk.
251260
- The return value of your hook function can either be `None`, or a `NotebookNode`.
252261

262+
#### Customize the page config object (`page_config_hook`)
263+
264+
The page_config_hook allows you to customize the page_config object, which controls various aspects of the Voila frontend. This is useful when you need to modify frontend settings such as the URLs for static assets or other configuration parameters.
265+
266+
By default, Voila uses the following page_config:
267+
268+
```python
269+
# Default page_config
270+
page_config = {
271+
"appVersion": __version__,
272+
"appUrl": "voila/",
273+
"themesUrl": "/voila/api/themes",
274+
"baseUrl": base_url,
275+
"terminalsAvailable": False,
276+
"fullStaticUrl": url_path_join(base_url, "voila/static"),
277+
"fullLabextensionsUrl": url_path_join(base_url, "voila/labextensions"),
278+
"extensionConfig": voila_configuration.extension_config,
279+
}
280+
```
281+
282+
The format of this hook should be:
283+
284+
```python
285+
def page_config_hook(
286+
current_page_config: Dict[str, Any],
287+
base_url: str,
288+
settings: Dict[str, Any],
289+
log: Logger,
290+
voila_configuration: VoilaConfiguration,
291+
notebook_path: str
292+
) -> Dict[str, Any]:
293+
```
294+
253295
#### Adding the hook function to Voilà
254296

255297
There are two ways to add the hook function to Voilà:
@@ -259,16 +301,22 @@ There are two ways to add the hook function to Voilà:
259301
Here is an example of the configuration file. This file needs to be placed in the directory where you start Voilà.
260302

261303
```python
262-
def hook_function(req, notebook, cwd):
304+
def prelaunch_hook_function(req, notebook, cwd):
263305
"""Do your stuffs here"""
264306
return notebook
265307

266-
c.Voila.prelaunch_hook = hook_function
308+
def page_config_hook_function(current_page_config, **kwargs):
309+
"""Modify the current_page_config"""
310+
return new_page_config
311+
312+
c.VoilaConfiguration.prelaunch_hook = hook_function
313+
c.VoilaConfiguration.page_config_hook = page_config_hook
314+
267315
```
268316

269317
- Start Voilà from a python script:
270318

271-
Here is an example of a custom `prelaunch-hook` to execute a notebook with `papermill`:
319+
Here is an example of a custom `prelaunch-hook` to execute a notebook with `papermill`, and a `page_config_hook` to add a custom labextensions URL:
272320

273321
```python
274322
def parameterize_with_papermill(req, notebook, cwd):
@@ -302,9 +350,22 @@ def parameterize_with_papermill(req, notebook, cwd):
302350

303351
# Parameterize with papermill
304352
return parameterize_notebook(notebook, parameters)
353+
354+
355+
def page_config_hook(
356+
current_page_config: Dict[str, Any],
357+
base_url: str,
358+
settings: Dict[str, Any],
359+
log: Logger,
360+
voila_configuration: VoilaConfiguration,
361+
notebook_path: str
362+
):
363+
page_config['fullLabextensionsUrl'] = '/custom/labextensions_url'
364+
return page_config
365+
305366
```
306367

307-
To add this hook to your `Voilà` application:
368+
You can use both hooks simultaneously to customize notebook execution and frontend configuration, to add this hooks to your `Voilà` application:
308369

309370
```python
310371
from voila.app import Voila
@@ -313,15 +374,18 @@ from voila.config import VoilaConfiguration
313374
# customize config how you like
314375
config = VoilaConfiguration()
315376

377+
# set the prelaunch hook
378+
config.prelaunch_hook = parameterize_with_papermill
379+
380+
# set the page config hook
381+
config.page_config_hook = page_config_hook
382+
316383
# create a voila instance
317384
app = Voila()
318385

319386
# set the config
320387
app.voila_configuration = config
321388

322-
# set the prelaunch hook
323-
app.prelaunch_hook = parameterize_with_papermill
324-
325389
# launch
326390
app.start()
327391
```

tests/app/page_config_hook_test.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import os
2+
3+
import pytest
4+
5+
6+
BASE_DIR = os.path.dirname(__file__)
7+
8+
9+
@pytest.fixture
10+
def voila_notebook(notebook_directory):
11+
return os.path.join(notebook_directory, "print.ipynb")
12+
13+
14+
@pytest.fixture
15+
def voila_config():
16+
def foo(current_page_config, **kwargs):
17+
current_page_config["foo"] = "my custom config"
18+
return current_page_config
19+
20+
def config(app):
21+
app.voila_configuration.page_config_hook = foo
22+
23+
return config
24+
25+
26+
async def test_prelaunch_hook(http_server_client, base_url):
27+
response = await http_server_client.fetch(base_url)
28+
assert response.code == 200
29+
assert "my custom config" in response.body.decode("utf-8")

voila/app.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,16 @@ def hook(req: tornado.web.RequestHandler,
352352
),
353353
)
354354

355+
@validate("prelaunch_hook")
356+
def _valid_prelaunch_hook(self, proposal):
357+
warn(
358+
"Voila.prelaunch_hook is deprecated, please use VoilaConfiguration.prelaunch_hook instead",
359+
DeprecationWarning,
360+
stacklevel=2,
361+
)
362+
self.voila_configuration.prelaunch_hook = proposal["value"]
363+
return proposal["value"]
364+
355365
if JUPYTER_SERVER_2:
356366
cookie_secret = Bytes(
357367
b"",
@@ -611,7 +621,7 @@ def init_settings(self) -> Dict:
611621
preheat_kernel: bool = self.voila_configuration.preheat_kernel
612622
pool_size: int = self.voila_configuration.default_pool_size
613623

614-
if preheat_kernel and self.prelaunch_hook:
624+
if preheat_kernel and self.voila_configuration.prelaunch_hook:
615625
raise Exception("`preheat_kernel` and `prelaunch_hook` are incompatible")
616626

617627
progressive_rendering = self.voila_configuration.progressive_rendering
@@ -629,6 +639,7 @@ def init_settings(self) -> Dict:
629639
self.voila_configuration.multi_kernel_manager_class,
630640
preheat_kernel,
631641
pool_size,
642+
page_config_hook=self.voila_configuration.page_config_hook,
632643
)
633644
self.kernel_manager = kernel_manager_class(
634645
parent=self,
@@ -812,19 +823,22 @@ def init_handlers(self) -> List:
812823
"template_paths": self.template_paths,
813824
"config": self.config,
814825
"voila_configuration": self.voila_configuration,
815-
"prelaunch_hook": self.prelaunch_hook,
816826
},
817827
)
818828
)
819829
else:
820830
self.log.debug("serving directory: %r", self.root_dir)
821831
handlers.extend(
822832
[
823-
(self.server_url, TornadoVoilaTreeHandler, tree_handler_conf),
833+
(
834+
self.server_url,
835+
TornadoVoilaTreeHandler,
836+
{"voila_configuration": self.voila_configuration},
837+
),
824838
(
825839
url_path_join(self.server_url, r"/voila/tree" + path_regex),
826840
TornadoVoilaTreeHandler,
827-
tree_handler_conf,
841+
{"voila_configuration": self.voila_configuration},
828842
),
829843
(
830844
url_path_join(self.server_url, r"/voila/render/(.*)"),
@@ -833,7 +847,6 @@ def init_handlers(self) -> List:
833847
"template_paths": self.template_paths,
834848
"config": self.config,
835849
"voila_configuration": self.voila_configuration,
836-
"prelaunch_hook": self.prelaunch_hook,
837850
},
838851
),
839852
# On serving a directory, expose the content handler.

voila/configuration.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
#############################################################################
99

1010
import traitlets.config
11-
from traitlets import Bool, Dict, Enum, Int, List, Type, Unicode, validate
11+
from traitlets import Bool, Callable, Dict, Enum, Int, List, Type, Unicode, validate
1212

1313
from warnings import warn
1414

@@ -218,6 +218,50 @@ def _valid_file_blacklist(self, proposal):
218218
help="""Whether or not voila should attempt to fix and resolve a notebooks kernelspec metadata""",
219219
)
220220

221+
prelaunch_hook = Callable(
222+
default_value=None,
223+
allow_none=True,
224+
config=True,
225+
help="""A function that is called prior to the launch of a new kernel instance
226+
when a user visits the voila webpage. Used for custom user authorization
227+
or any other necessary pre-launch functions.
228+
229+
Should be of the form:
230+
231+
def hook(req: tornado.web.RequestHandler,
232+
notebook: nbformat.NotebookNode,
233+
cwd: str)
234+
235+
Although most customizations can leverage templates, if you need access
236+
to the request object (e.g. to inspect cookies for authentication),
237+
or to modify the notebook itself (e.g. to inject some custom structure,
238+
although much of this can be done by interacting with the kernel
239+
in javascript) the prelaunch hook lets you do that.
240+
""",
241+
)
242+
243+
page_config_hook = Callable(
244+
default_value=None,
245+
allow_none=True,
246+
config=True,
247+
help="""A function that is called to modify the page config for a given notebook.
248+
Should be of the form:
249+
250+
def page_config_hook(
251+
current_page_config: Dict[str, Any],
252+
base_url: str,
253+
settings: Dict[str, Any],
254+
log: Logger,
255+
voila_configuration: VoilaConfiguration,
256+
notebook_path: str
257+
) -> Dict[str, Any]:
258+
259+
The hook receives the default page_config dictionary and all its parameters, it should
260+
return a dictionary that will be passed to the template as the `page_config` variable
261+
and the NotebookRenderer. This can be used to pass custom configuration.
262+
""",
263+
)
264+
221265
progressive_rendering = Bool(
222266
False,
223267
config=True,

voila/handler.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ def initialize(self, **kwargs):
7474
self.template_paths = kwargs.pop("template_paths", [])
7575
self.traitlet_config = kwargs.pop("config", None)
7676
self.voila_configuration: VoilaConfiguration = kwargs["voila_configuration"]
77-
self.prelaunch_hook = kwargs.get("prelaunch_hook", None)
77+
self.prelaunch_hook = self.voila_configuration.prelaunch_hook
78+
self.page_config_hook = self.voila_configuration.page_config_hook
79+
7880
# we want to avoid starting multiple kernels due to template mistakes
7981
self.kernel_started = False
8082

@@ -188,6 +190,23 @@ async def get_generator(self, path=None):
188190
return
189191
mathjax_config = self.settings.get("mathjax_config")
190192
mathjax_url = self.settings.get("mathjax_url")
193+
194+
page_config_kwargs = {
195+
"base_url": self.base_url,
196+
"settings": self.settings,
197+
"log": self.log,
198+
"voila_configuration": self.voila_configuration,
199+
}
200+
201+
page_config = get_page_config(**page_config_kwargs)
202+
203+
if self.page_config_hook:
204+
page_config = self.page_config_hook(
205+
page_config,
206+
**page_config_kwargs,
207+
notebook_path=notebook_path,
208+
)
209+
191210
gen = NotebookRenderer(
192211
request_handler=self,
193212
voila_configuration=self.voila_configuration,
@@ -199,12 +218,7 @@ async def get_generator(self, path=None):
199218
base_url=self.base_url,
200219
kernel_spec_manager=self.kernel_spec_manager,
201220
prelaunch_hook=self.prelaunch_hook,
202-
page_config=get_page_config(
203-
base_url=self.base_url,
204-
settings=self.settings,
205-
log=self.log,
206-
voila_configuration=self.voila_configuration,
207-
),
221+
page_config=page_config,
208222
mathjax_config=mathjax_config,
209223
mathjax_url=mathjax_url,
210224
)

voila/notebook_renderer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def __init__(self, **kwargs):
4545
self.config_manager = kwargs.get("config_manager")
4646
self.contents_manager = kwargs.get("contents_manager")
4747
self.kernel_spec_manager = kwargs.get("kernel_spec_manager")
48-
self.prelaunch_hook = kwargs.get("prelaunch_hook")
48+
self.prelaunch_hook = self.voila_configuration.prelaunch_hook
4949
self.base_url = kwargs.get("base_url")
5050
self.page_config = deepcopy(kwargs.get("page_config"))
5151
self.default_kernel_name = "python3"

voila/tornado/treehandler.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121

2222

2323
class TornadoVoilaTreeHandler(VoilaTreeHandler):
24+
def initialize(self, **kwargs):
25+
super().initialize(**kwargs)
26+
self.page_config_hook = self.voila_configuration.page_config_hook
27+
2428
@web.authenticated
2529
async def get(self, path=""):
2630
cm = self.contents_manager
@@ -58,12 +62,22 @@ def allowed_content(content):
5862

5963
theme_arg = self.validate_theme(theme_arg, classic_tree)
6064

61-
page_config = get_page_config(
62-
base_url=self.base_url,
63-
settings=self.settings,
64-
log=self.log,
65-
voila_configuration=self.voila_configuration,
66-
)
65+
page_config_kwargs = {
66+
"base_url": self.base_url,
67+
"settings": self.settings,
68+
"log": self.log,
69+
"voila_configuration": self.voila_configuration,
70+
}
71+
72+
page_config = get_page_config(**page_config_kwargs)
73+
74+
if self.page_config_hook:
75+
self.page_config_hook(
76+
page_config,
77+
**page_config_kwargs,
78+
notebook_path=path,
79+
)
80+
6781
page_config["jupyterLabTheme"] = theme_arg
6882
page_config["frontend"] = "voila"
6983
page_config["query"] = self.request.query

0 commit comments

Comments
 (0)