Skip to content

Commit

Permalink
Ensure ParamRef resolves synchronous generators asynchronously (#6885)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed May 31, 2024
1 parent 149b361 commit 4ffc86f
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 53 deletions.
20 changes: 2 additions & 18 deletions panel/chat/feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from ..layout.card import Card
from ..layout.spacer import VSpacer
from ..pane.image import SVG
from ..util import to_async_gen
from .message import ChatMessage

if TYPE_CHECKING:
Expand Down Expand Up @@ -457,31 +458,14 @@ async def _schedule_placeholder(
return
await asyncio.sleep(0.1)

async def _to_async_gen(self, sync_gen):
done = object()

def safe_next():
# Converts StopIteration to a sentinel value to avoid:
# TypeError: StopIteration interacts badly with generators and cannot be raised into a Future
try:
return next(sync_gen)
except StopIteration:
return done

while True:
value = await asyncio.to_thread(safe_next)
if value is done:
break
yield value

async def _handle_callback(self, message, loop: asyncio.BaseEventLoop):
callback_args = self._gather_callback_args(message)
if iscoroutinefunction(self.callback):
response = await self.callback(*callback_args)
elif isasyncgenfunction(self.callback):
response = self.callback(*callback_args)
elif isgeneratorfunction(self.callback):
response = self._to_async_gen(self.callback(*callback_args))
response = to_async_gen(self.callback(*callback_args))
# printing type(response) -> <class 'async_generator'>
else:
response = await asyncio.to_thread(self.callback, *callback_args)
Expand Down
14 changes: 3 additions & 11 deletions panel/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class Skip(RuntimeError):
from .reactive import Reactive
from .util import (
abbreviated_repr, flatten, full_groupby, fullpath, is_parameterized,
param_name, recursive_parameterized,
param_name, recursive_parameterized, to_async_gen,
)
from .util.checks import is_dataframe, is_mpl_axes, is_series
from .viewable import Layoutable, Viewable
Expand Down Expand Up @@ -888,19 +888,11 @@ def _replace_pane(self, *args, force=False):
param.DEBUG, 'Skip event was raised, skipping update.'
)
return
if isinstance(new_object, Generator):
new_object = to_async_gen(new_object)
if inspect.isawaitable(new_object) or isinstance(new_object, types.AsyncGeneratorType):
param.parameterized.async_executor(partial(self._eval_async, new_object))
return
elif isinstance(new_object, Generator):
append_mode = self.generator_mode == 'append'
if append_mode:
self._inner_layout[:] = []
for new_obj in new_object:
if append_mode:
self._inner_layout.append(new_obj)
self._pane = self._inner_layout[-1]
else:
self._update_inner(new_obj)
else:
self._update_inner(new_object)
finally:
Expand Down
44 changes: 20 additions & 24 deletions panel/tests/test_param.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
from panel.param import (
JSONInit, Param, ParamFunction, ParamMethod, Skip,
)
from panel.tests.util import mpl_available, mpl_figure
from panel.tests.util import (
async_wait_until, mpl_available, mpl_figure, wait_until,
)
from panel.widgets import (
AutocompleteInput, Button, Checkbox, DatePicker, DatetimeInput,
EditableFloatSlider, EditableRangeSlider, LiteralInput, NumberInput,
Expand Down Expand Up @@ -1789,11 +1791,11 @@ def function(value):

root = pane.get_root(document, comm)

assert root.children[0].text == '&lt;p&gt;False&lt;/p&gt;\n'
wait_until(lambda: root.children[0].text == '&lt;p&gt;False&lt;/p&gt;\n')

checkbox.value = True

assert root.children[0].text == '&lt;p&gt;True&lt;/p&gt;\n'
wait_until(lambda: root.children[0].text == '&lt;p&gt;True&lt;/p&gt;\n')


def test_param_generator_append(document, comm):
Expand All @@ -1807,15 +1809,17 @@ def function(value):

root = pane.get_root(document, comm)

assert len(root.children) == 2
wait_until(lambda: len(root.children) == 2)
assert root.children[0].text == '&lt;p&gt;False&lt;/p&gt;\n'
assert root.children[1].text == '&lt;p&gt;True&lt;/p&gt;\n'

checkbox.value = True

assert len(root.children) == 2
assert root.children[0].text == '&lt;p&gt;True&lt;/p&gt;\n'
assert root.children[1].text == '&lt;p&gt;False&lt;/p&gt;\n'
wait_until(lambda: len(root.children) == 2)
wait_until(lambda: (
(root.children[0].text == '&lt;p&gt;True&lt;/p&gt;\n') and
(root.children[1].text == '&lt;p&gt;False&lt;/p&gt;\n')
))

async def test_param_async_generator(document, comm):
checkbox = Checkbox(value=False)
Expand All @@ -1827,15 +1831,11 @@ async def function(value):

root = pane.get_root(document, comm)

await asyncio.sleep(0.01)

assert root.children[0].text == '&lt;p&gt;False&lt;/p&gt;\n'
await async_wait_until(lambda: root.children[0].text == '&lt;p&gt;False&lt;/p&gt;\n')

checkbox.value = True

await asyncio.sleep(0.01)

assert root.children[0].text == '&lt;p&gt;True&lt;/p&gt;\n'
await async_wait_until(lambda: root.children[0].text == '&lt;p&gt;True&lt;/p&gt;\n')

async def test_param_async_generator_append(document, comm):
checkbox = Checkbox(value=False)
Expand All @@ -1849,21 +1849,17 @@ async def function(value):

root = pane.get_root(document, comm)

await asyncio.sleep(0.01)
assert len(root.children) == 1
assert root.children[0].text == '&lt;p&gt;False&lt;/p&gt;\n'
await asyncio.sleep(0.01)
assert len(root.children) == 2
await async_wait_until(lambda: len(root.children) == 1, interval=10)
await async_wait_until(lambda: root.children[0].text == '&lt;p&gt;False&lt;/p&gt;\n')
await async_wait_until(lambda: len(root.children) == 2, interval=10)
assert root.children[0].text == '&lt;p&gt;False&lt;/p&gt;\n'
assert root.children[1].text == '&lt;p&gt;True&lt;/p&gt;\n'

checkbox.value = True

await asyncio.sleep(0.01)
assert len(root.children) == 1
await async_wait_until(lambda: len(root.children) == 1, interval=10)
assert root.children[0].text == '&lt;p&gt;True&lt;/p&gt;\n'
await asyncio.sleep(0.01)
assert len(root.children) == 2
await async_wait_until(lambda: len(root.children) == 2, interval=10)
assert root.children[0].text == '&lt;p&gt;True&lt;/p&gt;\n'
assert root.children[1].text == '&lt;p&gt;False&lt;/p&gt;\n'

Expand All @@ -1879,11 +1875,11 @@ def function(value):

root = pane.get_root(document, comm)

assert root.children[0].text == '&lt;p&gt;True&lt;/p&gt;\n'
wait_until(lambda: root.children[0].text == '&lt;p&gt;True&lt;/p&gt;\n')

checkbox.value = True

assert root.children[0].text == '&lt;p&gt;False&lt;/p&gt;\n'
wait_until(lambda: root.children[0].text == '&lt;p&gt;False&lt;/p&gt;\n')

async def test_param_async_generator_multiple(document, comm):
checkbox = Checkbox(value=False)
Expand Down
19 changes: 19 additions & 0 deletions panel/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import ast
import asyncio
import base64
import datetime as dt
import json
Expand Down Expand Up @@ -484,3 +485,21 @@ def try_datetime64_to_datetime(value):
if isinstance(value, np.datetime64):
value = value.astype('datetime64[ms]').astype(datetime)
return value


async def to_async_gen(sync_gen):
done = object()

def safe_next():
# Converts StopIteration to a sentinel value to avoid:
# TypeError: StopIteration interacts badly with generators and cannot be raised into a Future
try:
return next(sync_gen)
except StopIteration:
return done

while True:
value = await asyncio.to_thread(safe_next)
if value is done:
break
yield value

0 comments on commit 4ffc86f

Please sign in to comment.