fix(lazy): resolve mo.lazy eagerly in non-interactive exports#9644
Merged
Conversation
Refs #9624. ## Why `mo.lazy` rendered a `<marimo-lazy>` placeholder whose contents were fetched via a kernel RPC. Static exports (ipynb, PDF) have no kernel, so the placeholder rendered empty. ## What `mo.lazy.__new__` checks `is_non_interactive()` and, for synchronous elements, returns the rendered `Html` directly instead of constructing a lazy widget. The lazy wrapper is preserved unchanged for interactive notebooks — laziness in the editor isn't affected. This fixes ipynb and PDF exports, which already set `is_non_interactive()` around `run_app_until_completion`. Async callables can't be awaited from `__new__`; they fall back to the lazy widget and stay as placeholders. ## Not fixed in this PR HTML export still ships the `<marimo-lazy>` placeholder. Enabling `is_non_interactive()` there would also flip tables, altair, plotly, dataframes, and `mo.md` to non-interactive fallbacks, regressing the interactive HTML export. A lazy-specific resolution path (e.g. a narrower flag, or kernel-side resolution at export time) is a separate follow-up.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
1 issue found across 4 files
Architecture diagram
sequenceDiagram
participant Export as Export CLI/API
participant Runner as run_app_until_completion
participant Lazy as mo.lazy.__new__
participant Resolver as _resolve_eagerly
participant Html as Html (final output)
participant Widget as marimo-lazy Widget
Note over Export,Widget: Non-interactive export (ipynb/PDF) flow
Export->>Runner: export_notebook(filepath)
Note over Runner: Sets is_non_interactive() flag
Runner->>Lazy: mo.lazy(element) during cell execution
Lazy->>Lazy: __new__ checks is_non_interactive()
alt Non-interactive (ipynb/PDF)
Lazy->>Resolver: _resolve_eagerly(element)
alt Sync callable
Resolver->>Resolver: element() invoke
Resolver-->>Html: as_html(result) returns Html
Lazy-->>Html: Return Html directly (skip widget)
Note over Html: Rendered content appears in export
else Eager value (non-callable object)
Resolver-->>Html: as_html(element) returns Html
Lazy-->>Html: Return Html directly
else Async callable / coroutine function
Resolver-->>Lazy: Return None (cannot await)
Lazy->>Widget: Fall back to marimo-lazy placeholder
Widget-->>Export: Placeholder in output
Note over Widget: No kernel to resolve it
else Sync callable raises exception
Resolver-->>Lazy: Return None (log warning)
Lazy->>Widget: Fall back to placeholder
end
else Interactive notebook (normal runtime)
Lazy->>Widget: Return marimo-lazy UIElement
Widget-->>Runner: Lazy widget inserted
Note over Widget: Kernel RPC resolves on visibility
end
Note over Export: HTML export remains unchanged
Export->>Html: Export as HTML (no is_non_interactive)
Html-->>Export: All lazy elements ship as marimo-lazy
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes static (non-interactive) exports where mo.lazy previously produced an empty <marimo-lazy> placeholder because there is no kernel available to service the lazy-load RPC during export.
Changes:
- Resolve
mo.lazyeagerly whenis_non_interactive()is enabled, returning renderedHtmldirectly for sync values/callables. - Preserve existing lazy widget behavior for interactive contexts; async lazy callables still remain placeholders in static exports.
- Add regression tests for ipynb export resolution and HTML export placeholder behavior, plus a PDF smoke-test notebook covering eager/sync/async cases.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
marimo/_plugins/stateless/lazy.py |
Adds non-interactive eager resolution path for sync lazy elements; keeps interactive lazy widget behavior. |
tests/_plugins/stateless/test_lazy.py |
Expands tests to cover interactive laziness vs non-interactive eager resolution and async fallback cases. |
tests/_server/export/test_exporter.py |
Adds export regression tests asserting ipynb resolves sync lazy content while HTML export keeps placeholders. |
marimo/_smoke_tests/pdf_export/lazy_outputs.py |
Adds a PDF export smoke-test notebook validating eager/sync/async lazy behavior in exports. |
- `mo.lazy(async_fn())` (a raw coroutine, not a callable) was bypassing the async fallback because coroutines aren't `callable()`. It would hit `as_html(element)` and render the coroutine repr instead of falling back to the lazy widget. Added an explicit `iscoroutine` check at the top of `_resolve_eagerly` plus a `raw_coroutine` case in the existing parametrized fallback test. - Docstring claimed HTML exports were non-interactive; they aren't, per the scope decision in the previous commit. Clarified to mention ipynb/PDF only and call out that HTML keeps the widget.
Light2Dark
approved these changes
May 22, 2026
|
🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.23.8-dev1 |
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.
Refs #9624.
Why
mo.lazyrendered a<marimo-lazy>placeholder whose contents werefetched via a kernel RPC. Static exports (ipynb, PDF) have no kernel,
so the placeholder rendered empty.
What
mo.lazy.__new__checksis_non_interactive()and, for synchronouselements, returns the rendered
Htmldirectly instead of constructinga lazy widget. The lazy wrapper is preserved unchanged for interactive
notebooks — laziness in the editor isn't affected.
This fixes ipynb and PDF exports, which already set
is_non_interactive()aroundrun_app_until_completion. Asynccallables can't be awaited from
__new__; they fall back to the lazywidget and stay as placeholders.
Not fixed in this PR
HTML export still ships the
<marimo-lazy>placeholder. Enablingis_non_interactive()there would also flip tables, altair, plotly,dataframes, and
mo.mdto non-interactive fallbacks, regressing theinteractive HTML export. A lazy-specific resolution path (e.g. a
narrower flag, or kernel-side resolution at export time) is a separate
follow-up.