From cd254c9843e1c22dfc0e55f52863431cd4fc81fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Ferenc=20Gyarmati?= Date: Wed, 6 May 2026 14:03:16 +0200 Subject: [PATCH 1/6] feat: support SQL cells --- _extensions/marimo/extract.py | 13 +++++ _quarto.yml | 4 ++ lib/cell-execution-regex.ts | 8 ++- lib/is-marimo-cell.ts | 8 +-- tests/cell-execution-regex.test.ts | 20 +++++++ tests/is-marimo-cell.test.ts | 10 ++++ tests/python/test_extract.py | 20 ++++++- tutorials/sql.qmd | 94 +++++++++++++++++++++++++----- 8 files changed, 156 insertions(+), 21 deletions(-) diff --git a/_extensions/marimo/extract.py b/_extensions/marimo/extract.py index 7c85568..89bee4f 100755 --- a/_extensions/marimo/extract.py +++ b/_extensions/marimo/extract.py @@ -60,6 +60,17 @@ "editor": False, } +_PYTHON_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + + +def sql_code_to_python(code: str, query: Optional[str]) -> str: + """Convert a marimo markdown SQL cell into executable Python.""" + escaped = code.strip("\n").replace('"""', r"\"\"\"") + sql_call = f'mo.sql(fr"""\n{escaped}\n""")' + if query and _PYTHON_IDENTIFIER.match(query): + return f"{query} = {sql_call}" + return sql_call + def extract_and_strip_quarto_config(block: str) -> tuple[dict[str, Any], str]: pattern = r"^\s*\#\|\s*(.*?)\s*:\s*(.*?)(?=\n|\Z)" @@ -293,6 +304,8 @@ def tree_to_pandoc_export(root: Element) -> SafeWrap: # type: ignore[valid-type code = str(child.text) config, code = extract_and_strip_quarto_config(code) + if child.attrib.get("language") == "sql": + code = sql_code_to_python(code, child.attrib.get("query")) try: stub = app.add_code( diff --git a/_quarto.yml b/_quarto.yml index 18bd397..38d7086 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -1,6 +1,9 @@ project: type: website output-dir: _site + render: + - index.qmd + - tutorials/*.qmd post-render: python scripts/sync_local_frontend.py preview: port: 7777 @@ -31,6 +34,7 @@ website: - href: tutorials/plots.qmd - href: tutorials/layout.qmd - href: tutorials/fileformat.qmd + - href: tutorials/sql.qmd - href: tutorials/external_dependencies.qmd - href: tutorials/markdown_format.qmd - href: tutorials/for_jupyter_users.qmd diff --git a/lib/cell-execution-regex.ts b/lib/cell-execution-regex.ts index df78b24..5213ae4 100644 --- a/lib/cell-execution-regex.ts +++ b/lib/cell-execution-regex.ts @@ -7,22 +7,24 @@ * ```{python.marimo} ← pampa/dot-joined syntax * ```{python .marimo} ← preferred class syntax * ```python {.marimo} ← legacy (language outside braces) + * ```sql {.marimo} ← SQL marimo cells * * Groups: * 1: backticks (```+) - * 2: language ("python.marimo" or "python") + * 2: language ("python.marimo", "python", "sql.marimo", or "sql") */ // Matches all marimo cell syntaxes with language always in group 2: // ```{python.marimo} → group 2: "python.marimo" // ```{python .marimo} → group 2: "python" // ```python {.marimo} → group 2: "python" (legacy) +// ```sql {.marimo} → group 2: "sql" (SQL cells) // // Structure: // - Lookahead ensures .marimo appears somewhere // - \{? handles optional leading brace (present for braced syntax, absent for legacy) -// - Language capture: python or python.marimo +// - Language capture: python/sql or python.marimo/sql.marimo // - [^}]* consumes rest (classes, attributes) until closing brace // Note: accepts some invalid syntax (e.g. comma-separated) that will fail pampa parsing export const MARIMO_CELL_REGEX = - /^\s*(```+)\s*(?=.*\.marimo)\{?(python(?:\.marimo)?)[^}]*\}\s*$/; + /^\s*(```+)\s*(?=.*\.marimo)\{?((?:python|sql)(?:\.marimo)?)[^}]*\}\s*$/; diff --git a/lib/is-marimo-cell.ts b/lib/is-marimo-cell.ts index 5334407..4ff843c 100644 --- a/lib/is-marimo-cell.ts +++ b/lib/is-marimo-cell.ts @@ -11,12 +11,12 @@ export function isMarimoCell(cell: QuartoMdCell): boolean { return false; } const lang = cell.cell_type.language; - // Handle {python.marimo} syntax (quarto parses as language "python.marimo") - if (lang === "python.marimo") { + // Handle {python.marimo}/{sql.marimo} syntax. + if (lang === "python.marimo" || lang === "sql.marimo") { return true; } - // Handle {python .marimo} and legacy python {.marimo} syntax - if (lang === "python") { + // Handle class syntax and legacy language-outside-braces syntax. + if (lang === "python" || lang === "sql") { const firstLine = cell.sourceVerbatim.value.split('\n')[0] || ''; return /\.marimo/.test(firstLine); } diff --git a/tests/cell-execution-regex.test.ts b/tests/cell-execution-regex.test.ts index 42511a5..3dea186 100644 --- a/tests/cell-execution-regex.test.ts +++ b/tests/cell-execution-regex.test.ts @@ -128,6 +128,26 @@ Deno.test("python {.marimo} legacy with leading whitespace", () => { ); }); +Deno.test("sql {.marimo} legacy basic", () => { + assertMatch("```sql {.marimo}", "sql", "sql {.marimo} legacy basic"); +}); + +Deno.test("sql {.marimo} with query attribute", () => { + assertMatch( + '```sql {.marimo query="result"}', + "sql", + "sql {.marimo} with query attribute", + ); +}); + +Deno.test("{sql .marimo} class syntax", () => { + assertMatch("```{sql .marimo}", "sql", "{sql .marimo} class syntax"); +}); + +Deno.test("{sql.marimo} basic", () => { + assertMatch("```{sql.marimo}", "sql.marimo", "{sql.marimo} basic"); +}); + Deno.test("four backticks {python .marimo}", () => { assertMatch( "````{python .marimo}", diff --git a/tests/is-marimo-cell.test.ts b/tests/is-marimo-cell.test.ts index 60af89f..55a42cc 100644 --- a/tests/is-marimo-cell.test.ts +++ b/tests/is-marimo-cell.test.ts @@ -30,6 +30,16 @@ Deno.test("python {.marimo} legacy syntax returns true", () => { assertEquals(isMarimoCell(cell), true); }); +Deno.test("sql {.marimo} legacy syntax returns true", () => { + const cell = makeCell("sql", "```sql {.marimo query=\"result\"}\nSELECT 1"); + assertEquals(isMarimoCell(cell), true); +}); + +Deno.test("sql.marimo language returns true", () => { + const cell = makeCell("sql.marimo", "SELECT 1"); + assertEquals(isMarimoCell(cell), true); +}); + Deno.test("plain python without .marimo returns false", () => { const cell = makeCell("python", "print('hello')"); assertEquals(isMarimoCell(cell), false); diff --git a/tests/python/test_extract.py b/tests/python/test_extract.py index ed87334..cef70e2 100644 --- a/tests/python/test_extract.py +++ b/tests/python/test_extract.py @@ -12,6 +12,7 @@ extract_and_strip_quarto_config, get_mime_render, pyproject_to_script_metadata, + sql_code_to_python, ) @@ -59,7 +60,7 @@ def test_marimo_layout_maps_to_layout_file(self): assert config["layout_file"] == "grid.json" def test_marimo_version_stripped(self): - root = Element("root", attrib={"marimo-version": "0.14.0"}) + root = Element("root", attrib={"marimo-version": "0.23.1"}) config = app_config_from_root(root) assert "marimo-version" not in config @@ -94,6 +95,23 @@ def test_preserves_existing_script_metadata(self): assert metadata == "# /// script\n# dependencies = []\n# ///\n" +class TestSqlCodeToPython: + def test_without_query_renders_sql_expression(self): + result = sql_code_to_python("SELECT * FROM df;", None) + + assert result == 'mo.sql(fr"""\nSELECT * FROM df;\n""")' + + def test_with_query_assigns_result(self): + result = sql_code_to_python("SELECT * FROM df;", "filtered") + + assert result == 'filtered = mo.sql(fr"""\nSELECT * FROM df;\n""")' + + def test_invalid_query_falls_back_to_expression(self): + result = sql_code_to_python("SELECT * FROM df;", "not-valid") + + assert result == 'mo.sql(fr"""\nSELECT * FROM df;\n""")' + + class TestConvertFromMdToPandocExport: def test_injects_pyproject_into_exported_notebook(self): markdown = """--- diff --git a/tutorials/sql.qmd b/tutorials/sql.qmd index 58ac658..c0ed1ce 100644 --- a/tutorials/sql.qmd +++ b/tutorials/sql.qmd @@ -1,13 +1,23 @@ --- -title: Sql -marimo-version: 0.13.2 +title: SQL +marimo-version: 0.23.1 width: medium +header: |- + # Copyright 2026 Marimo. All rights reserved +pyproject: | + requires-python = ">=3.11" + dependencies = [ + "marimo[sql]>=0.23.1", + "matplotlib", + "pandas", + ] --- # Hello, SQL! _Let's dive into the world of SQL where we don't just address tables, we also join them!_ + With marimo, you can mix-and-match both **Python and SQL**. To create a SQL cell, you first need to install some additional dependencies, including [duckdb](https://duckdb.org/). Obtain these dependencies with @@ -71,10 +81,10 @@ else: Once the required dependencies are installed, you can create SQL cells in one of the following ways: -- right click the **Add Cell** ::lucide:circle-plus:: buttons on the left of +- right click the **Add Cell** buttons on the left of a cell; -- click the **Convert to SQL** ::lucide:database:: button in the cell menu ::lucide:ellipsis:: -- click the **Add SQL Cell** at the bottom of the page; +- click the **Convert to SQL** button in the cell menu +- click the **Add SQL Cell** at the bottom of the page; ## Python representation marimo is still just Python, even when using SQL. Here is an example of @@ -93,14 +103,17 @@ The SQL statement itself is an formatted string (f-string), so this means they can contain any valid Python code, such as the values of UI elements. This means your SQL statement and results can be reactive! 🚀 + ## Querying dataframes with SQL -/// Tip | "Data sources panel" - Click the database "barrel" icon in the left toolbar to see all dataframes and in- - memory tables that your notebook has access to. -/// +::: {.callout-tip title="Data sources panel"} + +Click the database "barrel" icon in the left toolbar to see all dataframes and in-memory tables that your notebook has access to. + +::: + Let's take a look at a SQL cell. The next cell generates a dataframe called `df`. ```python {.marimo hide_code="true"} @@ -156,6 +169,7 @@ SELECT * FROM df; ## From Python to SQL and back + You can create SQL statements that depend on Python values, such as UI elements: ```python {.marimo hide_code="true"} @@ -249,6 +263,7 @@ def render_plotly(): ## CSVs, Parquet, Postgres, and more ... + We're not limited to querying dataframes. We can also query an **HTTP URL, S3 path, or a file path to a local csv or parquet file**. ```sql @@ -262,12 +277,65 @@ SELECT * FROM read_parquet('path/to/example.parquet'); With a bit of boilerplate, you can even read and write to **Postgres**, and join Postgres tables with dataframes in the same query. For a full list of supported data sources, check out the [duckdb extensions](https://duckdb.org/docs/extensions/overview) and our [example notebook on duckdb connections](https://github.com/marimo-team/marimo/blob/main/examples/sql/duckdb_connections.**py**). -For this example, we will query an HTTP endpoint of a csv. +For this example, we will query a dataframe loaded from a csv. + +```python {.marimo hide_code="true"} +# We use pandas for the CSV fetch because DuckDB in Pyodide does not support HTTPFS extensions +def read_cars_df(): + import pandas as pd + + try: + return pd.read_csv("https://datasets.marimo.app/cars.csv") + except Exception: + # datasets.marimo.app returns 403 from time to time, so we add this as a fallback + return pd.DataFrame( + [ + { + "Name": "Honda Civic", + "Origin": "Asia", + "Cylinders": 4, + "MPG_City": 30, + "MPG_Highway": 38, + }, + { + "Name": "Toyota Camry", + "Origin": "Asia", + "Cylinders": 4, + "MPG_City": 28, + "MPG_Highway": 39, + }, + { + "Name": "Ford F-150", + "Origin": "USA", + "Cylinders": 6, + "MPG_City": 20, + "MPG_Highway": 26, + }, + { + "Name": "BMW 328i", + "Origin": "Europe", + "Cylinders": 4, + "MPG_City": 23, + "MPG_Highway": 34, + }, + { + "Name": "Mazda MX-5", + "Origin": "Asia", + "Cylinders": 4, + "MPG_City": 26, + "MPG_Highway": 34, + }, + ] + ) + +cars_df = read_cars_df() +``` ```sql {.marimo query="cars"} -- Download a CSV and create an in-memory table; this is optional. -CREATE OR replace TABLE cars as -FROM 'https://datasets.marimo.app/cars.csv'; +CREATE OR replace TABLE cars as ( + SELECT * FROM cars_df +); -- Query the table SELECT * from cars; @@ -314,4 +382,4 @@ import random ```python {.marimo hide_code="true"} import string -``` \ No newline at end of file +``` From e7c31b77b787936dcc49c6f418c2cf1229bddbf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Ferenc=20Gyarmati?= Date: Wed, 6 May 2026 14:03:38 +0200 Subject: [PATCH 2/6] chore: adjust quarto commands in make --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index e7ff109..25cfd53 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build test test-ts test-py lint render clean setup +.PHONY: build test test-ts test-py lint preview render clean setup VERSION := $(shell grep '^version:' _extensions/marimo/_extension.yml | sed 's/.*: *//') @@ -47,7 +47,7 @@ preview: quarto preview render: - quarto render tutorials/intro.qmd --to html + quarto render clean: rm -rf _site .quarto _extensions/marimo/marimo-engine-v*.js From 8eb2289b5729f1b119c52fe43814fff8dacae315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Ferenc=20Gyarmati?= Date: Wed, 6 May 2026 14:04:13 +0200 Subject: [PATCH 3/6] chore: pin nb versions, dependencies and copyright --- tutorials/dataflow.qmd | 35 +++++++++++++++++++++-------- tutorials/example-pdf.qmd | 2 ++ tutorials/external_dependencies.qmd | 2 ++ tutorials/fileformat.qmd | 25 +++++++++++++-------- tutorials/for_jupyter_users.qmd | 15 ++++++++++--- tutorials/index.qmd | 5 ++++- tutorials/intro.qmd | 17 +++++++++----- tutorials/layout.qmd | 9 +++++--- tutorials/markdown.qmd | 16 +++++++++++-- tutorials/markdown_format.qmd | 19 ++++++++++++++-- tutorials/plots.qmd | 17 +++++++++++--- tutorials/ui.qmd | 16 ++++++++----- 12 files changed, 134 insertions(+), 44 deletions(-) diff --git a/tutorials/dataflow.qmd b/tutorials/dataflow.qmd index 83ed652..e1744ff 100644 --- a/tutorials/dataflow.qmd +++ b/tutorials/dataflow.qmd @@ -1,6 +1,15 @@ --- title: Dataflow -marimo-version: 0.13.2 +marimo-version: 0.23.1 +header: |- + # Copyright 2026 Marimo. All rights reserved +pyproject: | + requires-python = ">=3.11" + dependencies = [ + "marimo>=0.23.1", + "matplotlib", + "numpy", + ] --- # How marimo notebooks run @@ -12,6 +21,7 @@ automatically. To provide reactive execution, marimo creates a dataflow graph out of your cells. + **Tip: disabling automatic execution.** marimo lets you disable automatic execution: just go into the notebook settings @@ -24,6 +34,7 @@ descendants as stale instead of automatically running them. The lazy runtime puts you in control over when cells are run, while still giving guarantees about the notebook state. + ## References and definitions A marimo notebook is a directed acyclic graph in which nodes represent @@ -39,6 +50,7 @@ global variables defined by the former cell. The rule for reactive execution can be restated in terms of the graph: when a cell is run, its descendants are run automatically. + ### Example The next four cells plot a sine wave with a given period and amplitude. @@ -118,6 +130,7 @@ mo.md( `ampltitude`, then click the run button ( ▷ ) to register your changes. See what happens to the sine wave. + Here is the dataflow graph for the cells that make the sine wave plot, plus the cells that import libraries. Each cell is labeled with its defs. @@ -140,12 +153,14 @@ the cells that import libraries. Each cell is labeled with its defs. The last cell, which doesn't define anything, produces the plot. + ## Dataflow programming marimo's runtime rule has some important consequences that may seem surprising if you are not used to dataflow programming. We list these below. + ### Execution order is not cell order The order in which cells are executed is determined entirely by the @@ -153,6 +168,7 @@ dataflow graph. This makes marimo notebooks more reproducible than traditional notebooks. It also lets you place boilerplate, like imports or long markdown strings, at the bottom of the editor. + ### Global variable names must be unique Every global variable can be defined by only one cell. Without this @@ -175,6 +191,7 @@ planet **🌊 Try it!** In the previous cell, change the name `planet` to `home`, then run the cell. + Because defs must be unique, global variables cannot be modified with operators like `+=` or `-=` in cells other than the one that created them; these operators count as redefinitions of a name. @@ -277,8 +294,8 @@ state.number = 0 mo.accordion( { "Why not track attributes?": """ - marimo can't reliably trace attributes - to cells that define them. For example, attributes are routinely + marimo can't reliably trace attributes + to cells that define them. For example, attributes are routinely created or modified by library code. """ } @@ -296,7 +313,7 @@ mo.accordion( { "Tip (advanced): mutable state": ( """ - You can use the fact that marimo does not track attributes or + You can use the fact that marimo does not track attributes or mutations to implement mutable state in marimo. An example of this is shown in the `ui` tutorial. """ @@ -339,9 +356,9 @@ tips = { "Use global variables sparingly": ( """ Keep the number of global variables in your program small to avoid - name collisions across cells. Keep the number of global variables + name collisions across cells. Keep the number of global variables defined by any one cell small to make sure that the units of - reactive execution are small. + reactive execution are small. """ ), "Use descriptive names": ( @@ -397,7 +414,7 @@ tips = { """ ), "Write idempotent cells": ( - """ + """ Write cells whose outputs and behavior are the same when given the same inputs (refs); such cells are called _idempotent_. This will help you avoid bugs, and let you cache expensive intermediate @@ -423,7 +440,7 @@ tips = { Whenever `compute_predictions` is called with a value of `problem_parameters` it has not seen, it will compute the predictions and store them in a cache. The next time it is called with the same - parameters, instead of recomputing the predictions, it will just + parameters, instead of recomputing the predictions, it will just fetch the previously computed ones from the cache. If you are familiar with `functools.cache`, `mo.cache` is @@ -436,4 +453,4 @@ tips = { ```{python .marimo} import marimo as mo -``` \ No newline at end of file +``` diff --git a/tutorials/example-pdf.qmd b/tutorials/example-pdf.qmd index b6ba3b6..83f0423 100644 --- a/tutorials/example-pdf.qmd +++ b/tutorials/example-pdf.qmd @@ -2,6 +2,8 @@ title: PDF example format: pdf engine: marimo +header: |- + # Copyright 2026 Marimo. All rights reserved pyproject: | requires-python = ">=3.11" dependencies = [ diff --git a/tutorials/external_dependencies.qmd b/tutorials/external_dependencies.qmd index 6e6b733..12ddc26 100644 --- a/tutorials/external_dependencies.qmd +++ b/tutorials/external_dependencies.qmd @@ -1,5 +1,7 @@ --- title: External dependencies +header: |- + # Copyright 2026 Marimo. All rights reserved pyproject: | requires-python = ">=3.11" dependencies = [ diff --git a/tutorials/fileformat.qmd b/tutorials/fileformat.qmd index a5b914a..2167ccb 100644 --- a/tutorials/fileformat.qmd +++ b/tutorials/fileformat.qmd @@ -1,8 +1,8 @@ --- title: Fileformat -marimo-version: 0.13.2 +marimo-version: 0.23.1 header: |- - # Copyright 2025 Marimo. All rights reserved + # Copyright 2026 Marimo. All rights reserved --- ```python {.marimo name="setup"} @@ -26,6 +26,7 @@ These files are: - 🐍 usable as Python scripts, with UI elements taking their default values - 🧩 modular, exposing functions and classes that can be imported from the notebook + ## Example Consider a marimo notebook with the following four cells. @@ -52,6 +53,7 @@ Fourth cell: import marimo as mo ``` + For the above example, marimo would generate the following file contents: @@ -92,6 +94,7 @@ generates. Moreover, the cell defining a single pure function `say_hello` was saved "top-level" in the notebook file, making it possible for you to import it into other Python files or notebooks. + ## Properties marimo's file format was designed to be easy to read and easy @@ -215,6 +218,7 @@ The details of marimo's file format are important if you want to import functions and classes defined in your notebook into other Python modules. If you don't intend to do so, you can skip this section. + ### Declaring imports used by functions and classes marimo can serialize functions and classes into the top-level of a file, so you can import them with regular Python syntax: @@ -238,8 +242,9 @@ with app.setup: Modules imported in a setup cell can be used in "top-level" functions or classes. You can add the setup cell from the general menu of the editor under: -::lucide:diamond-plus:: Add setup cell. +Add setup cell. + ### Functions and classes Notebook files expose functions and classes that depend only on variables defined in the setup cell (or on other such functions or classes). For example, the following cell: @@ -274,6 +279,7 @@ Making it importable as from my_notebook import roll_die ``` + Standalone classes are also exposed: ```python {.marimo name="*SimulationExample"} @@ -297,12 +303,12 @@ class SimulationExample: return [roll_die() for _ in range(self.n_rolls)] ``` -/// attention | Heads up -/// -Not all standalone functions will be exposed in the module. If your -function depends on variables that are defined in other cells, then it won't -be exposed top-level. +::: {.callout-warning title="Heads up"} + +Not all standalone functions will be exposed in the module. If your function depends on variables that are defined in other cells, then it won't be exposed top-level. + +::: For example, this function will not be exposed: @@ -337,6 +343,7 @@ classes](https://links.marimo.app/reusable-functions). See the docs on [testing](https://docs.marimo.io/guides/testing/). + ## This notebook's source code The source code of this notebook is shown below: @@ -348,4 +355,4 @@ with open(__file__, "r", encoding="utf-8") as f: ```{python .marimo} mo.ui.code_editor(contents) -``` \ No newline at end of file +``` diff --git a/tutorials/for_jupyter_users.qmd b/tutorials/for_jupyter_users.qmd index e4b73f7..0b8ff8b 100644 --- a/tutorials/for_jupyter_users.qmd +++ b/tutorials/for_jupyter_users.qmd @@ -1,8 +1,8 @@ --- title: marimo for Jupyter users -marimo-version: 0.13.2 +marimo-version: 0.23.1 header: |- - # Copyright 2024 Marimo. All rights reserved. + # Copyright 2026 Marimo. All rights reserved --- # marimo for Jupyter users @@ -10,6 +10,7 @@ header: |- This notebook explains important differences between Jupyter and marimo. If you're familiar with Jupyter and are trying out marimo for the first time, read on! + ## Reactive execution The biggest difference between marimo and Jupyter is *reactive execution*. @@ -33,6 +34,7 @@ cells that depend on it, not unlike a spreadsheet. In contrast, Jupyter requires you to manually run each cell. + ### Why? Reactive execution frees you from the tedious task of manually re-running cells. @@ -45,6 +47,7 @@ program memory. Affected cells are automatically invalidated. This makes marimo notebooks as reproducible as regular Python scripts. + ## Interactive elements built-in marimo comes with a [large library of UI elements](https://docs.marimo.io/guides/interactivity.html) that are automatically @@ -72,6 +75,7 @@ cells that refer to them. In contrast, Jupyter's lack of reactivity makes IPyWidgets difficult to use. + ## Shareable as apps marimo notebooks can be shared as read-only web apps: just serve it with @@ -84,6 +88,7 @@ Not every marimo notebook needs to be shared as an app, but marimo makes it seamless to do so if you want to. In this way, marimo works as a replacement for both Jupyter and Streamlit. + ## Cell order In marimo, cells can be arranged in any order — marimo figures out the one true way to execute them based on variable declarations and references (in a ["topologically sorted"](https://en.wikipedia.org/wiki/Topological_sorting#:~:text=In%20computer%20science%2C%20a%20topological,before%20v%20in%20the%20ordering.) order) @@ -100,6 +105,7 @@ This lets you arrange your cells in the way that makes the most sense to you. Fo In contrast, Jupyter notebooks implicitly assume a top-to-bottom execution order. + ## Re-assigning variables marimo disallows variable re-assignment. Here is something commonly done in Jupyter notebooks that cannot be done in marimo: @@ -124,6 +130,7 @@ If you run into this error, here are your options: 2. prefix variables with an underscore (`_df`) to make them local to the cell 3. wrap your code in functions, or give your variables more descriptive names + ## Markdown marimo only has Python cells, but you can still write Markdown: `import marimo as mo` and use `mo.md` to write Markdown. @@ -143,6 +150,7 @@ objects, like plots. _Tip: toggle a markdown view via `Cmd/Ctrl-Shift-M` in an empty cell._ + ## Notebook files Jupyter saves notebooks as JSON files, with outputs serialized in them. This is helpful as a record of your plots and other results, but makes notebooks difficult to version and reuse. @@ -157,6 +165,7 @@ marimo does _not_ save your outputs in the file; if you want them saved, make su marimo is designed so that small changes in your code yield small git diffs! + ## Parting thoughts marimo is a **reinvention** of the Python notebook as a reproducible, interactive, and shareable Python program, instead of an error-prone scratchpad. @@ -168,4 +177,4 @@ Check out [our docs](https://docs.marimo.io/) to learn more! _This guide was adapted from [Pluto for Jupyter users](https://featured.plutojl.org/basic/pluto%20for%20jupyter%20users). -We ❤️ Pluto.jl!_ \ No newline at end of file +We ❤️ Pluto.jl!_ diff --git a/tutorials/index.qmd b/tutorials/index.qmd index 37a52bd..66b9789 100644 --- a/tutorials/index.qmd +++ b/tutorials/index.qmd @@ -1,6 +1,8 @@ --- title: marimo tutorials format: html +header: |- + # Copyright 2026 Marimo. All rights reserved --- Open a tutorial. @@ -20,5 +22,6 @@ Recommended sequence: - [plots](plots.html) - [layout](layout.html) - [fileformat](fileformat.html) +- [sql](sql.html) - [external-dependencies](external_dependencies.html) -- [for-jupyter-users](marimo_for_jupyter_users.html) +- [for-jupyter-users](for_jupyter_users.html) diff --git a/tutorials/intro.qmd b/tutorials/intro.qmd index 1682e4b..32d40f6 100644 --- a/tutorials/intro.qmd +++ b/tutorials/intro.qmd @@ -1,8 +1,8 @@ --- title: Intro -marimo-version: 0.13.2 +marimo-version: 0.23.1 header: |- - # Copyright 2024 Marimo. All rights reserved. + # Copyright 2026 Marimo. All rights reserved --- ```{python .marimo} @@ -156,6 +156,7 @@ every cell that references that element is re-run. marimo provides a library of UI elements to choose from under `marimo.ui`. + **🌊 Some UI elements.** Try interacting with the below elements. ```{python .marimo} @@ -189,6 +190,7 @@ The Python files generated by marimo are: values, and - importable by other modules (more on that in the future). + ## 4. Running notebooks as apps marimo notebooks can double as apps. Click the app window icon in the @@ -198,6 +200,7 @@ Serve a notebook as an app with `marimo run` at the command-line. Of course, you can use marimo just to level-up your notebooking, without ever making apps. + ## 5. The `marimo` command-line tool **Creating and editing notebooks.** Use @@ -248,6 +251,7 @@ marimo tutorial dataflow In addition to tutorials, we have examples in our [our GitHub repo](https://www.github.com/marimo-team/marimo/tree/main/examples). + ## 6. The marimo editor Here are some tips to help you get started with the marimo editor. @@ -258,6 +262,7 @@ mo.accordion(tips) ## Finally, a fun fact + The name "marimo" is a reference to a type of algae that, under the right conditions, clumps together to form a small sphere called a "marimo moss ball". Made of just strands of algae, these @@ -303,7 +308,7 @@ tips = { the plus button to the left of the cell, which appears on mouse hover. - 2. _Move_ a cell up or down by dragging on the handle to the + 2. _Move_ a cell up or down by dragging on the handle to the right of the cell, which appears on mouse hover. 3. _Delete_ a cell by clicking the trash bin icon. Bring it @@ -364,10 +369,10 @@ tips = { """ You can leave Marimo & shut down the server by clicking the circled X at the top right of the screen and responding - to the prompt. + to the prompt. - :floppy_disk: _Be sure to save your work first!_ + :floppy_disk: _Be sure to save your work first!_ """ ), } -``` \ No newline at end of file +``` diff --git a/tutorials/layout.qmd b/tutorials/layout.qmd index 72ee067..3b82ae5 100644 --- a/tutorials/layout.qmd +++ b/tutorials/layout.qmd @@ -1,8 +1,8 @@ --- title: Layout -marimo-version: 0.13.2 +marimo-version: 0.23.1 header: |- - # Copyright 2024 Marimo. All rights reserved. + # Copyright 2026 Marimo. All rights reserved --- # Layout @@ -10,6 +10,7 @@ header: |- `marimo` provides functions to help you lay out your output, such as in rows and columns, accordions, tabs, and callouts. + ## Rows and columns Arrange objects into rows and columns with `mo.hstack` and `mo.vstack`. @@ -116,6 +117,7 @@ using `mo.hstack`, `Html` objects (returned by most marimo functions, and subclassed by most marimo classes) have a shortcut using via their `center`, `right`, and `left` methods. + This markdown is left-justified. ```{python .marimo} @@ -140,6 +142,7 @@ mo.accordion( Create expandable shelves of content using `mo.accordion`: + An accordion can contain multiple items: ```{python .marimo} @@ -241,4 +244,4 @@ mo.accordion({"Documentation: `mo.callout`": mo.doc(mo.callout)}) ```{python .marimo} import marimo as mo -``` \ No newline at end of file +``` diff --git a/tutorials/markdown.qmd b/tutorials/markdown.qmd index f6a0d63..5e2d584 100644 --- a/tutorials/markdown.qmd +++ b/tutorials/markdown.qmd @@ -1,6 +1,16 @@ --- title: Markdown -marimo-version: 0.13.2 +marimo-version: 0.23.1 +header: |- + # Copyright 2026 Marimo. All rights reserved +pyproject: | + requires-python = ">=3.11" + dependencies = [ + "marimo>=0.23.1", + "matplotlib", + "numpy", + "polars", + ] --- # Hello, Markdown! @@ -22,6 +32,7 @@ mo.md( ) ``` + **Tip: toggling between the Markdown and Python editor** Although markdown is written with `mo.md`, marimo provides a markdown editor @@ -36,6 +47,7 @@ also **hide** the markdown editor through the cell actions menu. need to use `mo.md(f"...")` directly; the markdown view does not support f-strings. + ## LaTeX You can embed LaTeX in Markdown. @@ -257,4 +269,4 @@ import numpy as np import math import marimo as mo -``` \ No newline at end of file +``` diff --git a/tutorials/markdown_format.qmd b/tutorials/markdown_format.qmd index b4ec86f..c75b537 100644 --- a/tutorials/markdown_format.qmd +++ b/tutorials/markdown_format.qmd @@ -1,6 +1,14 @@ --- title: Markdown -marimo-version: 0.13.2 +marimo-version: 0.23.1 +header: |- + # Copyright 2026 Marimo. All rights reserved +pyproject: | + requires-python = ">=3.11" + dependencies = [ + "marimo[sql]>=0.23.1", + "matplotlib", + ] --- # Markdown file format @@ -27,6 +35,7 @@ To run it as an app, use $ marimo run notebook.md ``` + ## Exporting from Python You can export marimo notebooks that are stored as Python to the markdown format @@ -36,6 +45,7 @@ by running the following command: $ marimo export md notebook.py > notebook.md ``` + ## Creating Python cells When you do need to create a Python cell in the markdown format, you can use a @@ -64,8 +74,10 @@ As long as your code block contains the word `marimo` in a brace, like You can break up markdown into multiple cells by using an empty html tag ``: + View the source of this notebook to see how this cell was created. + You can still hide cell code in markdown notebooks: ````md @@ -170,6 +182,7 @@ It's not likely that you'll run into this issue, but rest assured that marimo is working behind the scenes to keep your notebooks unambiguous and clean as possible. + ### Saving multicolumn mode Multicolumn mode works, but the first cell in a column must be a python cell in @@ -181,6 +194,7 @@ print("First cell in column 1") ``` ```` + ### Naming cells Since the markdown notebook really is just markdown, you can't import from a @@ -255,6 +269,7 @@ format. You can always convert back to a Python notebook if you need to: $ marimo convert my_marimo.md > my_marimo.py ``` + ## More on markdown Be sure to checkout the markdown.py tutorial (`marimo tutorial markdown`) for @@ -262,4 +277,4 @@ more information on to type-set and render markdown in marimo. ```python {.marimo hide_code="true"} import marimo as mo -``` \ No newline at end of file +``` diff --git a/tutorials/plots.qmd b/tutorials/plots.qmd index dd4e7bb..e29a7ce 100644 --- a/tutorials/plots.qmd +++ b/tutorials/plots.qmd @@ -1,7 +1,15 @@ --- title: Plots -marimo-version: 0.13.2 -eval: false +marimo-version: 0.23.1 +header: |- + # Copyright 2026 Marimo. All rights reserved +pyproject: | + requires-python = ">=3.11" + dependencies = [ + "marimo>=0.23.1", + "matplotlib", + "numpy", + ] --- # Plotting @@ -16,8 +24,10 @@ plotly, seaborn, and altair. This tutorial gives examples using matplotlib; other libraries are used similarly. + ## Matplotlib + To show a plot, include it in the last expression of a cell (just like any other output). @@ -122,6 +132,7 @@ mo.md( ## Other libraries + marimo also supports these other plotting libraries: - Plotly @@ -139,7 +150,7 @@ module_not_found_explainer = mo.md( """ ## Oops! - It looks like you're missing a package that this tutorial + It looks like you're missing a package that this tutorial requires. Use the package manager panel on the left to install **numpy** and **matplotlib**, diff --git a/tutorials/ui.qmd b/tutorials/ui.qmd index a0ed497..94d36bb 100644 --- a/tutorials/ui.qmd +++ b/tutorials/ui.qmd @@ -1,8 +1,8 @@ --- -title: Ui -marimo-version: 0.13.2 +title: UI +marimo-version: 0.23.1 header: |- - # Copyright 2024 Marimo. All rights reserved. + # Copyright 2026 Marimo. All rights reserved --- # UI Elements @@ -11,6 +11,7 @@ One of marimo's most powerful features is its first-class support for interactive user interface (UI) elements: interacting with a UI element will automatically run cells that reference it. + ## marimo.ui ```{python .marimo} @@ -45,14 +46,14 @@ mo.accordion( { "Tip: assign UI elements to global variables": ( """ - Interacting with a displayed UI element will only + Interacting with a displayed UI element will only trigger reactive execution if the UI element is assigned to a global variable. """ ), "Tip: accessing an element's value": ( """ - Every UI element has a value attribute that you can access in + Every UI element has a value attribute that you can access in Python. """ ), @@ -69,6 +70,7 @@ mo.accordion( ### Simple elements + marimo has a [large library of simple UI elements](https://docs.marimo.io/api/inputs/index.html). Here are a just few examples: ```python {.marimo hide_code="true"} @@ -169,6 +171,7 @@ documentation(basic_ui_elements.value) create a dynamic set of UI elements, or reduce the number of global variables in your program. + This first example shows how to create an array of UI elements using `mo.ui.array`. When you interact with an element in the array, all cells that reference the array are reactively run. If you instead used a regular Python list, cells referring to the list would _not_ be run. @@ -227,6 +230,7 @@ your own interactive UI elements, or use widgets built by others in the community. To learn more, [see our docs](https://docs.marimo.io/guides/integrating_with_marimo/custom_ui_plugins.html). + ## Appendix The remaining cells are helper data structures and functions. You can look at their code if you're curious how certain parts of this @@ -390,4 +394,4 @@ def documentation(element): ```{python .marimo} import marimo as mo -``` \ No newline at end of file +``` From 4bf7cdbda239cccf3a6f52f26b5a143e432c940a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Ferenc=20Gyarmati?= Date: Wed, 6 May 2026 14:29:17 +0200 Subject: [PATCH 4/6] chore: address comments --- _extensions/marimo/extract.py | 27 ++++++++++++++++-- tests/python/test_extract.py | 54 +++++++++++++++++++++++++++++++++-- tutorials/sql.qmd | 2 ++ 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/_extensions/marimo/extract.py b/_extensions/marimo/extract.py index 89bee4f..21d66bc 100755 --- a/_extensions/marimo/extract.py +++ b/_extensions/marimo/extract.py @@ -14,6 +14,7 @@ import asyncio import json +import keyword import os import re import sys @@ -60,18 +61,37 @@ "editor": False, } -_PYTHON_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") +SQL_LANGUAGES = {"sql", "sql.marimo"} +SQL_DOT_FENCE_REGEX = re.compile( + r"^(\s*`{3,})\s*\{\s*sql\.marimo(?P[^}]*)\}\s*$", + re.MULTILINE, +) + + +def is_valid_python_identifier(name: str) -> bool: + return name.isidentifier() and not keyword.iskeyword(name) def sql_code_to_python(code: str, query: Optional[str]) -> str: """Convert a marimo markdown SQL cell into executable Python.""" escaped = code.strip("\n").replace('"""', r"\"\"\"") sql_call = f'mo.sql(fr"""\n{escaped}\n""")' - if query and _PYTHON_IDENTIFIER.match(query): + if query and is_valid_python_identifier(query): return f"{query} = {sql_call}" return sql_call +def normalize_sql_marimo_fences(text: str) -> str: + """Rewrite dot-joined SQL fences into the parser's supported SQL form.""" + + def replace(match: re.Match[str]) -> str: + attrs = match.group("attrs").strip() + suffix = f" {attrs}" if attrs else "" + return f"{match.group(1)}sql {{.marimo{suffix}}}" + + return SQL_DOT_FENCE_REGEX.sub(replace, text) + + def extract_and_strip_quarto_config(block: str) -> tuple[dict[str, Any], str]: pattern = r"^\s*\#\|\s*(.*?)\s*:\s*(.*?)(?=\n|\Z)" config: dict[str, Any] = {} @@ -304,7 +324,7 @@ def tree_to_pandoc_export(root: Element) -> SafeWrap: # type: ignore[valid-type code = str(child.text) config, code = extract_and_strip_quarto_config(code) - if child.attrib.get("language") == "sql": + if child.attrib.get("language") in SQL_LANGUAGES: code = sql_code_to_python(code, child.attrib.get("query")) try: @@ -382,6 +402,7 @@ def convert_from_md_to_pandoc_export(text: str, mime_sensitive: bool) -> dict[st """ if not text: return {"header": "", "outputs": []} + text = normalize_sql_marimo_fences(text) if mime_sensitive: parser = MarimoPandocParser(output_format="marimo-pandoc-export-with-mime") # type: ignore[arg-type] else: diff --git a/tests/python/test_extract.py b/tests/python/test_extract.py index cef70e2..80a6755 100644 --- a/tests/python/test_extract.py +++ b/tests/python/test_extract.py @@ -111,8 +111,26 @@ def test_invalid_query_falls_back_to_expression(self): assert result == 'mo.sql(fr"""\nSELECT * FROM df;\n""")' + def test_keyword_query_falls_back_to_expression(self): + result = sql_code_to_python("SELECT * FROM df;", "class") + + assert result == 'mo.sql(fr"""\nSELECT * FROM df;\n""")' + class TestConvertFromMdToPandocExport: + def _extract_notebook_code(self, header: str) -> str: + notebook_match = re.search(r"", header) + assert notebook_match is not None + return unquote(notebook_match.group(1)) + + def _convert_without_eval(self, markdown: str) -> dict: + original_eval = default_config["eval"] + default_config["eval"] = False + try: + return convert_from_md_to_pandoc_export(markdown, mime_sensitive=False) + finally: + default_config["eval"] = original_eval + def test_injects_pyproject_into_exported_notebook(self): markdown = """--- title: External dependencies @@ -136,9 +154,7 @@ def test_injects_pyproject_into_exported_notebook(self): assert "__MARIMO_EXPORT_CONTEXT__" in header assert "