whitphx / stlite

In-browser Streamlit 🎈🚀

Home Page:https://edit.share.stlite.net

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.