marimo-team / marimo

A reactive notebook for Python — run reproducible experiments, execute as a script, deploy as an app, and version with git.
https://marimo.io
Apache License 2.0
6.14k stars 200 forks source link

Feature Request: Encapsulating Multiple Cells into Reusable Components #1910

Open HikariIce opened 1 month ago

HikariIce commented 1 month ago

Description

I'm working on several marimo notebooks where I find myself repeatedly using similar code blocks or cells across different projects. These cells often perform common tasks such as data preprocessing, visualization, or model training.

My question is whether there is a standard or best practice method to encapsulate these cells into reusable components. Ideally, I'd like to be able to:

import marimo

__generated_with = "0.7.12"
app = marimo.App(width="medium")

@app.cell
def __(mo):
    mo.md(r"""# A state counter""")
    return

@app.cell
def __(mo):
    get_state, set_state = mo.state(0)
    return get_state, set_state

@app.cell
def __(get_state, mo, set_state):
    slider = mo.ui.slider(0, 100, value=get_state(), on_change=set_state)
    return slider,

@app.cell
def __(get_state, mo, set_state):
    number = mo.ui.number(0, 100, value=get_state(), on_change=set_state)
    return number,

@app.cell
def __(number, slider):
    [slider, number]
    return

@app.cell
def __(mo):
    mo.md(r"""# If I need another one, I should:""")
    return

@app.cell
def __(mo):
    get_state1, set_state1 = mo.state(0)
    return get_state1, set_state1

@app.cell
def __(get_state1, mo, set_state1):
    slider1 = mo.ui.slider(0, 100, value=get_state1(), on_change=set_state1)
    return slider1,

@app.cell
def __(get_state1, mo, set_state1):
    number1 = mo.ui.number(0, 100, value=get_state1(), on_change=set_state1)
    return number1,

@app.cell
def __(number1, slider1):
    [slider1, number1]
    return

@app.cell
def __():
    import marimo as mo
    return mo,

if __name__ == "__main__":
    app.run()

Suggested solution

Ideally, I'm looking for a way to:

Alternative

No response

Additional context

No response

akshayka commented 1 month ago

Thanks for the thoughtful feature request.

The app.embed API is designed for this purpose. This API was quietly released and hasn't gotten widespread use yet. I would appreciate feedback on whether it's useful to you — it's early enough that we would be open to iterating on it if needed. API: https://docs.marimo.io/api/app.html#marimo.App.embed

We also have the cell.run API for running individual cells: https://docs.marimo.io/api/cell.html#marimo.Cell.run

HikariIce commented 1 month ago

Thank you for your reply. I will try and give you feedback

akshayka commented 1 month ago

Thank you for your reply. I will try and give you feedback

Thank you!

HikariIce commented 1 month ago

Is there a way to prevent two embeddings from sharing state?And I don't know why I can't import count from a.py image

a.py:

import marimo

__generated_with = "0.7.12"
app = marimo.App(width="medium")

@app.cell
def __(mo):
    get_state, set_state = mo.state(0)
    return get_state, set_state

@app.cell
def __(get_state, mo, set_state):
    slider = mo.ui.slider(0, 100, value=get_state(), on_change=set_state)
    return slider,

@app.cell
def __(get_state, mo, set_state):
    number = mo.ui.number(0, 100, value=get_state(), on_change=set_state)
    return number,

@app.cell
def count(number, slider):
    [slider, number]
    return

@app.cell
def __():
    import marimo as mo
    return mo,

if __name__ == "__main__":
    app.run()

b.py:

import marimo

__generated_with = "0.7.14"
app = marimo.App(width="medium")

@app.cell
def __():
    import marimo as mo
    from a import app
    return app, mo

@app.cell
async def __(app):
    count1 = await app.embed()
    return count1,

@app.cell
def __(count1):
    count1.output
    return

@app.cell
async def __(app):
    count2 = await app.embed()
    return count2,

@app.cell
def __(count2):
    count2.output
    return

if __name__ == "__main__":
    app.run()
akshayka commented 1 month ago

I don't know why I can't import count from a.py

Hmm, I'm not sure why that isn't working for you. I tried and it works for me at least.

Can you share what you see for sys.path in the notebook where you're trying to import count from a?

Is there a way to prevent two embeddings from sharing state?

No that is not currently possible. But it makes sense why you would want that. I suppose your app is a small component that you would like to reuse multiple times in the same notebook? Or what is your use case?

HikariIce commented 1 month ago

I don't know why I can't import count from a.py

it's ok, maybe sth. goes wrong

your app is a small component that you would like to reuse multiple times in the same notebook

yes, you got me.

majidaldo commented 1 month ago

...what about code generation so that marimo could be taken out as a dependency in the generated code.?

ggggggggg commented 3 weeks ago

I have a closely related request. I want to write a class that has a method moshow that when returned, shows a ui input element (say dropdown) and some data based on the selection in that element (say plot a member variable of this class). I can do it with two cells, as shown below the first contains dropdown = db.make_dropdown() while the second contains contains moshow_explicit(dropdown). But that makes it hard to remember how to call, and slow to call during interactive use, plus to you have to make up a new name for dropdown each time. Imagine that this component is called up for debugging, but is often not desired.

Two uses of moshow in the same notebook should not share state and would potentially have totally separate data or represent different views on the same data.

It feels very close to embeding, I just want to define everything in the class rather than import another notebook. I wrote moshow imagining how the syntax might look.

import marimo

__generated_with = "0.8.0"
app = marimo.App(width="medium")

@app.cell
def __():
    import marimo as mo
    import numpy as np
    import pylab as plt
    from dataclasses import dataclass
    import typing
    return dataclass, mo, np, plt, typing

@app.cell
def __(dataclass, mo, np, plt):
    @dataclass
    class DataBundle:
        data1: list[str]
        data2: list

        def make_dropdown(self):
            return mo.ui.dropdown(self.data1, value=self.data1[0])

        def moshow_explicit(self, dropdown):
            ind = self.data1.index(dropdown.value)
            self.plot(ind)
            return mo.vstack([dropdown, mo.mpl.interactive(plt.gcf())])

        def plot(self, ind):
            plt.plot(db.data2[ind])
            plt.xlabel("x")
            plt.ylabel("y")
            plt.title(db.data1[ind])

        def moshow(self):
            app = mo.App()
            @app.cell(hidden=True)
            def first_cell():
                dropdown = self.make_dropdown()
                return dropdown
            @app.cell
            def second_cell(self, dropdown):
                return self.moshow_explicit(dropdown)
            return app

    db = DataBundle(
        ["linear", "sin", "cos", "square"],
        [
            np.arange(100),
            np.sin(np.arange(100) / np.pi),
            np.cos(np.arange(100) / np.pi),
            np.arange(100) ** 2,
        ],
    )
    return DataBundle, db

@app.cell
def __(db):
    dropdown = db.make_dropdown()
    return dropdown,

@app.cell
def __(db, dropdown):
    db.moshow_explicit(dropdown)
    return

@app.cell
def __(db):
    db.moshow()
    return

if __name__ == "__main__":
    app.run()