From 4ac9ef113d4763657dc15a1471bfb7793d3bdf57 Mon Sep 17 00:00:00 2001 From: Akshay Agrawal Date: Mon, 13 May 2024 21:22:00 -0700 Subject: [PATCH 01/11] feat: marimo export ipynb Export marimo notebooks to Jupyter (ipynb) notebooks. This is a pretty-bare bones implementation that doesn't convert markdown or create/save outputs. Still, I had a need for this today -- more convenient than copy-pasting, and didn't want to go through VSCode. --- docs/guides/exporting.md | 28 +++++++++--- marimo/_cli/export/commands.py | 61 +++++++++++++++++++++++++++ marimo/_dependencies/dependencies.py | 7 ++- marimo/_server/api/endpoints/login.py | 1 + marimo/_server/export/__init__.py | 11 +++++ marimo/_server/export/exporter.py | 20 +++++++++ pyproject.toml | 2 + tests/_cli/snapshots/ipynb.txt | 12 ++++++ tests/_cli/test_cli_export.py | 15 +++++++ 9 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 tests/_cli/snapshots/ipynb.txt diff --git a/docs/guides/exporting.md b/docs/guides/exporting.md index 0a7fc8e6ea..364ac7ca3c 100644 --- a/docs/guides/exporting.md +++ b/docs/guides/exporting.md @@ -1,5 +1,11 @@ # Exporting +Export marimo notebooks to other file formats at the command line using + +``` +marimo export +``` + ## Export to static HTML Export the current view your notebook to static HTML via the notebook @@ -26,8 +32,8 @@ marimo export html notebook.py -o notebook.html --watch ## Export to a Python script -Export the notebook to a flat Python script at the command-line. -This exports the notebook in topological order, so the cells adhere to their dependency graph. +Export to a flat Python script in topological order, so the cells adhere to +their dependency graph. ```bash marimo export script notebook.py -o notebook.script.py @@ -36,14 +42,15 @@ marimo export script notebook.py -o notebook.script.py ```{admonition} Top-level await not supported :class: warning -Exporting to a flat Python script does not support top-level await. -If you have top-level await in your notebook, you can still execute the notebook as a script with `python notebook.py`. +Exporting to a flat Python script does not support top-level await. If you have +top-level await in your notebook, you can still execute the notebook as a +script with `python notebook.py`. ``` ## Export to markdown -Export the notebook to markdown at the command-line. -This exports the notebook in top to bottom order, so the cells are in the order as they appear in the notebook. +Export to markdown notebook in top to bottom order, so the cells are in the +order as they appear in the notebook. ```bash marimo export md notebook.py -o notebook.md @@ -56,3 +63,12 @@ You can also convert the markdown back to a marimo notebook: ```bash marimo convert notebook.md > notebook.py ``` + +## Export to Jupyter notebook + +Export to Jupyter notebook in topological order, so the cells adhere to +their dependency graph. + +```bash +marimo export ipynb notebook.py -o notebook.ipynb +``` diff --git a/marimo/_cli/export/commands.py b/marimo/_cli/export/commands.py index d53c5a85c2..07fb92fe18 100644 --- a/marimo/_cli/export/commands.py +++ b/marimo/_cli/export/commands.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import importlib.util from typing import TYPE_CHECKING, Callable import click @@ -9,6 +10,7 @@ from marimo._cli.parse_args import parse_args from marimo._cli.print import green from marimo._server.export import ( + export_as_ipynb, export_as_md, export_as_script, run_app_then_export_as_html, @@ -252,6 +254,65 @@ def export_callback(file_path: MarimoPath) -> str: return watch_and_export(MarimoPath(name), output, watch, export_callback) +@click.command( + help=""" +Export a marimo notebook as a Jupyter notebook in topological order. + +Example: + + \b + * marimo export ipynb notebook.py -o notebook.ipynb + +Watch for changes and regenerate the script on modification: + + \b + * marimo export script notebook.py -o notebook.ipynb --watch + +Requires nbformat to be installed. +""" +) +@click.option( + "--watch/--no-watch", + default=False, + show_default=True, + type=bool, + help=""" + Watch notebook for changes and regenerate the ipynb on modification. + If watchdog is installed, it will be used to watch the file. + Otherwise, file watcher will poll the file every 1s. + """, +) +@click.option( + "-o", + "--output", + type=str, + default=None, + help=""" + Output file to save the script to. + If not provided, the script will be printed to stdout. + """, +) +@click.argument("name", required=True) +def ipynb( + name: str, + output: str, + watch: bool, +) -> None: + """ + Export a marimo notebook as a Jupyter notebook in topological order. + """ + + def export_callback(file_path: MarimoPath) -> str: + return export_as_ipynb(file_path)[0] + + if importlib.util.find_spec("nbformat") is None: + raise ModuleNotFoundError( + "Install `nbformat` from PyPI to use marimo export ipynb" + ) + return watch_and_export(MarimoPath(name), output, watch, export_callback) + + export.add_command(html) export.add_command(script) export.add_command(md) +export.add_command(ipynb) diff --git a/marimo/_dependencies/dependencies.py b/marimo/_dependencies/dependencies.py index fa851b6842..c9b472ef9f 100644 --- a/marimo/_dependencies/dependencies.py +++ b/marimo/_dependencies/dependencies.py @@ -144,7 +144,7 @@ def has_plotly() -> bool: @staticmethod def has_matplotlib() -> bool: - """Return True if plotly is installed.""" + """Return True if matplotlib is installed.""" return importlib.util.find_spec("matplotlib") is not None @staticmethod @@ -161,3 +161,8 @@ def has_watchdog() -> bool: def has_ipython() -> bool: """Return True if IPython is installed.""" return importlib.util.find_spec("IPython") is not None + + @staticmethod + def has_nbformat() -> bool: + """Return True if nbformat is installed.""" + return importlib.util.find_spec("nbformat") is not None diff --git a/marimo/_server/api/endpoints/login.py b/marimo/_server/api/endpoints/login.py index a2c4f4fab9..60ae38025f 100644 --- a/marimo/_server/api/endpoints/login.py +++ b/marimo/_server/api/endpoints/login.py @@ -1,3 +1,4 @@ +# Copyright 2024 Marimo. All rights reserved. from __future__ import annotations from typing import TYPE_CHECKING diff --git a/marimo/_server/export/__init__.py b/marimo/_server/export/__init__.py index 9fc8ee972d..b603c93959 100644 --- a/marimo/_server/export/__init__.py +++ b/marimo/_server/export/__init__.py @@ -40,6 +40,17 @@ def export_as_md( return Exporter().export_as_md(file_manager) +def export_as_ipynb( + path: MarimoPath, +) -> tuple[str, str]: + file_router = AppFileRouter.from_filename(path) + file_key = file_router.get_unique_file_key() + assert file_key is not None + file_manager = file_router.get_file_manager(file_key) + + return Exporter().export_as_ipynb(file_manager) + + async def run_app_then_export_as_html( path: MarimoPath, include_code: bool, diff --git a/marimo/_server/export/exporter.py b/marimo/_server/export/exporter.py index b100e26d4a..5b0e363b0c 100644 --- a/marimo/_server/export/exporter.py +++ b/marimo/_server/export/exporter.py @@ -1,6 +1,7 @@ # Copyright 2024 Marimo. All rights reserved. from __future__ import annotations +import io import mimetypes import os from typing import cast @@ -119,6 +120,25 @@ def export_as_script( download_filename = get_download_filename(file_manager, ".script.py") return code, download_filename + def export_as_ipynb( + self, + file_manager: AppFileManager, + ) -> tuple[str, str]: + import nbformat + + notebook = nbformat.v4.new_notebook() # type: ignore + graph = file_manager.app.graph + notebook["cells"] = [ + nbformat.v4.new_code_cell(graph.cells[cid].code) # type: ignore + for cid in dataflow.topological_sort(graph, graph.cells.keys()) + ] + + stream = io.StringIO() + nbformat.write(notebook, stream) # type: ignore + stream.seek(0) + download_filename = get_download_filename(file_manager, ".ipynb") + return stream.read(), download_filename + def export_as_md(self, file_manager: AppFileManager) -> tuple[str, str]: import yaml diff --git a/pyproject.toml b/pyproject.toml index 58b9df62e5..f65ab556da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -132,6 +132,8 @@ testoptional = [ "anywidget~=0.9.3", "ipython~=8.12.3", "openai~=1.12.0", + # exporting as ipynb + "nbformat >= 5.0.0", ] [project.urls] diff --git a/tests/_cli/snapshots/ipynb.txt b/tests/_cli/snapshots/ipynb.txt new file mode 100644 index 0000000000..dacd3c295f --- /dev/null +++ b/tests/_cli/snapshots/ipynb.txt @@ -0,0 +1,12 @@ +--- +title: Notebook +marimo-version: 0.5.2 +--- + +```{.python.marimo} +import marimo as mo +``` + +```{.python.marimo} +slider = mo.ui.slider(0, 10) +``` diff --git a/tests/_cli/test_cli_export.py b/tests/_cli/test_cli_export.py index 84ef3dd6da..f0d436ec04 100644 --- a/tests/_cli/test_cli_export.py +++ b/tests/_cli/test_cli_export.py @@ -434,3 +434,18 @@ def test_export_watch_script_no_out_dir( in line ) break + + +class TestExportIpynb: + @pytest.mark.skipif( + not DependencyManager.has_nbformat(), + reason="This test requires nbformat.", + ) + def test_export_ipynb(self, temp_marimo_file: str) -> None: + p = subprocess.run( + ["marimo", "export", "md", temp_marimo_file], + capture_output=True, + ) + assert p.returncode == 0, p.stderr.decode() + output = p.stdout.decode() + snapshot("ipynb.txt", output) From d6e48f63067a4e15fb823f153bb4afe7a3e8e6fb Mon Sep 17 00:00:00 2001 From: Akshay Agrawal Date: Mon, 13 May 2024 21:25:54 -0700 Subject: [PATCH 02/11] fix comments --- marimo/_cli/export/commands.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/marimo/_cli/export/commands.py b/marimo/_cli/export/commands.py index 07fb92fe18..e138dcd8b8 100644 --- a/marimo/_cli/export/commands.py +++ b/marimo/_cli/export/commands.py @@ -266,7 +266,7 @@ def export_callback(file_path: MarimoPath) -> str: Watch for changes and regenerate the script on modification: \b - * marimo export script notebook.py -o notebook.ipynb --watch + * marimo export ipynb notebook.py -o notebook.ipynb --watch Requires nbformat to be installed. """ @@ -288,8 +288,8 @@ def export_callback(file_path: MarimoPath) -> str: type=str, default=None, help=""" - Output file to save the script to. - If not provided, the script will be printed to stdout. + Output file to save the ipynb file to. If not provided, the ipynb contents + will be printed to stdout. """, ) @click.argument("name", required=True) From 31fe5437fb0828de75f8a8f10aef7e7e9d882552 Mon Sep 17 00:00:00 2001 From: Akshay Agrawal Date: Tue, 14 May 2024 12:11:06 -0700 Subject: [PATCH 03/11] fix test --- tests/_cli/snapshots/ipynb.txt | 38 ++++++++++++++++++++++++---------- tests/_cli/test_cli_export.py | 2 +- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/tests/_cli/snapshots/ipynb.txt b/tests/_cli/snapshots/ipynb.txt index dacd3c295f..2ab21170d0 100644 --- a/tests/_cli/snapshots/ipynb.txt +++ b/tests/_cli/snapshots/ipynb.txt @@ -1,12 +1,28 @@ ---- -title: Notebook -marimo-version: 0.5.2 ---- +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "cee3eba2", + "metadata": {}, + "outputs": [], + "source": [ + "import marimo as mo" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bbb9a3bf", + "metadata": {}, + "outputs": [], + "source": [ + "slider = mo.ui.slider(0, 10)" + ] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +} -```{.python.marimo} -import marimo as mo -``` - -```{.python.marimo} -slider = mo.ui.slider(0, 10) -``` diff --git a/tests/_cli/test_cli_export.py b/tests/_cli/test_cli_export.py index f0d436ec04..069f36b750 100644 --- a/tests/_cli/test_cli_export.py +++ b/tests/_cli/test_cli_export.py @@ -443,7 +443,7 @@ class TestExportIpynb: ) def test_export_ipynb(self, temp_marimo_file: str) -> None: p = subprocess.run( - ["marimo", "export", "md", temp_marimo_file], + ["marimo", "export", "ipynb", temp_marimo_file], capture_output=True, ) assert p.returncode == 0, p.stderr.decode() From ee72546ac127a88f3e4434911ffde95f511f53f2 Mon Sep 17 00:00:00 2001 From: Akshay Agrawal Date: Tue, 14 May 2024 12:38:35 -0700 Subject: [PATCH 04/11] support markdown cells --- marimo/_server/export/exporter.py | 12 +++++++++++- tests/_cli/snapshots/ipynb.txt | 22 ++++++++++++++++++++-- tests/_cli/snapshots/markdown.txt | 6 ++++++ tests/_cli/snapshots/script.txt | 6 ++++++ tests/conftest.py | 10 ++++++++++ 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/marimo/_server/export/exporter.py b/marimo/_server/export/exporter.py index 5b0e363b0c..c4d0e067dd 100644 --- a/marimo/_server/export/exporter.py +++ b/marimo/_server/export/exporter.py @@ -7,6 +7,7 @@ from typing import cast from marimo import __version__ +from marimo._ast.cell import Cell, CellImpl from marimo._config.config import ( DEFAULT_CONFIG, DisplayConfig, @@ -126,10 +127,19 @@ def export_as_ipynb( ) -> tuple[str, str]: import nbformat + def create_notebook_cell(cell: CellImpl) -> nbformat.NotebookNode: + markdown_string = get_markdown_from_cell( + Cell(_name="__", _cell=cell), cell.code + ) + if markdown_string is not None: + return nbformat.v4.new_markdown_cell(markdown_string) # type: ignore + else: + return nbformat.v4.new_code_cell(cell.code) # type: ignore + notebook = nbformat.v4.new_notebook() # type: ignore graph = file_manager.app.graph notebook["cells"] = [ - nbformat.v4.new_code_cell(graph.cells[cid].code) # type: ignore + create_notebook_cell(graph.cells[cid]) # type: ignore for cid in dataflow.topological_sort(graph, graph.cells.keys()) ] diff --git a/tests/_cli/snapshots/ipynb.txt b/tests/_cli/snapshots/ipynb.txt index 2ab21170d0..7a518cd1b4 100644 --- a/tests/_cli/snapshots/ipynb.txt +++ b/tests/_cli/snapshots/ipynb.txt @@ -3,7 +3,7 @@ { "cell_type": "code", "execution_count": null, - "id": "cee3eba2", + "id": "0e29fba8", "metadata": {}, "outputs": [], "source": [ @@ -13,12 +13,30 @@ { "cell_type": "code", "execution_count": null, - "id": "bbb9a3bf", + "id": "d63167cc", "metadata": {}, "outputs": [], "source": [ "slider = mo.ui.slider(0, 10)" ] + }, + { + "cell_type": "markdown", + "id": "9bd0e4b0", + "metadata": {}, + "source": [ + "a markdown cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ebfd9cd", + "metadata": {}, + "outputs": [], + "source": [ + "mo.md(f\"parametrized markdown: {slider}\")" + ] } ], "metadata": {}, diff --git a/tests/_cli/snapshots/markdown.txt b/tests/_cli/snapshots/markdown.txt index 24ce211de7..90d38c3c91 100644 --- a/tests/_cli/snapshots/markdown.txt +++ b/tests/_cli/snapshots/markdown.txt @@ -10,3 +10,9 @@ import marimo as mo ```{.python.marimo} slider = mo.ui.slider(0, 10) ``` + +a markdown cell + +```{.python.marimo} +mo.md(f"parametrized markdown: {slider}") +``` diff --git a/tests/_cli/snapshots/script.txt b/tests/_cli/snapshots/script.txt index 87dc8a0a85..52193bc06d 100644 --- a/tests/_cli/snapshots/script.txt +++ b/tests/_cli/snapshots/script.txt @@ -6,3 +6,9 @@ import marimo as mo # %% slider = mo.ui.slider(0, 10) + +# %% +mo.md("a markdown cell") + +# %% +mo.md(f"parametrized markdown: {slider}") diff --git a/tests/conftest.py b/tests/conftest.py index 152729d3b2..4cc9c7ece6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -178,6 +178,16 @@ def __(mo): slider = mo.ui.slider(0, 10) return slider, + @app.cell + def __(mo): + mo.md("a markdown cell") + return + + @app.cell + def __(mo, slider): + mo.md(f"parametrized markdown: {slider}") + return + if __name__ == "__main__": app.run() """ From 5d84c1b7baa168ff8e62141c4fc6ea89e135c504 Mon Sep 17 00:00:00 2001 From: Akshay Agrawal Date: Tue, 14 May 2024 12:40:51 -0700 Subject: [PATCH 05/11] type ignore ... --- marimo/_server/export/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/marimo/_server/export/exporter.py b/marimo/_server/export/exporter.py index c4d0e067dd..833f2264b7 100644 --- a/marimo/_server/export/exporter.py +++ b/marimo/_server/export/exporter.py @@ -125,7 +125,7 @@ def export_as_ipynb( self, file_manager: AppFileManager, ) -> tuple[str, str]: - import nbformat + import nbformat # type: ignore def create_notebook_cell(cell: CellImpl) -> nbformat.NotebookNode: markdown_string = get_markdown_from_cell( From 5a05ba7ddfda28fbae1153e3ef63830dea8d5aec Mon Sep 17 00:00:00 2001 From: Akshay Agrawal Date: Tue, 14 May 2024 13:53:13 -0700 Subject: [PATCH 06/11] update snapshots --- tests/_cli/snapshots/ipynb.txt | 20 ++++++++++---------- tests/_cli/snapshots/script.txt | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/_cli/snapshots/ipynb.txt b/tests/_cli/snapshots/ipynb.txt index 7a518cd1b4..6f42826bf0 100644 --- a/tests/_cli/snapshots/ipynb.txt +++ b/tests/_cli/snapshots/ipynb.txt @@ -3,7 +3,7 @@ { "cell_type": "code", "execution_count": null, - "id": "0e29fba8", + "id": "cd2168d6", "metadata": {}, "outputs": [], "source": [ @@ -11,27 +11,27 @@ ] }, { - "cell_type": "code", - "execution_count": null, - "id": "d63167cc", + "cell_type": "markdown", + "id": "096c0c88", "metadata": {}, - "outputs": [], "source": [ - "slider = mo.ui.slider(0, 10)" + "a markdown cell" ] }, { - "cell_type": "markdown", - "id": "9bd0e4b0", + "cell_type": "code", + "execution_count": null, + "id": "158c3bbb", "metadata": {}, + "outputs": [], "source": [ - "a markdown cell" + "slider = mo.ui.slider(0, 10)" ] }, { "cell_type": "code", "execution_count": null, - "id": "6ebfd9cd", + "id": "b52b1b4a", "metadata": {}, "outputs": [], "source": [ diff --git a/tests/_cli/snapshots/script.txt b/tests/_cli/snapshots/script.txt index 52193bc06d..3bb390171d 100644 --- a/tests/_cli/snapshots/script.txt +++ b/tests/_cli/snapshots/script.txt @@ -5,10 +5,10 @@ __generated_with = "0.0.0" import marimo as mo # %% -slider = mo.ui.slider(0, 10) +mo.md("a markdown cell") # %% -mo.md("a markdown cell") +slider = mo.ui.slider(0, 10) # %% mo.md(f"parametrized markdown: {slider}") From 31e41402e19495635bb44adcd87767231060fe89 Mon Sep 17 00:00:00 2001 From: Akshay Agrawal Date: Tue, 14 May 2024 14:18:07 -0700 Subject: [PATCH 07/11] update snapshots --- tests/_cli/snapshots/ipynb.txt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/_cli/snapshots/ipynb.txt b/tests/_cli/snapshots/ipynb.txt index 6f42826bf0..4f555a5bdc 100644 --- a/tests/_cli/snapshots/ipynb.txt +++ b/tests/_cli/snapshots/ipynb.txt @@ -3,7 +3,7 @@ { "cell_type": "code", "execution_count": null, - "id": "cd2168d6", + "id": "b78825b5", "metadata": {}, "outputs": [], "source": [ @@ -11,27 +11,27 @@ ] }, { - "cell_type": "markdown", - "id": "096c0c88", + "cell_type": "code", + "execution_count": null, + "id": "cc36e50f", "metadata": {}, + "outputs": [], "source": [ - "a markdown cell" + "slider = mo.ui.slider(0, 10)" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "158c3bbb", + "cell_type": "markdown", + "id": "71098525", "metadata": {}, - "outputs": [], "source": [ - "slider = mo.ui.slider(0, 10)" + "a markdown cell" ] }, { "cell_type": "code", "execution_count": null, - "id": "b52b1b4a", + "id": "a9ca0f4a", "metadata": {}, "outputs": [], "source": [ From fe4474c44670a05de4dd50dac605f016af7e0504 Mon Sep 17 00:00:00 2001 From: Akshay Agrawal Date: Tue, 14 May 2024 14:24:46 -0700 Subject: [PATCH 08/11] dont check snapshot equality for ipynb --- tests/_cli/snapshots/ipynb.txt | 8 ++++---- tests/_cli/test_cli_export.py | 7 +++---- tests/mocks.py | 5 +++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/_cli/snapshots/ipynb.txt b/tests/_cli/snapshots/ipynb.txt index 4f555a5bdc..3e37afacb4 100644 --- a/tests/_cli/snapshots/ipynb.txt +++ b/tests/_cli/snapshots/ipynb.txt @@ -3,7 +3,7 @@ { "cell_type": "code", "execution_count": null, - "id": "b78825b5", + "id": "b97f6c04", "metadata": {}, "outputs": [], "source": [ @@ -13,7 +13,7 @@ { "cell_type": "code", "execution_count": null, - "id": "cc36e50f", + "id": "70821312", "metadata": {}, "outputs": [], "source": [ @@ -22,7 +22,7 @@ }, { "cell_type": "markdown", - "id": "71098525", + "id": "ac7010cc", "metadata": {}, "source": [ "a markdown cell" @@ -31,7 +31,7 @@ { "cell_type": "code", "execution_count": null, - "id": "a9ca0f4a", + "id": "f89690c1", "metadata": {}, "outputs": [], "source": [ diff --git a/tests/_cli/test_cli_export.py b/tests/_cli/test_cli_export.py index 069f36b750..0a8a4a728a 100644 --- a/tests/_cli/test_cli_export.py +++ b/tests/_cli/test_cli_export.py @@ -140,9 +140,7 @@ def test_export_watch_no_out_dir(self, temp_marimo_file: str) -> None: class TestExportHtmlSmokeTests: - def assert_not_errored( - self, p: subprocess.CompletedProcess[bytes] - ) -> None: + def assert_not_errored(self, p: subprocess.CompletedProcess[bytes]) -> None: assert p.returncode == 0 assert not any( line.startswith("Traceback") @@ -448,4 +446,5 @@ def test_export_ipynb(self, temp_marimo_file: str) -> None: ) assert p.returncode == 0, p.stderr.decode() output = p.stdout.decode() - snapshot("ipynb.txt", output) + # ipynb has non-deterministic ids + snapshot("ipynb.txt", output, assert_equal=False) diff --git a/tests/mocks.py b/tests/mocks.py index 110bc5e6e8..1ccd5d709e 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -18,7 +18,7 @@ def snapshotter(current_file: str) -> Callable[[str, str], None]: but the snapshot will be updated with the new result. """ - def snapshot(filename: str, result: str) -> None: + def snapshot(filename: str, result: str, assert_equal: bool = True) -> None: filepath = os.path.join( os.path.dirname(current_file), "snapshots", filename ) @@ -57,6 +57,7 @@ def normalize(string: str) -> str: ) ) - assert result == expected, f"Snapshot differs:\n{text_diff}" + if assert_equal: + assert result == expected, f"Snapshot differs:\n{text_diff}" return snapshot From 3494c08e3ad16074848a8cee8833bda99e1d3f62 Mon Sep 17 00:00:00 2001 From: Akshay Agrawal Date: Tue, 14 May 2024 14:33:08 -0700 Subject: [PATCH 09/11] fix nondeterminism in snapshot ... --- tests/_cli/snapshots/ipynb.txt | 20 ++++--------- tests/_cli/snapshots/markdown.txt | 6 ---- tests/_cli/snapshots/script.txt | 6 ---- tests/_cli/test_cli_export.py | 4 +-- tests/conftest.py | 48 ++++++++++++++++++++++++------- 5 files changed, 45 insertions(+), 39 deletions(-) diff --git a/tests/_cli/snapshots/ipynb.txt b/tests/_cli/snapshots/ipynb.txt index 3e37afacb4..ac3de618a1 100644 --- a/tests/_cli/snapshots/ipynb.txt +++ b/tests/_cli/snapshots/ipynb.txt @@ -3,7 +3,7 @@ { "cell_type": "code", "execution_count": null, - "id": "b97f6c04", + "id": "985326b3", "metadata": {}, "outputs": [], "source": [ @@ -13,29 +13,19 @@ { "cell_type": "code", "execution_count": null, - "id": "70821312", + "id": "aba12f0d", "metadata": {}, "outputs": [], "source": [ - "slider = mo.ui.slider(0, 10)" + "mo.md(f\"parametrized markdown {123}\")" ] }, { "cell_type": "markdown", - "id": "ac7010cc", + "id": "bd665075", "metadata": {}, "source": [ - "a markdown cell" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f89690c1", - "metadata": {}, - "outputs": [], - "source": [ - "mo.md(f\"parametrized markdown: {slider}\")" + "markdown" ] } ], diff --git a/tests/_cli/snapshots/markdown.txt b/tests/_cli/snapshots/markdown.txt index 90d38c3c91..24ce211de7 100644 --- a/tests/_cli/snapshots/markdown.txt +++ b/tests/_cli/snapshots/markdown.txt @@ -10,9 +10,3 @@ import marimo as mo ```{.python.marimo} slider = mo.ui.slider(0, 10) ``` - -a markdown cell - -```{.python.marimo} -mo.md(f"parametrized markdown: {slider}") -``` diff --git a/tests/_cli/snapshots/script.txt b/tests/_cli/snapshots/script.txt index 3bb390171d..87dc8a0a85 100644 --- a/tests/_cli/snapshots/script.txt +++ b/tests/_cli/snapshots/script.txt @@ -4,11 +4,5 @@ __generated_with = "0.0.0" # %% import marimo as mo -# %% -mo.md("a markdown cell") - # %% slider = mo.ui.slider(0, 10) - -# %% -mo.md(f"parametrized markdown: {slider}") diff --git a/tests/_cli/test_cli_export.py b/tests/_cli/test_cli_export.py index 0a8a4a728a..a7e35cb49e 100644 --- a/tests/_cli/test_cli_export.py +++ b/tests/_cli/test_cli_export.py @@ -439,9 +439,9 @@ class TestExportIpynb: not DependencyManager.has_nbformat(), reason="This test requires nbformat.", ) - def test_export_ipynb(self, temp_marimo_file: str) -> None: + def test_export_ipynb(self, temp_marimo_file_with_md: str) -> None: p = subprocess.run( - ["marimo", "export", "ipynb", temp_marimo_file], + ["marimo", "export", "ipynb", temp_marimo_file_with_md], capture_output=True, ) assert p.returncode == 0, p.stderr.decode() diff --git a/tests/conftest.py b/tests/conftest.py index 4cc9c7ece6..bf15b53830 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -178,16 +178,6 @@ def __(mo): slider = mo.ui.slider(0, 10) return slider, - @app.cell - def __(mo): - mo.md("a markdown cell") - return - - @app.cell - def __(mo, slider): - mo.md(f"parametrized markdown: {slider}") - return - if __name__ == "__main__": app.run() """ @@ -272,6 +262,44 @@ def __(): tmp_dir.cleanup() +@pytest.fixture +def temp_marimo_file_with_md() -> Generator[str, None, None]: + tmp_dir = TemporaryDirectory() + tmp_file = tmp_dir.name + "/notebook.py" + content = inspect.cleandoc( + """ + import marimo + app = marimo.App() + + @app.cell + def __(): + import marimo as mo + return mo, + + @app.cell + def __(mo): + mo.md("markdown") + return + + @app.cell + def __(mo): + mo.md(f"parametrized markdown {123}") + return + + + if __name__ == "__main__": + app.run() + """ + ) + + try: + with open(tmp_file, "w") as f: + f.write(content) + yield tmp_file + finally: + tmp_dir.cleanup() + + # Factory to create ExecutionRequests and abstract away cell ID class ExecReqProvider: def __init__(self) -> None: From a66384b170698776983f1cd98fbc9c3e6e562ba1 Mon Sep 17 00:00:00 2001 From: Akshay Agrawal Date: Tue, 14 May 2024 14:35:28 -0700 Subject: [PATCH 10/11] lint --- tests/_cli/test_cli_export.py | 4 +++- tests/mocks.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/_cli/test_cli_export.py b/tests/_cli/test_cli_export.py index a7e35cb49e..3b6caf3b8f 100644 --- a/tests/_cli/test_cli_export.py +++ b/tests/_cli/test_cli_export.py @@ -140,7 +140,9 @@ def test_export_watch_no_out_dir(self, temp_marimo_file: str) -> None: class TestExportHtmlSmokeTests: - def assert_not_errored(self, p: subprocess.CompletedProcess[bytes]) -> None: + def assert_not_errored( + self, p: subprocess.CompletedProcess[bytes] + ) -> None: assert p.returncode == 0 assert not any( line.startswith("Traceback") diff --git a/tests/mocks.py b/tests/mocks.py index 1ccd5d709e..be822c66ad 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -18,7 +18,9 @@ def snapshotter(current_file: str) -> Callable[[str, str], None]: but the snapshot will be updated with the new result. """ - def snapshot(filename: str, result: str, assert_equal: bool = True) -> None: + def snapshot( + filename: str, result: str, assert_equal: bool = True + ) -> None: filepath = os.path.join( os.path.dirname(current_file), "snapshots", filename ) From a5193be7532546757ab8e0abdddb2b384bbbd954 Mon Sep 17 00:00:00 2001 From: Akshay Agrawal Date: Tue, 14 May 2024 23:39:58 -0700 Subject: [PATCH 11/11] provide ids to exported ipynb --- marimo/_server/export/exporter.py | 6 ++++-- tests/_cli/snapshots/ipynb.txt | 18 +++++++++--------- tests/_cli/test_cli_export.py | 2 +- tests/mocks.py | 8 ++------ 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/marimo/_server/export/exporter.py b/marimo/_server/export/exporter.py index 833f2264b7..43b5dbd815 100644 --- a/marimo/_server/export/exporter.py +++ b/marimo/_server/export/exporter.py @@ -132,9 +132,11 @@ def create_notebook_cell(cell: CellImpl) -> nbformat.NotebookNode: Cell(_name="__", _cell=cell), cell.code ) if markdown_string is not None: - return nbformat.v4.new_markdown_cell(markdown_string) # type: ignore + return nbformat.v4.new_markdown_cell( # type: ignore + markdown_string, id=cell.cell_id + ) else: - return nbformat.v4.new_code_cell(cell.code) # type: ignore + return nbformat.v4.new_code_cell(cell.code, id=cell.cell_id) # type: ignore notebook = nbformat.v4.new_notebook() # type: ignore graph = file_manager.app.graph diff --git a/tests/_cli/snapshots/ipynb.txt b/tests/_cli/snapshots/ipynb.txt index ac3de618a1..b51916d9d1 100644 --- a/tests/_cli/snapshots/ipynb.txt +++ b/tests/_cli/snapshots/ipynb.txt @@ -3,7 +3,7 @@ { "cell_type": "code", "execution_count": null, - "id": "985326b3", + "id": "Hbol", "metadata": {}, "outputs": [], "source": [ @@ -11,21 +11,21 @@ ] }, { - "cell_type": "code", - "execution_count": null, - "id": "aba12f0d", + "cell_type": "markdown", + "id": "MJUe", "metadata": {}, - "outputs": [], "source": [ - "mo.md(f\"parametrized markdown {123}\")" + "markdown" ] }, { - "cell_type": "markdown", - "id": "bd665075", + "cell_type": "code", + "execution_count": null, + "id": "vblA", "metadata": {}, + "outputs": [], "source": [ - "markdown" + "mo.md(f\"parametrized markdown {123}\")" ] } ], diff --git a/tests/_cli/test_cli_export.py b/tests/_cli/test_cli_export.py index 3b6caf3b8f..5016a80449 100644 --- a/tests/_cli/test_cli_export.py +++ b/tests/_cli/test_cli_export.py @@ -449,4 +449,4 @@ def test_export_ipynb(self, temp_marimo_file_with_md: str) -> None: assert p.returncode == 0, p.stderr.decode() output = p.stdout.decode() # ipynb has non-deterministic ids - snapshot("ipynb.txt", output, assert_equal=False) + snapshot("ipynb.txt", output) diff --git a/tests/mocks.py b/tests/mocks.py index be822c66ad..9c83e929db 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -18,9 +18,7 @@ def snapshotter(current_file: str) -> Callable[[str, str], None]: but the snapshot will be updated with the new result. """ - def snapshot( - filename: str, result: str, assert_equal: bool = True - ) -> None: + def snapshot(filename: str, result: str) -> None: filepath = os.path.join( os.path.dirname(current_file), "snapshots", filename ) @@ -58,8 +56,6 @@ def normalize(string: str) -> str: ) ) ) - - if assert_equal: - assert result == expected, f"Snapshot differs:\n{text_diff}" + assert result == expected, f"Snapshot differs:\n{text_diff}" return snapshot