holoviz/panel · #8787

fix: Keep notebook stylesheets inline after updates

philippjfr · merged Oct 2, 20265 files · 61 + / 12 −
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 = stylesheets
panel/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)