Skip to content

Allow mo.state setters in widget callbacks#8244

Merged
mscolnick merged 4 commits intomainfrom
push-lxptyyxqpunl
Feb 10, 2026
Merged

Allow mo.state setters in widget callbacks#8244
mscolnick merged 4 commits intomainfrom
push-lxptyyxqpunl

Conversation

@manzt
Copy link
Collaborator

@manzt manzt commented Feb 9, 2026

Replaces #8243

Widget observe callbacks triggered by frontend model updates happen outside cell execution, so there's no execution context. Previously register_state_update asserted one existed, crashing the setter.

The context was only needed for self-loop prevention (don't re-run the cell that called the setter). We use a "__external__" sentinel cell ID that won't match any real cell. I believe this is safe since there is no "self" cell to loop back to. An alternative would be to install a real execution context by tracking which cell created each model, but that adds complexity for a case that's truly external.

Also ensures handle_receive_model_message flushes pending state updates when no UIElement triggers a run through the normal path.

@manzt manzt requested a review from dmadisetti as a code owner February 9, 2026 20:58
@manzt manzt added the enhancement New feature or request label Feb 9, 2026
@vercel
Copy link

vercel bot commented Feb 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment Feb 10, 2026 8:28pm

Request Review

# callback triggered by a frontend message). Use a sentinel
# that won't match any real cell, so self-loop prevention
# is skipped.
setter_cell_id = CellId_t("__external__")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this actually might fix a few other errors we say when people called this in async background tasks

mscolnick
mscolnick previously approved these changes Feb 9, 2026
@dmadisetti
Copy link
Collaborator

You may need to rebase against main (should fix failing ci too)

mscolnick
mscolnick previously approved these changes Feb 10, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables mo.state setters to be called from widget/async callbacks that execute outside normal cell execution by introducing an “external setter” sentinel cell id, and ensures model-message handling flushes pending state updates so dependent cells re-run (or are marked stale in lazy mode).

Changes:

  • Update Kernel.register_state_update to allow state setters without an active execution context using a CellId_t("__external__") sentinel, and refactor cell selection into _find_cells_for_state.
  • Flush pending state_updates after processing a model update message when no UIElement-driven run occurs.
  • Add regression tests covering external set_state and anywidget model-message/observe + mo.state interactions (including nested models and self-loop avoidance expectations).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
marimo/_runtime/runtime.py Allows external state updates via sentinel cell id; adds _find_cells_for_state; flushes pending state updates after model messages.
tests/_runtime/test_state.py Adds tests for calling set_state outside cell execution and verifying downstream re-runs.
tests/_plugins/ui/_impl/test_anywidget.py Adds regression tests ensuring model-message-triggered observe callbacks can safely call mo.state setters (including nested models).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

manzt and others added 4 commits February 10, 2026 15:21
Replaces #8243

Widget observe callbacks triggered by frontend model updates happen
outside cell execution, so there's no execution context. Previously
`register_state_update` asserted one existed, crashing the setter.

The context was only needed for self-loop prevention (don't re-run the
cell that called the setter). We use a `"__external__"` sentinel cell ID
that won't match any real cell — I believe this is safe since there is
no "self" cell to loop back to. An alternative would be to install a
real execution context by tracking which cell created each model, but
that adds complexity for a case that's truly external.

Also ensures `handle_receive_model_message` flushes pending state
updates when no UIElement triggers a run through the normal path.
Adds a test where the same cell defines a state, reads it, and has an
observe callback that calls the setter. Confirms the defining cell does
not re-run — only downstream cells do.
State setters called outside cell execution (async tasks, widget
callbacks) would queue an update but nothing would process it. The
runner only flushes `state_updates` during `_run_cells`, which only runs
when something explicitly triggers it.

Now `register_state_update` enqueues an `ExecuteStaleCellsCommand` when
called outside cell execution, mirroring what `mo.Thread` already does.
This keeps the fix in one place rather than needing flush logic in every
message handler.
@manzt manzt force-pushed the push-lxptyyxqpunl branch from 22caa81 to a6c46b1 Compare February 10, 2026 20:27
@mscolnick mscolnick merged commit e7e1a0a into main Feb 10, 2026
49 of 79 checks passed
@mscolnick mscolnick deleted the push-lxptyyxqpunl branch February 10, 2026 22:10
@github-actions
Copy link

🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.19.10-dev44

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

Labels

bash-focus Area to focus on during release bug bash enhancement New feature or request

5 participants