Repository navigation
feat: let the browser open widget comms (bqplot toolbar, pythreejs light defaults) - #346
Merged
Merged
Conversation
cpsievert
force-pushed
the
frontend-comm-open
branch
from
October 27, 2026 15:16
982566e to
2d71825
Compare
This was referenced Oct 27, 2026
cpsievert
added a commit
that referenced
this pull request
Oct 27, 2026
Browser-created widgets sync to Python now (#346), so the old 'don't sync / TraitError' phrasing is wrong. The explicit target/shadow workaround still earns its keep: browser-created widgets live until the session ends, so without it every re-render leaks 3 models per light (and logs a transient console error).
cpsievert
added a commit
that referenced
this pull request
Oct 28, 2026
Browser-created widgets sync to Python now (#346), so the old 'don't sync / TraitError' phrasing is wrong. The explicit target/shadow workaround still earns its keep: browser-created widgets live until the session ends, so without it every re-render leaks 3 models per light (and logs a transient console error).
…n use The duplicate-id guard only checked the session's comm manager, but the widget registry is process-wide: a comm_open naming a widget that was constructed but not yet opened (or one from another session) replaced it in the registry. Also wraps a long comment ruff reformatted awkwardly.
cpsievert
force-pushed
the
frontend-comm-open
branch
from
October 28, 2026 00:32
2d71825 to
f4a2047
Compare
cpsievert
added a commit
that referenced
this pull request
Oct 28, 2026
Browser-created widgets sync to Python now (#346), so the old 'don't sync / TraitError' phrasing is wrong. The explicit target/shadow workaround still earns its keep: browser-created widgets live until the session ends, so without it every re-render leaks 3 models per light (and logs a transient console error).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Some widgets create models in the browser rather than in Python. Two examples are bqplot's toolbar, which creates a
PanZoominteraction when you click its button, and pythreejs, which creates a defaulttargetandshadowfor lights you don't give them. In Jupyter, the kernel answers the browser'scomm_openby building the matching Python widget. shinywidgets had no equivalent, so these models had no Python counterpart. That caused several problems:TraitErrors. These fired whenever the browser sent a reference (IPY_MODEL_<id>) to one of these models. pythreejs scenes raised them on every render unless you built the light'stargetandshadowyourself.reactive_read(fig.widget, "interaction")couldn't see which toolbar tool was active.With this PR, the browser can open, update, and close comms the same way it would talk to a kernel. bqplot's built-in toolbar now works end to end. A browser-created widget is a real Python widget for the life of the session, and when the browser closes it, Python closes it too.
The PR also adds a browser test confirming that widget updates made from a background thread, such as nglview's remote-call thread or your own
threading.Thread, reach the browser promptly. This needed no change in shinywidgets itself, because py-shiny main already makes the output flush thread-safe. The test just pins that behavior.Context
This came out of a look at whether shinywidgets should run a real Jupyter kernel behind each session instead of replacing the comm layer. The answer was no: a kernel would break Shinylive, where there's no zmq or ipykernel, and would add per-session startup time and memory. Instead, this PR closes the one real gap a kernel would have closed, which is comms the browser opens.
Implementation notes
header.msg_typeofcomm_open,comm_msg, orcomm_close. Messages without a header are still treated ascomm_msg.comm_openbuilds the widget through ipywidgets' ownWidget.handle_comm_opened, so construction, buffers, andecho_updatebehave as they would under a kernel. The browser already has the model, so nothing is published back.comm_openis ignored with a warning if its target isn'tjupyter.widget, or if its id is already in use by any widget in the process. That covers other sessions' widgets and widgets that haven't been sent to the browser yet. An unknown model class is logged, and the comm is cleaned up without killing the session's input handling._create_commnow returns a realShinyCommthat announces itself withcomm_open. Closing a comm from the browser sendscomm_closeand runs the model's close handler, the way JupyterLab'sCommHandlerdoes, so the manager drops the model. A close that started in Python no longer bounces acomm_closeback. Closing a comm during disconnect does not send acomm_close; sending is restored when Shiny reconnects.Known limitation
A browser-created widget that is referenced only by a closed parent stays alive until the session ends. For example, re-rendering a figure while
PanZoomis active retains that PanZoom and its two scales. Repeated PanZoom activation followed by re-rendering retains another such set each time, so memory can grow during a long-lived session. Closing the session releases them.Stack
This PR is stacked on #343 (generic AMD externals). #324 (pythreejs) and #329 (ipyvolume) are stacked on this PR. Merge #343 first, then this PR, then #324, then #329. This PR doesn't depend on anything in #343. It sits there only because #324 needs both.
Verification
make check: format, pyright and unit tests all pass. New unit tests cover open, update, close, duplicate and in-use ids, unknown targets, unknown model classes, binary buffers, and messages without a header.comm_closeor accumulating models beyond the PanZoom and scales described above when PanZoom is activated only once.comm_closeafter a real WebSocket reconnect.🤖 Generated with Claude Code