holoviz/panel · #8787
fix: Keep notebook stylesheets inline after updates
panel/io/notebook.py18 + / 6 −
@@ -513,19 +513,31 @@ def __init__(self, mimebundle): def _repr_mimebundle_(self, include=None, exclude=None): return self._mimebundle -def replace_inline_css(stylesheet: ImportedStyleSheet):+def replace_inline_css(+ stylesheet: ImportedStyleSheet, cache: dict[str, InlineStyleSheet] | None = None+) -> ImportedStyleSheet | InlineStyleSheet: if not stylesheet.url.startswith(CDN_DIST): return stylesheet- path = DIST_DIR / stylesheet.url.replace(CDN_DIST, '').split('?')[0] # type: ignore+ url = stylesheet.url.split('?')[0]+ if cache is not None and url in cache:+ return cache[url]+ path = DIST_DIR / url.replace(CDN_DIST, '') # type: ignore if not path.exists(): return stylesheet- return InlineStyleSheet(css=path.read_text(encoding='utf-8'))--def patch_inline_stylesheets(model: UIElement):+ inlined = InlineStyleSheet(css=path.read_text(encoding='utf-8'))+ if cache is not None:+ cache[url] = inlined+ return inlined++def patch_inline_stylesheets(model: UIElement, doc: Document | None = None):+ doc = doc or model.document+ # Registering the Document makes later updates, which re-resolve+ # stylesheets to CDN urls, inline them too (see Reactive._resolve_stylesheets).+ cache = None if doc is None else state._inline_stylesheets.setdefault(doc, {}) stylesheets = [] for sts in model.stylesheets: if isinstance(sts, ImportedStyleSheet):- sts = replace_inline_css(sts)+ sts = replace_inline_css(sts, cache) stylesheets.append(sts) if stylesheets != model.stylesheets: model.stylesheets = stylesheetspanel/io/state.py9 + / 1 −
@@ -43,7 +43,7 @@ from bokeh.application.application import SessionContext from bokeh.document import Document from bokeh.model import Model- from bokeh.models import ImportedStyleSheet+ from bokeh.models import ImportedStyleSheet, InlineStyleSheet from bokeh.server.contexts import ApplicationContext, BokehSessionContext from bokeh.server.session import ServerSession from IPython.display import DisplayHandle@@ -238,6 +238,10 @@ class _state(param.Parameterized): # Style cache _stylesheets: t.ClassVar[WeakKeyDictionary[Document, dict[str, ImportedStyleSheet]]] = WeakKeyDictionary() + # Inlined copies of the CDN stylesheets on Documents rendered with+ # inline resources, keyed by the url they replace.+ _inline_stylesheets: t.ClassVar[WeakKeyDictionary[Document, dict[str, InlineStyleSheet]]] = WeakKeyDictionary()+ # Loaded extensions _extensions_: t.ClassVar[WeakKeyDictionary[Document, list[str]]] = WeakKeyDictionary() @@ -520,6 +524,9 @@ def _destroy_session(self, session_context): if doc in self._stylesheets: del self._stylesheets[doc] + if doc in self._inline_stylesheets:+ del self._inline_stylesheets[doc]+ @property def _current_stack(self): current_thread = threading.current_thread()@@ -1065,6 +1072,7 @@ def reset(self): self._on_session_created.clear() self._on_session_destroyed.clear() self._stylesheets.clear()+ self._inline_stylesheets.clear() self._scheduled.clear() self._periodic.clear() panel/reactive.py6 + / 1 −
@@ -36,7 +36,7 @@ ) from .io.document import hold, unlocked-from .io.notebook import push+from .io.notebook import push, replace_inline_css from .io.resource_spec import lazy_load_available, resource_spec from .io.resources import ( CDN_DIST, get_dist_path, loading_css, patch_stylesheet, process_raw_css,@@ -714,6 +714,9 @@ def _resolve_stylesheets(self, properties: dict[str, t.Any], doc: Document | Non dist_url = doc._template_variables['dist_url'] else: dist_url = CDN_DIST+ # Documents rendered with inline resources, e.g. in a notebook, must+ # keep inlining updates since the CDN may not serve these files.+ inline_cache = state._inline_stylesheets.get(doc) if doc else None stylesheets = [] for stylesheet in properties['stylesheets']: if isinstance(stylesheet, ImportedStyleSheet):@@ -732,6 +735,8 @@ def _resolve_stylesheets(self, properties: dict[str, t.Any], doc: Document | Non stylesheet = ImportedStyleSheet(url=url) css_cache[url] = stylesheet patch_stylesheet(stylesheet, dist_url)+ if inline_cache is not None:+ stylesheet = replace_inline_css(stylesheet, inline_cache) stylesheets.append(stylesheet) properties['stylesheets'] = stylesheets panel/tests/io/test_notebook.py25 + / 1 −
@@ -20,7 +20,7 @@ from panel.models.echarts import ECharts from panel.models.perspective import Perspective from panel.models.tabulator import DataTabulator-from panel.pane import Str+from panel.pane import DataFrame, Str from panel.widgets import TextEditor from ..util import jb_available@@ -112,6 +112,30 @@ def test_notebook_inline_css_stylesheets(nb_loaded): for stylesheet in model.stylesheets[:len(model.__css__)]: assert isinstance(stylesheet, InlineStyleSheet) +def test_notebook_inline_css_stylesheets_kept_on_update(nb_loaded):+ pd = pytest.importorskip('pandas')+ pane = DataFrame(pd.DataFrame({'a': [1, 2]}))+ with config.set(inline=True):+ pane._repr_mimebundle_()+ model = list(pane._models.values())[0][0]+ stylesheets = list(model.stylesheets)+ assert not any(isinstance(sts, ImportedStyleSheet) for sts in stylesheets)++ pane.object = pd.DataFrame({'a': [3, 4]})++ assert model.stylesheets == stylesheets++def test_notebook_cdn_css_stylesheets_not_inlined_on_update(nb_loaded):+ pd = pytest.importorskip('pandas')+ pane = DataFrame(pd.DataFrame({'a': [1, 2]}))+ with config.set(inline=False):+ pane._repr_mimebundle_()+ model = list(pane._models.values())[0][0]++ pane.object = pd.DataFrame({'a': [3, 4]})++ assert not any(isinstance(sts, InlineStyleSheet) for sts in model.stylesheets)+ def test_notebook_inline_resources_shadow_amd_globals(monkeypatch, notebook_bootstrap): """panel/viewable.py3 + / 3 −
@@ -654,10 +654,10 @@ def _preprocess(self, root: Model, changed=None, old_models=None) -> None: """ ref = root.ref['id'] if changed is not None and ref in changed._models and ref in state._views:- _, _, _, comm = state._views[ref]+ _, _, doc, comm = state._views[ref] if comm is not None and config.inline: for model in changed._models[ref][0].select({'type': UIElement}):- patch_inline_stylesheets(model)+ patch_inline_stylesheets(model, doc) changed = self if changed is None else changed hooks = self._preprocessing_hooks+self._hooks@@ -893,7 +893,7 @@ def _repr_mimebundle_(self, include=None, exclude=None): for submodel in model.select({'type': UIElement}): if not isinstance(submodel, UIElement): continue- patch_inline_stylesheets(submodel)+ patch_inline_stylesheets(submodel, doc) bundle, meta = self._render_mimebundle(model, doc, comm, location)