From efb488b87030fae376b3001c790412ab22e46fdc Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 25 Aug 2023 12:36:10 +0200 Subject: [PATCH] Add and clean up utilities for event handling (#5444) * Add and clean up utilities for event handling * Fix imports --- panel/io/__init__.py | 6 +++- panel/io/document.py | 30 +++++++++++++++++++- panel/io/model.py | 67 +++++++++++++++++++++++++++++--------------- 3 files changed, 78 insertions(+), 25 deletions(-) diff --git a/panel/io/__init__.py b/panel/io/__init__.py index 5f1145a001..770afdd4eb 100644 --- a/panel/io/__init__.py +++ b/panel/io/__init__.py @@ -6,7 +6,9 @@ from .cache import cache # noqa from .callbacks import PeriodicCallback # noqa -from .document import init_doc, unlocked, with_lock # noqa +from .document import ( # noqa + hold, immediate_dispatch, init_doc, unlocked, with_lock, +) from .embed import embed_state # noqa from .logging import panel_logger # noqa from .model import add_to_doc, diff, remove_root # noqa @@ -31,6 +33,8 @@ __all__ = ( "PeriodicCallback", "Resources", + "hold", + "immediate_dispatch", "ipywidget", "panel_logger", "profile", diff --git a/panel/io/document.py b/panel/io/document.py index 2b6bb1f0f7..c5979a27e2 100644 --- a/panel/io/document.py +++ b/panel/io/document.py @@ -25,7 +25,7 @@ from ..config import config from ..util import param_watchers from .loading import LOADING_INDICATOR_CSS_CLASS -from .model import monkeypatch_events +from .model import hold, monkeypatch_events # noqa: API import from .state import curdoc_locked, state logger = logging.getLogger(__name__) @@ -295,3 +295,31 @@ async def handle_write_errors(): except RuntimeError: if remaining_events: curdoc.add_next_tick_callback(partial(_dispatch_events, curdoc, remaining_events)) + +@contextmanager +def immediate_dispatch(doc: Document | None = None): + """ + Context manager to trigger immediate dispatch of events triggered + inside the execution context even when Document events are + currently on hold. + + Arguments + --------- + doc: Document + The document to dispatch events on (if `None` then `state.curdoc` is used). + """ + doc = doc or state.curdoc + + # Skip if not in a server context + if not doc or not doc._session_context: + yield + return + + old_events = doc.callbacks._held_events + held = doc.callbacks._hold + doc.callbacks._held_events = [] + doc.callbacks.unhold() + with unlocked(): + yield + doc.callbacks._hold = held + doc.callbacks._held_events = old_events diff --git a/panel/io/model.py b/panel/io/model.py index 5c2af3f1c5..44e6606d35 100644 --- a/panel/io/model.py +++ b/panel/io/model.py @@ -45,7 +45,7 @@ def __eq__(self, other: Any) -> bool: def __ne__(self, other: Any) -> bool: return not np.array_equal(self, other, equal_nan=True) -def monkeypatch_events(events: List['DocumentChangedEvent']) -> None: +def monkeypatch_events(events: List[DocumentChangedEvent]) -> None: """ Patch events applies patches to events that are to be dispatched avoiding various issues in Bokeh. @@ -66,7 +66,7 @@ def monkeypatch_events(events: List['DocumentChangedEvent']) -> None: #--------------------------------------------------------------------- def diff( - doc: 'Document', binary: bool = True, events: Optional[List['DocumentChangedEvent']] = None + doc: Document, binary: bool = True, events: Optional[List[DocumentChangedEvent]] = None ) -> Message[Any] | None: """ Returns a json diff required to update an existing plot with @@ -92,7 +92,7 @@ def diff( msg.add_buffer(buffer) return msg -def remove_root(obj: 'Model', replace: Optional['Document'] = None) -> None: +def remove_root(obj: Model, replace: Document | None = None) -> None: """ Removes the document from any previously displayed bokeh object """ @@ -104,7 +104,7 @@ def remove_root(obj: 'Model', replace: Optional['Document'] = None) -> None: if replace: model._document = replace -def add_to_doc(obj: 'Model', doc: 'Document', hold: bool = False): +def add_to_doc(obj: Model, doc: Document, hold: bool = False): """ Adds a model to the supplied Document removing it from any existing Documents. """ @@ -114,24 +114,6 @@ def add_to_doc(obj: 'Model', doc: 'Document', hold: bool = False): if doc.callbacks.hold_value is None and hold: doc.hold() -@contextmanager -def hold(doc: 'Document', policy: 'HoldPolicyType' = 'combine', comm: Optional['Comm'] = None): - held = doc.callbacks.hold_value - try: - if policy is None: - doc.unhold() - else: - doc.hold(policy) - yield - finally: - if held: - doc.callbacks._hold = held - else: - if comm is not None: - from .notebook import push - push(doc, comm) - doc.unhold() - def patch_cds_msg(model, msg): """ Required for handling messages containing JSON serialized typed @@ -149,7 +131,7 @@ def patch_cds_msg(model, msg): _DEFAULT_IGNORED_REPR = frozenset(['children', 'text', 'name', 'toolbar', 'renderers', 'below', 'center', 'left', 'right']) -def bokeh_repr(obj: 'Model', depth: int = 0, ignored: Optional[Iterable[str]] = None) -> str: +def bokeh_repr(obj: Model, depth: int = 0, ignored: Optional[Iterable[str]] = None) -> str: """ Returns a string repr for a bokeh model, useful for recreating panel objects using pure bokeh. @@ -184,3 +166,42 @@ def bokeh_repr(obj: 'Model', depth: int = 0, ignored: Optional[Iterable[str]] = else: r += '{cls}({props})'.format(cls=cls, props=props_repr) return r + +@contextmanager +def hold(doc: Document, policy: HoldPolicyType = 'combine', comm: Comm | None = None): + """ + Context manager that holds events on a particular Document + allowing them all to be collected and dispatched when the context + manager exits. This allows multiple events on the same object to + be combined if the policy is set to 'combine'. + + Arguments + --------- + doc: Document + The Bokeh Document to hold events on. + policy: HoldPolicyType + One of 'combine', 'collect' or None determining whether events + setting the same property are combined or accumulated to be + dispatched when the context manager exits. + comm: Comm + The Comm to dispatch events on when the context manager exits. + """ + doc = doc or state.curdoc + if doc is None: + yield + return + held = doc.callbacks.hold_value + try: + if policy is None: + doc.unhold() + else: + doc.hold(policy) + yield + finally: + if held: + doc.callbacks._hold = held + else: + if comm is not None: + from .notebook import push + push(doc, comm) + doc.unhold()