8000 Add separate persistance layer and /panel-preview/layout endpoint · holoviz/panel@e5a88b2 · GitHub
[go: up one dir, main page]

Skip to content

Commit e5a88b2

Browse files
committed
Add separate persistance layer and /panel-preview/layout endpoint
1 parent 90064cb commit e5a88b2

File tree

2 files changed

+70
-16
lines changed

2 files changed

+70
-16
lines changed

panel/io/handlers.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from __future__ import annotations
22

33
import ast
4+
import json
45
import logging
56
import os
7+
import pathlib
68
import re
79
import sys
810

@@ -211,6 +213,7 @@ def __init__(self, *, filename: PathLike, argv: list[str] = [], package: ModuleT
211213
filename (str) : a path to a Jupyter notebook (".ipynb") file
212214
213215
'''
216+
self._load_layout(filename)
214217
super().__init__(source=self._parse(filename), filename=filename)
215218
self._stale = False
216219

@@ -274,9 +277,23 @@ def __call__(self, nb, resources):
274277
md = ''.join(cell['source'])
275278
code.append(f'_pn__state._cell_outputs[{cell_id!r}].append("""{md}""")')
276279
code = '\n'.join(code)
280+
281+
for cell_id, layout in self._layout.get('cells', {}).items():
282+
state._cell_layouts[self][cell_id] = layout
283+
277284
self._nb = nb
278285
return code
279286

287+
def _load_layout(self, filename):
288+
nb_path = pathlib.Path(filename)
289+
layout_path = nb_path.parent / f'.{nb_path.name}.layout'
290+
if layout_path.is_file():
291+
with open(layout_path) as f:
292+
self._layout = json.load(f)
293+
else:
294+
self._layout = {}
295+
return self._layout
296+
280297
def modify_document(self, doc: Document) -> None:
281298
''' Run Bokeh application code to update a ``Document``
282299
@@ -286,6 +303,7 @@ def modify_document(self, doc: Document) -> None:
286303
'''
287304
path = self._runner._path
288305
if self._stale:
306+
self._load_layout(path)
289307
source = self._parse(path)
290308
nodes = ast.parse(source, os.fspath(path))
291309
self._runner._code = compile(nodes, filename=path, mode='exec', dont_inherit=True)
@@ -352,6 +370,11 @@ def modify_document(self, doc: Document) -> None:
352370
continue
353371
obj_id = cells[cell_id]
354372
ordered[obj_id] = layouts[obj_id]
373+
for cell_id in self._layout.get('order', []):
374+
if cell_id not in cells:
375+
continue
376+
obj_id = cells[cell_id]
377+
ordered[obj_id] = layouts[obj_id]
355378
for obj_id, spec in layouts.items():
356379
if obj_id not in ordered:
357380
ordered[obj_id] = spec
@@ -369,25 +392,31 @@ def _update_position_metadata(self, event):
369392
notebook and then overwrites notebook metadata with updated
370393
layout information.
371394
"""
372-
import nbformat
373395
nb = self._nb
374396
doc = event.obj._documents[-1]
375397
outputs = state._session_outputs[doc]
376-
cell_ids = {}
398+
cell_data, cell_ids = {}, {}
377399
for cell in nb['cells']:
378400
if cell['id'] in outputs:
379401
out = outputs[cell['id']]
380402
cell_ids[id(out)] = cell['id']
381403
spec = dict(event.new[id(out)])
382404
del spec['id']
383-
cell['metadata']['panel-layout'] = spec
384-
nb['metadata']['panel-cell-order'] = [cell_ids[obj_id] for obj_id in event.new]
385-
nbformat.write(nb, self._runner.path)
405+
cell_data[cell['id']] = spec
406+
order = [cell_ids[obj_id] for obj_id in event.new]
407+
nb_layout = {
408+
'cells': cell_data,
409+
'order': order
410+
}
411+
nb_path = pathlib.Path(self._runner.path)
412+
path = nb_path.parent / f'.{nb_path.name}.layout'
413+
with open(path, 'w') as f:
414+
json.dump(nb_layout, f)
386415
self._stale = True
387416

388417

389418
def build_single_handler_application(path, argv=None):
390-
if not os.path.isfile(path) or not (path.endswith(".md") or path.endswith(".ipynb")):
419+
if not os.path.isfile(path) or not path.endswith((".md", ".ipynb")):
391420
return _build_application(path, argv)
392421

393422
from .server import Application

panel/io/jupyter_server_extension.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,37 @@ def generate_executor(path: str, token: str, root_url: str) -> str:
145145
execute_template.render(path=path, token=token, root_url=root_url)
146146
)
147147

148-
class PanelJupyterHandler(JupyterHandler):
148+
class PanelBaseHandler(JupyterHandler):
149+
150+
def initialize(self, **kwargs):
151+
super().initialize(**kwargs)
152+
self.notebook_path = kwargs.pop('notebook_path', [])
153+
154+
def nb_path(self, path=None):
155+
root_dir = get_server_root_dir(self.application.settings)
156+
rel_path = pathlib.Path(self.notebook_path or path)
157+
if rel_path.is_absolute():
158+
notebook_path = str(rel_path)
159+
else:
160+
notebook_path = str((root_dir / rel_path).absolute())
161+
return pathlib.Path(notebook_path)
162+
163+
164+
class PanelLayoutHandler(PanelBaseHandler):
165+
166+
@tornado.web.authenticated
167+
async def get(self, path=None):
168+
nb_path = self.nb_path(path)
169+
layout_path = nb_path.parent / f'.{nb_path.name}.layout'
170+
if not layout_path.is_file():
171+
return {}
172+
with open(layout_path, encoding='utf-8') as f:
173+
layout = json.load(f)
174+
self.set_header('Content-Type', 'text/json')
175+
self.finish(layout)
176+
177+
178+
class PanelJupyterHandler(PanelBaseHandler):
149179
"""
150180
The PanelJupyterHandler expects to be given a path to a notebook,
151181
.py file or Lumen .yaml file. Based on the kernelspec in the
@@ -159,7 +189,6 @@ class PanelJupyterHandler(JupyterHandler):
159189

160190
def initialize(self, **kwargs):
161191
super().initialize(**kwargs)
162-
self.notebook_path = kwargs.pop('notebook_path', [])
163192
self.kernel_started = False
164193

165194
async def _get_info(self, msg_id, timeout=KERNEL_TIMEOUT):
@@ -193,13 +222,7 @@ async def _get_info(self, msg_id, timeout=KERNEL_TIMEOUT):
193222

194223
@tornado.web.authenticated
195224
async def get(self, path=None):
196-
root_dir = get_server_root_dir(self.application.settings)
197-
rel_path = pathlib.Path(self.notebook_path or path)
198-
if rel_path.is_absolute():
199-
notebook_path = str(rel_path)
200-
else:
201-
notebook_path = str((root_dir / rel_path).absolute())
202-
225+
notebook_path = self.nb_path(path)
203226
if (
204227
self.notebook_path and path
205228
): # when we are in single notebook mode but have a path
@@ -218,7 +241,7 @@ async def get(self, path=None):
218241
# Provision a kernel with the desired kernelspec
219242
if self.request.arguments.get('kernel'):
220243
requested_kernel = self.request.arguments.pop('kernel')[0].decode('utf-8')
221-
elif notebook_path.endswith('.ipynb'):
244+
elif notebook_path.suffix == '.ipynb':
222245
with open(notebook_path) as f:
223246
nb = json.load(f)
224247
requested_kernel = nb.get('metadata', {}).get('kernelspec', {}).get('name')
@@ -446,6 +469,8 @@ def _load_jupyter_server_extension(notebook_app):
446469
PanelWSProxy),
447470
(urljoin(base_url, r"panel-preview/render/(.*)"),
448471
PanelJupyterHandler, {}),
472+
(urljoin(base_url, r"panel-preview/layout/(.*)"),
473+
PanelLayoutHandler, {}),
449474
(urljoin(base_url, r"panel_dist/(.*)"),
450475
StaticFileHandler, dict(path=DIST_DIR)),
451476
(urljoin(base_url, f'panel-preview/{COMPONENT_PATH}(.*)'),

0 commit comments

Comments
 (0)
0