diff --git a/panel/io/server.py b/panel/io/server.py index 3e56f9ba95..b951562b3d 100644 --- a/panel/io/server.py +++ b/panel/io/server.py @@ -139,6 +139,8 @@ def _eval_panel( """) ) + doc.on_event('document_ready', partial(state._schedule_on_load, doc)) + # Set up instrumentation for logging sessions logger.info(LOG_SESSION_LAUNCHING, id(doc)) def _log_session_destroyed(session_context): @@ -771,6 +773,8 @@ def modify_document(self, doc: 'Document'): logger.info(LOG_SESSION_LAUNCHING, id(doc)) + doc.on_event('document_ready', partial(state._schedule_on_load, doc)) + if config.autoreload: path = self._runner.path argv = self._runner._argv diff --git a/panel/io/state.py b/panel/io/state.py index d610c7497c..09b60a8e42 100644 --- a/panel/io/state.py +++ b/panel/io/state.py @@ -376,8 +376,7 @@ def _schedule_on_load(self, doc: Document, event) -> None: def _on_load(self, doc: Optional[Document] = None) -> None: doc = doc or self.curdoc - callbacks = self._onload.pop(doc, []) - if not callbacks: + if doc not in self._onload: self._loaded[doc] = True return @@ -385,13 +384,16 @@ def _on_load(self, doc: Optional[Document] = None) -> None: from .profile import profile_ctx with set_curdoc(doc): if (doc and doc in self._launching) or not config.profiler: - for cb, threaded in callbacks: - self.execute(cb, schedule='thread' if threaded else False) + while doc in self._onload: + for cb, threaded in self._onload.pop(doc): + self.execute(cb, schedule='thread' if threaded else False) + self._loaded[doc] = True return with profile_ctx(config.profiler) as sessions: - for cb, threaded in callbacks: - self.execute(cb, schedule='thread' if threaded else False) + while doc in self._onload: + for cb, threaded in self._onload.pop(doc): + self.execute(cb, schedule='thread' if threaded else False) path = doc.session_context.request.path self._profiles[(path+':on_load', config.profiler)] += sessions self.param.trigger('_profiles') @@ -680,7 +682,7 @@ def onload(self, callback: Callable[[], None | Awaitable[None]] | Coroutine[Any, threaded: bool Whether the onload callback can be threaded """ - if self.curdoc is None or self._is_pyodide: + if self.curdoc is None or self._is_pyodide or self.loaded: if self._thread_pool: future = self._thread_pool.submit(partial(self.execute, callback, schedule=False)) future.add_done_callback(self._handle_future_exception) @@ -689,10 +691,6 @@ def onload(self, callback: Callable[[], None | Awaitable[None]] | Coroutine[Any, return elif self.curdoc not in self._onload: self._onload[self.curdoc] = [] - try: - self.curdoc.on_event('document_ready', partial(self._schedule_on_load, self.curdoc)) - except AttributeError: - pass # Document already cleaned up self._onload[self.curdoc].append((callback, threaded)) def on_session_created(self, callback: Callable[[BokehSessionContext], None]) -> None: diff --git a/panel/tests/test_server.py b/panel/tests/test_server.py index 1471a7553f..9261499ed6 100644 --- a/panel/tests/test_server.py +++ b/panel/tests/test_server.py @@ -480,6 +480,56 @@ def test_serve_can_serve_bokeh_app_from_file(): assert "/bk-app" in server._tornado.applications +def test_server_on_load_after_init(threads, port): + loaded = [] + + def cb(): + loaded.append(state.loaded) + + def cb2(): + state.execute(cb, schedule=True) + + def app(): + state.onload(cb) + state.onload(cb2) + # Simulate rendering + def loaded(): + state.curdoc + state._schedule_on_load(state.curdoc, None) + state.execute(loaded, schedule=True) + return 'App' + + serve_and_request(app) + + # Checks whether onload callback was executed twice once before and once after load + wait_until(lambda: loaded == [False, True]) + + +def test_server_on_load_during_load(threads, port): + loaded = [] + + def cb(): + loaded.append(state.loaded) + + def cb2(): + state.onload(cb) + + def app(): + state.onload(cb) + state.onload(cb2) + # Simulate rendering + def loaded(): + state.curdoc + state._schedule_on_load(state.curdoc, None) + state.execute(loaded, schedule=True) + return 'App' + + serve_and_request(app) + + # Checks whether onload callback was executed twice once before and once during load + wait_until(lambda: loaded == [False, False]) + + def test_server_thread_pool_on_load(threads, port): counts = []