Skip to content

fix(lazy): resolve mo.lazy eagerly in non-interactive exports#9644

Merged
Light2Dark merged 2 commits into
mainfrom
ms/fix/issue-9624
May 22, 2026
Merged

fix(lazy): resolve mo.lazy eagerly in non-interactive exports#9644
Light2Dark merged 2 commits into
mainfrom
ms/fix/issue-9624

Conversation

@mscolnick
Copy link
Copy Markdown
Contributor

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.

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.
Copilot AI review requested due to automatic review settings May 21, 2026 15:14
@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

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

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment May 21, 2026 5:00pm

Request Review

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread marimo/_plugins/stateless/lazy.py
Copy link
Copy Markdown
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

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.lazy eagerly when is_non_interactive() is enabled, returning rendered Html directly 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.

Comment thread marimo/_plugins/stateless/lazy.py Outdated
@mscolnick mscolnick added the bug Something isn't working label May 21, 2026
@mscolnick mscolnick requested a review from Light2Dark May 21, 2026 15:37
- `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 Light2Dark merged commit cbde228 into main May 22, 2026
40 of 43 checks passed
@Light2Dark Light2Dark deleted the ms/fix/issue-9624 branch May 22, 2026 03:02
@github-actions
Copy link
Copy Markdown

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

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants