Skip to content

Commit

Permalink
Add and clean up utilities for event handling (#5444)
Browse files Browse the repository at this point in the history
* Add and clean up utilities for event handling

* Fix imports
  • Loading branch information
philippjfr authored Aug 25, 2023
1 parent 905a44b commit efb488b
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 25 deletions.
6 changes: 5 additions & 1 deletion panel/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,6 +33,8 @@
__all__ = (
"PeriodicCallback",
"Resources",
"hold",
"immediate_dispatch",
"ipywidget",
"panel_logger",
"profile",
Expand Down
30 changes: 29 additions & 1 deletion panel/io/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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
67 changes: 44 additions & 23 deletions panel/io/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
"""
Expand All @@ -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.
"""
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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()

0 comments on commit efb488b

Please sign in to comment.