Skip to content

Commit 3ba99a5

Browse files
ahuang11philippjfr
authored andcommitted
Fix ChatFeed / Interface tests and async generator placeholders (#6245)
* Fix chat issues * Lower timeout
1 parent 6ffd310 commit 3ba99a5

File tree

4 files changed

+85
-61
lines changed

4 files changed

+85
-61
lines changed

panel/chat/feed.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010

1111
from enum import Enum
1212
from functools import partial
13-
from inspect import isasyncgen, isawaitable, isgenerator
13+
from inspect import (
14+
isasyncgen, isasyncgenfunction, isawaitable, iscoroutinefunction,
15+
isgenerator,
16+
)
1417
from io import BytesIO
1518
from typing import (
1619
TYPE_CHECKING, Any, Callable, ClassVar, Dict, List, Literal,
@@ -479,12 +482,24 @@ async def _schedule_placeholder(
479482
return
480483

481484
start = asyncio.get_event_loop().time()
482-
while not self._callback_state == CallbackState.IDLE and num_entries == len(self._chat_log):
485+
while not task.done() and num_entries == len(self._chat_log):
483486
duration = asyncio.get_event_loop().time() - start
484487
if duration > self.placeholder_threshold:
485488
self.append(self._placeholder)
486489
return
487-
await asyncio.sleep(0.28)
490+
await asyncio.sleep(0.1)
491+
492+
async def _handle_callback(self, message, loop):
493+
callback_args = self._gather_callback_args(message)
494+
if iscoroutinefunction(self.callback):
495+
response = await self.callback(*callback_args)
496+
elif isasyncgenfunction(self.callback):
497+
response = self.callback(*callback_args)
498+
else:
499+
response = await loop.run_in_executor(
500+
None, partial(self.callback, *callback_args)
501+
)
502+
await self._serialize_response(response)
488503

489504
async def _prepare_response(self, _) -> None:
490505
"""
@@ -505,19 +520,12 @@ async def _prepare_response(self, _) -> None:
505520
return
506521

507522
num_entries = len(self._chat_log)
508-
callback_args = self._gather_callback_args(message)
509523
loop = asyncio.get_event_loop()
510-
if asyncio.iscoroutinefunction(self.callback):
511-
future = loop.create_task(self.callback(*callback_args))
512-
else:
513-
future = loop.run_in_executor(None, partial(self.callback, *callback_args))
524+
future = loop.create_task(self._handle_callback(message, loop))
514525
self._callback_future = future
515-
await self._schedule_placeholder(future, num_entries)
516-
517-
if not future.cancelled():
518-
await future
519-
response = future.result()
520-
await self._serialize_response(response)
526+
await asyncio.gather(
527+
self._schedule_placeholder(future, num_entries), future,
528+
)
521529
except StopCallback:
522530
# callback was stopped by user
523531
self._callback_state = CallbackState.STOPPED
@@ -536,10 +544,16 @@ async def _prepare_response(self, _) -> None:
536544
else:
537545
raise e
538546
finally:
539-
with param.parameterized.batch_call_watchers(self):
540-
self._replace_placeholder(None)
541-
self._callback_state = CallbackState.IDLE
542-
self.disabled = self._was_disabled
547+
await self._cleanup_response()
548+
549+
async def _cleanup_response(self):
550+
"""
551+
Events to always execute after the callback is done.
552+
"""
553+
with param.parameterized.batch_call_watchers(self):
554+
self._replace_placeholder(None)
555+
self._callback_state = CallbackState.IDLE
556+
self.disabled = self._was_disabled
543557

544558
# Public API
545559

panel/chat/interface.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,3 +590,10 @@ async def _update_input_disabled(self):
590590
with param.parameterized.batch_call_watchers(self):
591591
self._buttons["send"].visible = False
592592
self._buttons["stop"].visible = True
593+
594+
async def _cleanup_response(self):
595+
"""
596+
Events to always execute after the callback is done.
597+
"""
598+
await super()._cleanup_response()
599+
await self._update_input_disabled()

panel/tests/chat/test_feed.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
"max_width": 201,
2121
}
2222

23+
ChatFeed.callback_exception = "raise"
24+
2325

2426
@pytest.fixture
2527
def chat_feed():
@@ -514,7 +516,6 @@ async def echo(contents, user, instance):
514516
assert len(chat_feed.objects) == 2
515517
assert chat_feed.objects[1].object == "Message"
516518

517-
@pytest.mark.asyncio
518519
def test_generator(self, chat_feed):
519520
async def echo(contents, user, instance):
520521
message = ""
@@ -569,7 +570,6 @@ def echo(contents, user, instance):
569570
chat_feed.callback = echo
570571
chat_feed.send("Message", respond=True)
571572
assert chat_feed._placeholder not in chat_feed._chat_log
572-
# append sent message and placeholder
573573

574574
def test_placeholder_threshold_under(self, chat_feed):
575575
async def echo(contents, user, instance):
@@ -606,13 +606,13 @@ async def echo(contents, user, instance):
606606

607607
def test_placeholder_threshold_exceed_generator(self, chat_feed):
608608
async def echo(contents, user, instance):
609-
assert instance._placeholder not in instance._chat_log
609+
await async_wait_until(lambda: instance._placeholder not in instance._chat_log)
610610
await asyncio.sleep(0.5)
611-
assert instance._placeholder in instance._chat_log
611+
await async_wait_until(lambda: instance._placeholder in instance._chat_log)
612612
yield "hello testing"
613-
assert instance._placeholder not in instance._chat_log
613+
await async_wait_until(lambda: instance._placeholder not in instance._chat_log)
614614

615-
chat_feed.placeholder_threshold = 0.2
615+
chat_feed.placeholder_threshold = 1
616616
chat_feed.callback = echo
617617
chat_feed.send("Message", respond=True)
618618
assert chat_feed._placeholder not in chat_feed._chat_log
@@ -701,7 +701,10 @@ async def callback(msg, user, instance):
701701
yield "B"
702702

703703
chat_feed.callback = callback
704-
chat_feed.send("Message", respond=True)
704+
try:
705+
chat_feed.send("Message", respond=True)
706+
except asyncio.CancelledError: # tests pick up this error
707+
pass
705708
# use sleep here instead of wait for because
706709
# the callback is timed and I want to confirm stop works
707710
time.sleep(1)
@@ -715,7 +718,10 @@ async def callback(msg, user, instance):
715718
instance.stream("B", message=message)
716719

717720
chat_feed.callback = callback
718-
chat_feed.send("Message", respond=True)
721+
try:
722+
chat_feed.send("Message", respond=True)
723+
except asyncio.CancelledError:
724+
pass
719725
# use sleep here instead of wait for because
720726
# the callback is timed and I want to confirm stop works
721727
time.sleep(1)
@@ -729,7 +735,10 @@ def callback(msg, user, instance):
729735
instance.stream("B", message=message) # should not reach this point
730736

731737
chat_feed.callback = callback
732-
chat_feed.send("Message", respond=True)
738+
try:
739+
chat_feed.send("Message", respond=True)
740+
except asyncio.CancelledError:
741+
pass
733742
# use sleep here instead of wait for because
734743
# the callback is timed and I want to confirm stop works
735744
time.sleep(1)

panel/tests/chat/test_interface.py

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import asyncio
2+
13
from io import BytesIO
24

35
import pytest
@@ -6,10 +8,12 @@
68
from panel.chat.interface import ChatInterface
79
from panel.layout import Row, Tabs
810
from panel.pane import Image
9-
from panel.tests.util import wait_until
11+
from panel.tests.util import async_wait_until, wait_until
1012
from panel.widgets.button import Button
1113
from panel.widgets.input import FileInput, TextAreaInput, TextInput
1214

15+
ChatInterface.callback_exception = "raise"
16+
1317

1418
class TestChatInterface:
1519
@pytest.fixture
@@ -88,12 +92,10 @@ def test_click_send(self, chat_interface: ChatInterface):
8892
def test_show_stop_disabled(self, chat_interface: ChatInterface):
8993
async def callback(msg, user, instance):
9094
yield "A"
91-
send_button = chat_interface._input_layout[1]
92-
stop_button = chat_interface._input_layout[2]
93-
assert send_button.name == "Send"
94-
assert stop_button.name == "Stop"
95-
assert send_button.visible
96-
assert not send_button.disabled
95+
send_button = instance._buttons["send"]
96+
stop_button = instance._buttons["stop"]
97+
wait_until(lambda: send_button.visible)
98+
wait_until(lambda: send_button.disabled) # should be disabled while callback is running
9799
assert not stop_button.visible
98100
yield "B" # should not stream this
99101

@@ -110,12 +112,10 @@ async def callback(msg, user, instance):
110112

111113
def test_show_stop_for_async(self, chat_interface: ChatInterface):
112114
async def callback(msg, user, instance):
113-
send_button = instance._input_layout[1]
114-
stop_button = instance._input_layout[2]
115-
assert send_button.name == "Send"
116-
assert stop_button.name == "Stop"
117-
assert not send_button.visible
118-
assert stop_button.visible
115+
send_button = instance._buttons["send"]
116+
stop_button = instance._buttons["stop"]
117+
await async_wait_until(lambda: stop_button.visible)
118+
await async_wait_until(lambda: not send_button.visible)
119119

120120
chat_interface.callback = callback
121121
chat_interface.send("Message", respond=True)
@@ -124,12 +124,10 @@ async def callback(msg, user, instance):
124124

125125
def test_show_stop_for_sync(self, chat_interface: ChatInterface):
126126
def callback(msg, user, instance):
127-
send_button = instance._input_layout[1]
128-
stop_button = instance._input_layout[2]
129-
assert send_button.name == "Send"
130-
assert stop_button.name == "Stop"
131-
assert not send_button.visible
132-
assert stop_button.visible
127+
send_button = instance._buttons["send"]
128+
stop_button = instance._buttons["stop"]
129+
wait_until(lambda: stop_button.visible)
130+
wait_until(lambda: not send_button.visible)
133131

134132
chat_interface.callback = callback
135133
chat_interface.send("Message", respond=True)
@@ -138,25 +136,21 @@ def callback(msg, user, instance):
138136

139137
def test_click_stop(self, chat_interface: ChatInterface):
140138
async def callback(msg, user, instance):
141-
send_button = instance._input_layout[1]
142-
stop_button = instance._input_layout[2]
143-
assert send_button.name == "Send"
144-
assert stop_button.name == "Stop"
145-
assert not send_button.visible
146-
assert stop_button.visible
147-
wait_until(lambda: len(instance.objects) == 2)
148-
assert instance._placeholder in instance.objects
139+
send_button = instance._buttons["send"]
140+
stop_button = instance._buttons["stop"]
141+
await async_wait_until(lambda: stop_button.visible)
142+
await async_wait_until(lambda: not send_button.visible)
149143
instance._click_stop(None)
150-
assert send_button.visible
151-
assert not send_button.disabled
152-
assert not stop_button.visible
153-
assert instance._placeholder not in instance.objects
154144

155145
chat_interface.callback = callback
156146
chat_interface.placeholder_threshold = 0.001
157-
chat_interface.send("Message", respond=True)
158-
send_button = chat_interface._input_layout[1]
159-
assert not send_button.disabled
147+
try:
148+
chat_interface.send("Message", respond=True)
149+
except asyncio.exceptions.CancelledError:
150+
pass
151+
wait_until(lambda: not chat_interface._buttons["send"].disabled)
152+
wait_until(lambda: chat_interface._buttons["send"].visible)
153+
wait_until(lambda: not chat_interface._buttons["stop"].visible)
160154

161155
@pytest.mark.parametrize("widget", [TextInput(), TextAreaInput()])
162156
def test_auto_send_types(self, chat_interface: ChatInterface, widget):

0 commit comments

Comments
 (0)