Skip to content

feat: share process-global widgets across sessions and re-renders (#348) - #351

Merged
cpsievert merged 0 commit into
render-pythreejsfrom
feat-348-shared-widgets
Oct 28, 2026
Merged

cpsievert merged 0 commit into
render-pythreejsfrom
feat-348-shared-widgets

Conversation

@cpsievert

@cpsievert cpsievert commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #346 (on top of #343). Merge order: #343 → #346 → this PR → #324 → #329. Fixes #348.

Restacked off #324: the shared-widget machinery has no functional dependency on the pythreejs renderer (only comment-level mentions), and the rebase onto #346 was conflict-free with the unit suite green. PR #324 and #329 still need their branches rebased and the GitHub stack re-linked in the new order before merge.

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 cached Template per .vue file, 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

  • ipyvuetify / ipyvue trees just work, including custom VueTemplates and module-level theme.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.
  • Ownership errors now explain both options: a widget built per session can be reused inside the session with session_lifetime(), or shared with every user with shared_lifetime().
  • Per-session widgets still raise if shown in another session or after their render was invalidated; nothing silently becomes shared.

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 reach main.

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

  • A shared widget that references a per-session widget (e.g. box.children += (HTML(...),) on a shared box) is an unsupported shape. Other sessions see a dangling reference rather than an error.
  • Models that a shared widget's JavaScript creates in one browser (e.g. pythreejs's default light 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 in tests/playwright/shared_pythreejs/). Give such traits explicit values built inside shared_lifetime(); the docstring says so. The new dangling-reference warning also fires in this scenario, naming the trait and the remedy.
  • References introduced by a trait update after a widget's comm is already open are not opened in other sessions (same as any cross-session reference today).
  • Pre-existing, not touched here: a container built in a rejected render over a closed child leaves a deferred open that errors, and py-shiny then ends the session.

Updates from review

  • Dangling references are now loud. open_shared_in() warns when a shared widget references a session-owned or closed widget, and a guard in SharedShinyComm.send() scans outgoing state for IPY_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 a UserWarning naming both widgets, the browser symptom, and the shared_lifetime() remedy. Also fixes close_render_widgets() closing a surviving shared widget's Layout/HTMLStyle defaults (surviving process-global widgets now root the keep-walk).
  • Upstream-internals canary tests (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 deprecated in __all__ (swept in when appended in Add session_lifetime() and fix widget cleanup leaks #258; the comment was introduced for register_widget in render_widget now attaches it's return value to the decorated function #119). It's now grouped with shared_lifetime() under # Lifetime methods.
  • The pythreejs known limitation is now verified (see Known limitations) with a two-session Playwright regression test. It uses render_widget rather than render_pythreejs so 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).
  • New Playwright coverage: render_ipyvuetify (re-renders and concurrent sessions) and shared_pythreejs (default vs. explicit light target/shadow across two sessions).
  • Earlier full-suite Playwright run (214) as before; render_geoviews/render_lonboard covered by CI. render_bqplot/render_ipydatagrid lifecycle tests flake intermittently on main too (verified on the base commit).

@cpsievert
cpsievert marked this pull request as ready for review October 27, 2026 22:24
@cpsievert
cpsievert marked this pull request as draft October 27, 2026 22:39
@cpsievert
cpsievert added this pull request to stack #352 October 27, 2026 22:46
@cpsievert
cpsievert marked this pull request as ready for review October 27, 2026 23:04
@cpsievert
cpsievert force-pushed the feat-348-shared-widgets branch from e53c07c to 8277f1f Compare October 27, 2026 23:45
@cpsievert
cpsievert merged commit 8277f1f into main Oct 28, 2026
4 of 5 checks passed
@cpsievert

Copy link
Copy Markdown
Collaborator Author

Heads-up: this PR was auto-marked as merged when its base branch render-pythreejs was force-pushed with a history that already contained this PR's head (8277f1f) — standard GitHub behavior for stacked PRs. The changes have not merged to main independently; they currently reach it only via #324's branch. A replacement PR targeting frontend-comm-open is being opened to preserve the intended merge order (#343 → #346 → this change → #324 → #329). All review history remains here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shared singleton widgets (e.g., ipyvue's template_registry) break under render-scoped cleanup and across sessions

1 participant