Support st.spinner
whitphx opened this issue · comments
It uses threading
that is not supported in the Pyodide environment and raises an exception, RuntimeError: can't start new thread
.
RuntimeError: can't start new thread
Traceback:
File "/lib/python3.10/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 555, in _run_script
exec(code, module.__dict__)
File "/home/pyodide/streamlit_app.py", line 10, in <module>
with st.spinner():
File "/lib/python3.10/contextlib.py", line 135, in __enter__
return next(self.gen)
File "/lib/python3.10/site-packages/streamlit/__init__.py", line 441, in spinner
_add_script_run_ctx(_threading.Timer(DELAY_SECS, set_message)).start()
File "/lib/python3.10/threading.py", line 928, in start
_start_new_thread(self._bootstrap, ())
Note: time.sleep()
is no-op on Pyodide, so awaiting would be a bit tricky.
In case this is useful to someone else who encountered the same error, I was able to resolve it in my app by setting show_spinner=False
in all of my @st.cache
decorators.
Supporting the original behavior seems to be impossible.
When we change the implementation of st.spinner
as below, the error disappears but the spinner is still not shown.
@_contextlib.contextmanager
def spinner(text: str = "In progress...") -> Iterator[None]:
...
with legacy_caching.suppress_cached_st_function_warning():
with caching.suppress_cached_st_function_warning():
message = empty()
try:
# Set the message 0.1 seconds in the future to avoid annoying
# flickering if this spinner runs too quickly.
DELAY_SECS = 0.1
display_message = True
display_message_lock = _threading.Lock()
def set_message():
with display_message_lock:
if display_message:
with legacy_caching.suppress_cached_st_function_warning():
with caching.suppress_cached_st_function_warning():
spinner_proto = SpinnerProto()
spinner_proto.text = clean_text(text)
message._enqueue("spinner", spinner_proto)
- _add_script_run_ctx(_threading.Timer(DELAY_SECS, set_message)).start()
+ async def defer():
+ await asyncio.sleep(DELAY_SECS)
+ set_message()
+
+ evloop = asyncio.get_event_loop()
+ evloop.create_task(defer())
# Yield control back to the context.
yield
finally:
if display_message_lock:
with display_message_lock:
display_message = False
with legacy_caching.suppress_cached_st_function_warning():
with caching.suppress_cached_st_function_warning():
message.empty()
We tested it with the following code:
import streamlit as st
import pyodide.http
with st.spinner('Wait for it...'):
pyodide.http.open_url("http://localhost:8000")
st.success('Done!')
with the following code running as the mock server at http://localhost:8000
that returns the response with delay.
const http = require("http")
const server = http.createServer((request, response) => {
setTimeout(() => {
response.writeHead(200, {
"Content-Type": "text/html",
'Access-Control-Allow-Origin': '*',
'Access-Control-Request-Method': '*',
'Access-Control-Allow-Methods': 'OPTIONS, GET',
'Access-Control-Allow-Headers': '*',
});
response.end("<html><head></head><body>hello</body></html>")
}, 5000)
});
server.listen(8000)
In this setting, pyodide.http.open_url()
pauses for 5 seconds and st.spinner()
is expected to show the spinner during it, however, set_message()
is called after 5 seconds.
It seems that pyodide.http.open_url()
occupies the event loop so that the coroutine defer()
is started after it even though defer()
is expected to start immediately.
It looks like this problem is derived from the Pyodide event loop design, so we can't find the solution anyway.
All we can do is just suppressing the error.