Repository: marimo-team/marimo

Issue:
Clone app objects to enable multiple embeddings
Make it possible to clone an `app` object, so that the same app can be embedded multiple times (independently) in the same notebook.

> I have another question (not sure if I can ask here or should open another request): I made a notebook (processor.py) that imports data from a file and processes them.
> I need to process three files at the same time, to quickly compare the results, so I tried to import processor.py in another notebook (main.py) like that:
> 
> ```
> from processor import app
> 
> processor1 = app.embed()
> processor2 = app.embed()
> processor3 = app.embed()
> ```
> 
> 
>  but the output is the same, as the three processor were not different instances of app, but just three placeholders.
> My workaround is to create three copies of processor.py and import them separately: is there any other way to obtain the same result? 

 _Originally posted by @pcomepaolo in [#3386](https://github.com/marimo-team/marimo/issues/3386#issuecomment-2582001444)_

Discussion:

Solution patch:
diff --git a/marimo/_ast/app.py b/marimo/_ast/app.py
index baaeec6b..f5245978 100644
--- a/marimo/_ast/app.py
+++ b/marimo/_ast/app.py
@@ -21,7 +21,7 @@ from typing import (
 from uuid import uuid4
 
 from marimo import _loggers
-from marimo._ast.cell import Cell, CellConfig, CellId_t
+from marimo._ast.cell import Cell, CellConfig, CellId_t, CellImpl
 from marimo._ast.cell_manager import CellManager
 from marimo._ast.errors import (
     CycleError,
@@ -177,6 +177,46 @@ class App:
             ...
         self._app_kernel_runner: AppKernelRunner | None = None
 
+    def clone(self) -> App:
+        """Clone an app to embed multiple copies of it.
+
+        Utility method to clone an app object; use with `embed()` to create
+        independent copies of apps.
+
+        Returns:
+            A new `app` object with the same code.
+        """
+        app = App()
+        app._cell_manager = CellManager(prefix=str(uuid4()))
+        for cell_id, code, name, config in zip(
+            self._cell_manager.cell_ids(),
+            self._cell_manager.codes(),
+            self._cell_manager.names(),
+            self._cell_manager.configs(),
+        ):
+            cell = None
+            # If the cell exists, the cell data should be set.
+            cell_data = self._cell_manager._cell_data.get(cell_id)
+            new_cell_id = app._cell_manager.create_cell_id()
+            if cell_data is not None:
+                cell = cell_data.cell
+                if cell is not None:
+                    new_cell = Cell(
+                        _name=cell.name,
+                        _cell=CellImpl(
+                            **{**cell._cell.__dict__, "cell_id": new_cell_id}
+                        ),
+                        _app=InternalApp(app),
+                    )
+                    app._cell_manager.register_cell(
+                        cell_id=new_cell_id,
+                        code=code,
+                        name=name,
+                        config=config,
+                        cell=new_cell,
+                    )
+        return app
+
     def cell(
         self,
         func: Callable[..., Any] | None = None,
@@ -351,10 +391,19 @@ class App:
         The `embed` method lets you embed the output of a notebook
         into another notebook and access the values of its variables.
 
-        Returns:
-            An object `result` with two attributes: `result.output` (visual
-            output of the notebook) and `result.defs` (a dictionary mapping
-            variable names defined by the notebook to their values).
+        Running `await app.embed()` executes the notebook and results an object
+        encapsulating the notebook visual output and its definitions.
+
+        Embedded notebook outputs are interactive: when you interact with
+        UI elements in an embedded notebook's output, any cell referring
+        to the `app` object other than the one that imported it is marked for
+        execution, and its internal state is automatically updated. This lets
+        you use notebooks as building blocks or components to create
+        higher-level notebooks.
+
+        Multiple levels of nesting are supported: it's possible to embed a
+        notebook that in turn embeds another notebook, and marimo will do the
+        right thing.
 
         Example:
             ```python
@@ -377,19 +426,27 @@ class App:
             result.defs
             ```
 
-            Running `await app.embed()` executes the notebook and results an object
-            encapsulating the notebook visual output and its definitions.
+        To embed independent copies of same app object, first clone the
+        app with `app.clone()`:
+
+            ```python
+            from my_notebook import app
+            ```
+
+            ```python
+            one = app.clone()
+            r1 = await one.embed()
+            ```
+
+            ```python
+            two = app.clone()
+            r2 = await two.embed()
 
-            Embedded notebook outputs are interactive: when you interact with
-            UI elements in an embedded notebook's output, any cell referring
-            to the `app` object other than the one that imported it is marked for
-            execution, and its internal state is automatically updated. This lets
-            you use notebooks as building blocks or components to create
-            higher-level notebooks.
+        Returns:
+            An object `result` with two attributes: `result.output` (visual
+            output of the notebook) and `result.defs` (a dictionary mapping
+            variable names defined by the notebook to their values).
 
-            Multiple levels of nesting are supported: it's possible to embed a
-            notebook that in turn embeds another notebook, and marimo will do the
-            right thing.
         """
         from marimo._plugins.stateless.flex import vstack
         from marimo._runtime.context.utils import running_in_notebook
diff --git a/marimo/_utils/data_uri.py b/marimo/_utils/data_uri.py
index 6428c752..7300549b 100644
--- a/marimo/_utils/data_uri.py
+++ b/marimo/_utils/data_uri.py
@@ -1,3 +1,4 @@
+# Copyright 2025 Marimo. All rights reserved.
 from __future__ import annotations
 
 import base64
diff --git a/pyproject.toml b/pyproject.toml
index dd091421..0d49971a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -143,11 +143,13 @@ path = "marimo/__init__.py"
 include = ["/marimo"]
 artifacts = ["marimo/_static/", "marimo/_lsp/", "third_party.txt", "third_party_licenses.txt"]
 exclude = ["marimo/_smoke_tests"]
+force-include = { "build_hook.py" = "build_hook.py" }
 
 [tool.hatch.build.targets.wheel]
 include = ["/marimo"]
 artifacts = ["marimo/_static/", "marimo/_lsp/", "third_party.txt", "third_party_licenses.txt"]
 exclude = ["marimo/_smoke_tests"]
+force-include = { "build_hook.py" = "build_hook.py" }
 
 # Override the default uv version to use the latest version
 [tool.hatch.envs.hatch-uv]


Test patch:
diff --git a/tests/_ast/test_app.py b/tests/_ast/test_app.py
index eeb7de14..e59de6ef 100644
--- a/tests/_ast/test_app.py
+++ b/tests/_ast/test_app.py
@@ -8,7 +8,7 @@ import textwrap
 from typing import Any
 
 import pytest
-from marimo._ast.app import App, _AppConfig
+from marimo._ast.app import App, _AppConfig, InternalApp
 from marimo._ast.errors import (
     CycleError,
     DeleteNonlocalError,
@@ -526,6 +526,28 @@ class TestApp:
         assert location is not None
         assert dirpath == location
 
+    def test_app_clone(self) -> None:
+        app = App()
+
+        @app.cell
+        def __():
+            import marimo as mo
+
+            dirpath = mo.notebook_dir()
+            location = mo.notebook_location()
+
+        # same codes and names, different cell_ids
+        clone = app.clone()
+        assert list(InternalApp(clone).cell_manager.codes()) == list(
+            InternalApp(app).cell_manager.codes()
+        )
+        assert list(InternalApp(clone).cell_manager.names()) == list(
+            InternalApp(app).cell_manager.names()
+        )
+        assert list(InternalApp(clone).cell_manager.cell_ids()) != list(
+            InternalApp(app).cell_manager.cell_ids()
+        )
+
 
 def test_app_config() -> None:
     config = _AppConfig.from_untrusted_dict({"width": "full"})