diff --git a/panel/io/middlewares.py b/panel/io/middlewares.py new file mode 100644 index 0000000000..6482dd76f5 --- /dev/null +++ b/panel/io/middlewares.py @@ -0,0 +1,22 @@ +from typing import Any, Dict + +from bokeh.document import Document +from bokeh.events import Event + +from ..reactive import Syncable + + +class BokehEventMiddleware: + def preprocess(self, syncable: Syncable, doc: Document, event: Event): + pass + + def postprocess(self): + pass + + +class PropertyChangeEventMiddleware: + def preprocess(self, syncable: Syncable, doc: Document, events: Dict[str, Any]): + pass + + def postprocess(self): + pass diff --git a/panel/io/state.py b/panel/io/state.py index 443d81f74a..aeebb05f54 100644 --- a/panel/io/state.py +++ b/panel/io/state.py @@ -55,6 +55,9 @@ from .browser import BrowserInfo from .callbacks import PeriodicCallback from .location import Location + from .middlewares import ( + BokehEventMiddleware, PropertyChangeEventMiddleware, + ) from .notifications import NotificationArea from .server import StoppableThread @@ -214,6 +217,10 @@ class _state(param.Parameterized): _oauth_user_overrides = {} _active_users = Counter() + # Middlewares + _bokeh_event_middlewares: List[BokehEventMiddleware] = [] + _property_change_event_middlewares: List[PropertyChangeEventMiddleware] = [] + def __repr__(self) -> str: server_info = [] for server, panel, docs in self._servers.values(): @@ -880,6 +887,34 @@ def sync_busy(self, indicator: BooleanIndicator) -> None: if indicator not in self._indicators: self._indicators.append(indicator) + def add_bokeh_event_middleware(self, middleware: BokehEventMiddleware) -> None: + """ + Adds a middleware to be triggered during processing of bokeh events. + Users should provide their own implementation for BokehEventMiddleware. + + Arguments + --------- + middleware: BokehEventMiddleware + Middleware whose preprocess and postprocess methods will be + executed before and after processing of a bokeh event on all + Panel components. + """ + self._bokeh_event_middlewares.append(middleware) + + def add_property_change_event_middleware(self, middleware: PropertyChangeEventMiddleware) -> None: + """ + Adds a middleware to be triggered during processing of property change + events. Users should provide their own implementation for PropertyChangeEventMiddleware. + + Arguments + --------- + middleware: PropertyChangeEventMiddleware + Middleware whose preprocess and postprocess methods will be + executed before and after processing of property change event(s) + on all Panel components. + """ + self._property_change_event_middlewares.append(middleware) + #---------------------------------------------------------------- # Public Properties #---------------------------------------------------------------- diff --git a/panel/reactive.py b/panel/reactive.py index b238b1d967..4e8350648f 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -408,6 +408,10 @@ def _process_events(self, events: Dict[str, Any]) -> None: state._busy_counter -= 1 def _process_bokeh_event(self, doc: Document, event: Event) -> None: + + for bokeh_event_middleware in state._bokeh_event_middlewares: + bokeh_event_middleware.preprocess(self, doc, event) + self._log('received bokeh event %s', event) with edit_readonly(state): state._busy_counter += 1 @@ -415,6 +419,9 @@ def _process_bokeh_event(self, doc: Document, event: Event) -> None: with set_curdoc(doc): self._process_event(event) finally: + for bokeh_event_middleware in state._bokeh_event_middlewares: + bokeh_event_middleware.postprocess() + self._log('finished processing bokeh event %s', event) with edit_readonly(state): state._busy_counter -= 1 @@ -442,10 +449,17 @@ async def _event_coroutine(self, doc: Document, event) -> None: def _change_event(self, doc: Document) -> None: events = self._events + + for property_change_event_middleware in state._property_change_event_middlewares: + property_change_event_middleware.preprocess(self, doc, events) + self._events = {} with set_curdoc(doc): self._process_events(events) + for property_change_event_middleware in state._property_change_event_middlewares: + property_change_event_middleware.postprocess() + def _schedule_change(self, doc: Document, comm: Comm | None) -> None: with hold(doc, comm=comm): self._change_event(doc) diff --git a/panel/tests/ui/io/test_middlewares.py b/panel/tests/ui/io/test_middlewares.py new file mode 100644 index 0000000000..d737091bca --- /dev/null +++ b/panel/tests/ui/io/test_middlewares.py @@ -0,0 +1,45 @@ +import time + +import pytest + +pytestmark = pytest.mark.ui + +import panel as pn + +from panel.io.middlewares import BokehEventMiddleware +from panel.tests.util import serve_component, wait_until +from panel.widgets import Button + +try: + from playwright.sync_api import expect +except ImportError: + pytestmark = pytest.mark.skip('playwright not available') + + +def test_middlware(page): + class TimingMiddleware(BokehEventMiddleware): + def preprocess(self, syncable, doc, event): + self.start_time = time.time() + def postprocess(self): + self.process_time = time.time() - self.start_time + timing_middleware = TimingMiddleware() + pn.state.add_bokeh_event_middleware(timing_middleware) + + button = Button(name='Button') + events = [] + def cb(event): + time_to_sleep = 2 + time.sleep(time_to_sleep) + events.append(event) + button.name = str(time_to_sleep) + button.on_click(cb) + + serve_component(page, button) + + page.click('.bk-btn') + + wait_until(lambda: len(events) == 1, page) + + btn = page.locator('.bk-btn') + expect(btn).to_have_count(1) + expect(btn).to_contain_text(str(int(timing_middleware.process_time)))