Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for - middleware for component level interactions + custom name for logging purposes #5273

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
22 changes: 22 additions & 0 deletions panel/io/middlewares.py
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions panel/io/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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
#----------------------------------------------------------------
Expand Down
14 changes: 14 additions & 0 deletions panel/reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,13 +408,20 @@ 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
try:
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
Expand Down Expand Up @@ -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)
Expand Down
45 changes: 45 additions & 0 deletions panel/tests/ui/io/test_middlewares.py
Original file line number Diff line number Diff line change
@@ -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)))