Repository navigation
feat: share process-global widgets across sessions and re-renders (#348) - #356
Merged
Merged
Conversation
This was referenced Oct 28, 2026
ipyvue's ForceLoad is a DOMWidget with `_view_module = None`, which crashed the dependency lookup. Fall back to `_model_module`, then the widget's package.
…templates ipywidgets treats a comm whose `kernel` is None like a DummyComm, and ipyvue >= 3 uses the same check to decide whether a new Template reached a frontend. Without it, ipyvue caches templates that no browser ever received.
A widget is process-global if it was built in shared_construction(), is held in ipyvue's template registry, or is a module-level widget ipyvue, ipyvuetify, lonboard or ipycanvas construct while being imported (including when that import first runs inside a session). Render invalidation and session end no longer close such widgets, since other renders and sessions still use them.
open_shared_in() moves a process-global widget onto a SharedShinyComm and opens it in a session the same way a session-owned widget's deferred open does (pre-serialize hooks, dependencies, extra open keys such as bokeh's ES modules and itables' jQuery protection). It follows the widget's direct references that are process-global or were built outside any session (e.g. the Layout ipyvue's ForceLoad materializes at import time), but never another session's widgets or closed ones, so per-session state is never shared.
Widgets constructed inside `with shared_lifetime():` (at module level or in a session) exist once per process and are mirrored into every session's browser; they are never freed. The ownership errors (widget constructed outside a session, closed, or from another session) now explain when to reach for session_lifetime() versus shared_lifetime().
…s read importing_ipyvue(), importing_ipyvuetify(), and in_template_registry() in _shinywidgets.py depend on upstream module internals (ipyvue.ForceLoad's force_load_instance, ipyvuetify.Themes' theme, ipyvue.Template's template_registry dict). If an upstream release restructures these, the heuristics silently stop firing and the import-time-widget bug (30s browser hangs) returns, previously caught only by a Playwright test with no scheduled CI run. These tests import the real packages so such a change fails immediately and loudly in the unit suite, naming the affected heuristic.
…idget in __all__ The '# Soft deprecated' group was introduced in 4451359 (PR #119) for register_widget alone. 1de3b2d (PR #258), the commit that added session_lifetime() as a new feature, appended it at the end of __all__ where the comment happened to sit, so it read as soft-deprecated even though nothing deprecates it. Group it with shared_lifetime() instead, since both are the lifetime APIs the ownership errors now recommend.
A shared widget that references a session-owned or closed widget leaves the other sessions' browsers waiting for a model they never receive. Cover the two places that must become loud: open_shared_in() skipping such a reference, and a SharedShinyComm update introducing one.
…t show Two structural holes made this silent: open_shared_in() skipped session-owned/closed references on the assumption that the render-time walk rejects them (it doesn't -- it prunes at process-global widgets), and trait updates fanned out by SharedShinyComm.send() could introduce references that exist in only one session's browser. - open_shared_in() now warns once per skipped reference, naming both widgets and pointing at shared_lifetime(). - SharedShinyComm.send() scans outgoing state for IPY_MODEL_<id> references (guard_shared_send): a process-global one (e.g. a shared widget built lazily, after some session already had this widget open) is opened in the fan-out sessions missing it; any other (session-owned or closed) warns once per referenced model. - close_render_widgets() now roots its keep-walk at process-global widgets in the closing set: they survive, so what they reference must survive too. The new warning surfaced this -- a template-registry widget outlived its render while its Layout/Style were closed.
…plicit target/shadow
Two-session Playwright regression tests for the shared_lifetime() known
limitation: pythreejs's JavaScript creates a default light's target/shadow
models in the first browser, and a second session referencing them waits
30s for models that never arrive ('Timeout waiting for widget model') and
the affected outputs never render. The remedy app (explicit target/shadow
built inside shared_lifetime()) renders in both sessions.
Uses render_widget (not render_pythreejs from #324) so the test runs on
this branch's base; pythreejs is added to the test dependency groups.
(With render_pythreejs, the failure was also seen to cascade to unrelated
outputs via 'Syncing error: no comm channel defined'.)
cpsievert
force-pushed
the
feat-348-shared-widgets
branch
from
October 28, 2026 00:32
8277f1f to
bbb36d1
Compare
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 this matters
Apps built on ipyvuetify or ipyvue (and anything that uses them under the hood, like ipyvolume's pylab API) hang for ~30 s after the first re-render or in a second user's session, and one dangling reference blanks every vuetify widget on the page. The cause: ipyvue and ipyvuetify create widgets for the whole Python process (
force_load_instance, one cachedTemplateper.vuefile,theme), but shinywidgets gives every widget to one session and closes it when that render is invalidated.With this PR those widgets render, re-render, and work across concurrent sessions, with no per-package workarounds. It also adds a public
shared_lifetime()for widgets you want every session to display.What changes for users
VueTemplates and module-leveltheme.dark = True, across re-renders and sessions (new Playwright regression test).shared_lifetime()(new, public): construct a widget inside it and every session shows the same Python widget. Any user's interaction changes it for everyone, and it is never freed, so it is for static content or deliberately shared state. The docstring says how to use it without unbounded growth.session_lifetime(), or shared with every user withshared_lifetime().Why it sits below #329
#329 currently works around this same gap for ipyvolume with
neutralize_ipyvolume_pylab()and an ipyvue/ipyvuetify import-tolerance entry. This PR replaces both, so #329 is restacked on this branch without them and the workarounds never reachmain.How it works (short)
Widgets known to be process-global (listed packages' import-time singletons, ipyvue's cached templates, and
shared_lifetime()widgets) get a comm that fans out to every session that uses them. They are opened in a session at session start, at render time, and when a referencing widget's comm opens, and they are skipped by render and session cleanup.Known limitations
box.children += (HTML(...),)on a shared box) is an unsupported shape. Other sessions see a dangling reference rather than an error.target/shadow) belong to that browser. Confirmed in a browser: a second session waits ~30 s for the missing model (Timeout waiting for widget model) and the affected outputs never render (two-session Playwright regression test intests/playwright/shared_pythreejs/). Give such traits explicit values built insideshared_lifetime(); the docstring says so. The new dangling-reference warning also fires in this scenario, naming the trait and the remedy.Updates from review
open_shared_in()warns when a shared widget references a session-owned or closed widget, and a guard inSharedShinyComm.send()scans outgoing state forIPY_MODEL_references: process-global references are opened in the missing sessions on the spot (this keeps the documented "lazy construction in a callback" pattern working), while session-owned/closed references produce aUserWarningnaming both widgets, the browser symptom, and theshared_lifetime()remedy. Also fixesclose_render_widgets()closing a surviving shared widget'sLayout/HTMLStyledefaults (surviving process-global widgets now root the keep-walk).tests/unit/test_upstream_internals.py): the ipyvue/ipyvuetify heuristics' assumptions (template_registry, import-time module bindings) are now asserted against the real packages, so an upstream restructure fails the unit suite immediately instead of surfacing as a Playwright timeout.session_lifetime()un-deprecated: it was accidentally grouped under# Soft deprecatedin__all__(swept in when appended in Add session_lifetime() and fix widget cleanup leaks #258; the comment was introduced forregister_widgetinrender_widgetnow attaches it's return value to the decorated function #119). It's now grouped withshared_lifetime()under# Lifetime methods.render_widgetrather thanrender_pythreejsso it runs on this branch's feat: let the browser open widget comms (bqplot toolbar, pythreejs light defaults) #346 base.Verification
make check: 484 unit tests passed on the restacked branch (the 9 original commits plus the 5 review-feedback commits).render_ipyvuetify(re-renders and concurrent sessions) andshared_pythreejs(default vs. explicit light target/shadow across two sessions).render_geoviews/render_lonboardcovered by CI.render_bqplot/render_ipydatagridlifecycle tests flake intermittently onmaintoo (verified on the base commit).