When a Python callback is invoked asynchronously from JS (e.g. via pyodide.ffi.create_proxy, the pattern used to receive progress callbacks from a JS library like WebLLM), calling any Streamlit widget (st.progress, st.empty().caption, etc.) from inside that callback raises:
AttributeError: 'NoneType' object has no attribute 'get_name'
at streamlit/runtime/scriptrunner_utils/script_run_context.py, inside get_script_run_ctx().
I followed the documented workaround for calling Streamlit from background threads (capturing get_script_run_ctx() beforehand and calling add_script_run_ctx(threading.current_thread(), ctx) inside the callback), but it still fails at the same line - I suspect threading under Pyodide doesn't behave the way Streamlit's context-tracking expects (no real OS threads).
Minimal reproducer:
import asyncio
import streamlit as st
from streamlit.runtime.scriptrunner import add_script_run_ctx, get_script_run_ctx
st.title("Minimal repro: st.progress() inside a JS-invoked callback")
if st.button("Run"):
progress_bar = st.progress(0)
ctx = get_script_run_ctx()
def on_tick(value):
add_script_run_ctx(__import__("threading").current_thread(), ctx)
progress_bar.progress(value)
async def fake_async_work():
import js # type: ignore[import-not-found]
from pyodide.ffi import create_proxy
proxy = create_proxy(on_tick)
# Simulate a JS library invoking our Python callback asynchronously,
# the same way WebLLM's initProgressCallback does.
await js.eval("new Promise(r => setTimeout(r, 50))")
proxy(0.5)
await js.eval("new Promise(r => setTimeout(r, 50))")
proxy(1.0)
proxy.destroy()
asyncio.run(fake_async_work())
Expected: the widget updates using the captured context, same as the documented threading pattern works in server-based Streamlit.
Actual: crashes with the AttributeError above, even with add_script_run_ctx.
Use case: wiring up initProgressCallback from @mlc-ai/web-llm (via import js + create_proxy) to show real download progress for an in-browser LLM - this is a common pattern for any JS library with an async progress callback.
When a Python callback is invoked asynchronously from JS (e.g. via
pyodide.ffi.create_proxy, the pattern used to receive progress callbacks from a JS library like WebLLM), calling any Streamlit widget (st.progress,st.empty().caption, etc.) from inside that callback raises:at
streamlit/runtime/scriptrunner_utils/script_run_context.py, insideget_script_run_ctx().I followed the documented workaround for calling Streamlit from background threads (capturing
get_script_run_ctx()beforehand and callingadd_script_run_ctx(threading.current_thread(), ctx)inside the callback), but it still fails at the same line - I suspectthreadingunder Pyodide doesn't behave the way Streamlit's context-tracking expects (no real OS threads).Minimal reproducer:
import asyncio
import streamlit as st
from streamlit.runtime.scriptrunner import add_script_run_ctx, get_script_run_ctx
st.title("Minimal repro: st.progress() inside a JS-invoked callback")
if st.button("Run"):
progress_bar = st.progress(0)
ctx = get_script_run_ctx()
Expected: the widget updates using the captured context, same as the documented threading pattern works in server-based Streamlit.
Actual: crashes with the AttributeError above, even with
add_script_run_ctx.Use case: wiring up
initProgressCallbackfrom@mlc-ai/web-llm(viaimport js+create_proxy) to show real download progress for an in-browser LLM - this is a common pattern for any JS library with an async progress callback.