marimo icon indicating copy to clipboard operation
marimo copied to clipboard

Feature Request: Encapsulating Multiple Cells into Reusable Components

Open HikariIce opened this issue 1 year ago • 10 comments

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:

  • Create a library of components: Store these components in a central place, such as a Python module or package.
  • Import and use components across notebooks: Easily import these components into any notebook where they are needed, without having to rewrite the code.
  • Maintain and update components: Update the components in one place and have the changes reflected in all notebooks where they are used.
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:

  • Define components: Define a cell or group of cells as a component.
  • Export components: Export these components to a Python library or a similar format.
  • Import components: Import these components into any notebook seamlessly.

Alternative

No response

Additional context

No response

HikariIce avatar Jul 30 '24 03:07 HikariIce

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

akshayka avatar Jul 30 '24 03:07 akshayka

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

HikariIce avatar Jul 30 '24 04:07 HikariIce

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

Thank you!

akshayka avatar Jul 30 '24 04:07 akshayka

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()

HikariIce avatar Jul 30 '24 09:07 HikariIce

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?

akshayka avatar Jul 31 '24 19:07 akshayka

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.

HikariIce avatar Aug 01 '24 01:08 HikariIce

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

majidaldo avatar Aug 09 '24 16:08 majidaldo

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()

ggggggggg avatar Aug 22 '24 20:08 ggggggggg

Not sure if it is the right place to ask, but how can we pass data from the top-level app to embedded apps?

chenlijun99 avatar Jul 26 '25 10:07 chenlijun99

Not sure if it is the right place to ask, but how can we pass data from the top-level app to embedded apps?

I have the same question.

mthiboust avatar Nov 03 '25 12:11 mthiboust