diff --git a/panel/param.py b/panel/param.py index f20d1b4c41..3ead3ae6e3 100644 --- a/panel/param.py +++ b/panel/param.py @@ -24,9 +24,16 @@ import param +try: + from param import Skip +except Exception: + class Skip(Exception): + """ + Exception that allows skipping an update for function-level updates. + """ from param.parameterized import ( - classlist, discard_events, eval_function_with_deps, get_method_owner, - iscoroutinefunction, resolve_ref, resolve_value, + Undefined, classlist, discard_events, eval_function_with_deps, + get_method_owner, iscoroutinefunction, resolve_ref, resolve_value, ) from param.reactive import rx @@ -829,9 +836,20 @@ async def _eval_async(self, awaitable): self._inner_layout.append(new_obj) self._pane = self._inner_layout[-1] else: - self._update_inner(new_obj) + try: + self._update_inner(new_obj) + except Skip: + pass else: - self._update_inner(await awaitable) + try: + new = await awaitable + if new is Skip or new is Undefined: + raise Skip + self._update_inner(new) + except Skip: + self.param.log( + param.DEBUG, 'Skip event was raised, skipping update.' + ) except Exception as e: if not curdoc or (has_context and curdoc.session_context): raise e @@ -850,7 +868,15 @@ def _replace_pane(self, *args, force=False): if self.object is None: new_object = Spacer() else: - new_object = self.eval(self.object) + try: + new_object = self.eval(self.object) + if new_object is Skip and new_object is Undefined: + raise Skip + except Skip: + self.param.log( + param.DEBUG, 'Skip event was raised, skipping update.' + ) + return if inspect.isawaitable(new_object) or isinstance(new_object, types.AsyncGeneratorType): param.parameterized.async_executor(partial(self._eval_async, new_object)) return diff --git a/panel/tests/test_param.py b/panel/tests/test_param.py index 3fd39574a9..30928fc01d 100644 --- a/panel/tests/test_param.py +++ b/panel/tests/test_param.py @@ -6,10 +6,10 @@ import pytest from bokeh.models import ( - AutocompleteInput as BkAutocompleteInput, Button, Checkbox as BkCheckbox, - Column as BkColumn, Div, MultiSelect, RangeSlider as BkRangeSlider, - Row as BkRow, Select, Slider, Tabs as BkTabs, TextInput, - TextInput as BkTextInput, Toggle, + AutocompleteInput as BkAutocompleteInput, Button as BkButton, + Checkbox as BkCheckbox, Column as BkColumn, Div, MultiSelect, + RangeSlider as BkRangeSlider, Row as BkRow, Select, Slider, Tabs as BkTabs, + TextInput, TextInput as BkTextInput, Toggle, ) from packaging.version import Version @@ -22,11 +22,11 @@ HTML, Bokeh, Markdown, Matplotlib, PaneBase, Str, panel, ) from panel.param import ( - JSONInit, Param, ParamFunction, ParamMethod, + JSONInit, Param, ParamFunction, ParamMethod, Skip, ) from panel.tests.util import mpl_available, mpl_figure from panel.widgets import ( - AutocompleteInput, Checkbox, DatePicker, DatetimeInput, + AutocompleteInput, Button, Checkbox, DatePicker, DatetimeInput, EditableFloatSlider, EditableRangeSlider, LiteralInput, NumberInput, RangeSlider, ) @@ -392,7 +392,7 @@ class Test(param.Parameterized): model = test_pane.get_root(document, comm=comm) button = model.children[1] - assert isinstance(button, Button) + assert isinstance(button, BkButton) # Check that the action is actually executed pn_button = test_pane.layout[1] @@ -1920,3 +1920,48 @@ async def function(value): assert root.children[0].text == '<p>5</p>\n' await asyncio.sleep(0.1) assert root.children[0].text == '<p>6</p>\n' + + +def test_skip_param(document, comm): + checkbox = Checkbox(value=False) + button = Button() + + def layout(value, click): + if not click: + raise Skip() + return Markdown(f"{value}") + + layout = ParamFunction(bind(layout, checkbox, button)) + + root = layout.get_root(document, comm) + + div = root.children[0] + assert div.text == '<pre> </pre>' + checkbox.value = True + assert div.text == '<pre> </pre>' + button.param.trigger('value') + assert div.text == '<pre> </pre>' + +@pytest.mark.asyncio +async def test_async_skip_param(document, comm): + checkbox = Checkbox(value=False) + button = Button() + + async def layout(value, click): + if not click: + raise Skip() + return Markdown(f"{value}") + + layout = ParamFunction(bind(layout, checkbox, button)) + + root = layout.get_root(document, comm) + + div = root.children[0] + await asyncio.sleep(0.01) + assert div.text == '<pre> </pre>' + checkbox.value = True + await asyncio.sleep(0.01) + assert div.text == '<pre> </pre>' + button.param.trigger('value') + await asyncio.sleep(0.01) + assert div.text == '<pre> </pre>'