Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "mobuild"
version = "0.2.0"
description = "Add your description here"
version = "0.3.0"
description = "A minimal build tool to turn marimo notebooks into Python packages."
readme = "README.md"
authors = [
{ name = "koaning", email = "vincentwarmerdam@gmail.com" }
Expand Down
23 changes: 13 additions & 10 deletions src/mobuild/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def _collect_top_level_names(code: str) -> list[str]:
return names


def _write_file(file: Path, out_folder: Path):
def _write_file(file: Path, out_path: Path):
"""Extract exported code from a single marimo notebook and write it as a plain Python file.

Marimo notebooks are valid Python files. Each notebook defines a global ``app``
Expand All @@ -45,7 +45,7 @@ def _write_file(file: Path, out_folder: Path):
execution order.
3. Selects only cells whose source code contains the ``## EXPORT`` marker.
4. Concatenates those cells in execution order (respecting the DAG) and
writes the result to ``out_folder/<notebook_filename>``.
writes the result to ``out_path``.
5. Prepends an ``__all__`` so the generated module only exposes the
public names that were explicitly exported.

Expand Down Expand Up @@ -93,19 +93,22 @@ def _write_file(file: Path, out_folder: Path):
if names:
code_export = "__all__ = " + repr(names) + "\n\n" + code_export

(out_folder / file.name).write_text(code_export)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(code_export)


@app.command(no_args_is_help=True)
def export(input_file: Path, output_folder: Path):
def export(input_file: Path, output_path: Path):
"""Build a Python file from a single Marimo notebook."""
input_file = Path(input_file)
output_folder = Path(output_folder)
output_path = Path(output_path)
if not input_file.exists():
typer.echo(f"Input file {input_file} does not exist")
raise typer.Exit(1)
output_folder.mkdir(parents=True, exist_ok=True)
_write_file(input_file, output_folder)
if output_path.is_dir():
typer.echo(f"Output path {output_path} is a directory, please provide a file path")
raise typer.Exit(1)
_write_file(input_file, output_path)


@app.command(no_args_is_help=True)
Expand All @@ -124,6 +127,6 @@ def init(name: str, output_folder: Path = Path(".")):
)


def runtime_sync(output_folder: Path):
"""Write the current marimo notebook to the output folder as a normal Python file."""
_write_file(__file__, output_folder)
def runtime_sync(output_path: Path):
"""Write the current marimo notebook to the given output path as a normal Python file."""
_write_file(Path(__file__), output_path)
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ clean:
rm -rf .pytest_cache/

build:
uv run mobuild export nbs/__init__.py src
uv run mobuild export nbs/__init__.py src/__init__.py
33 changes: 25 additions & 8 deletions tests/test_basics.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
import click
import pytest
from mobuild import export, init


def test_basics(tmp_path):
out_dir = tmp_path / "proj1_out"
out_dir.mkdir()
export(input_file="tests/proj1/__init__.py", output_folder=out_dir)
out = (out_dir / "__init__.py").read_text()
out_file = tmp_path / "proj1_out" / "__init__.py"
export(input_file="tests/proj1/__init__.py", output_path=out_file)
out = out_file.read_text()
assert "a = 1" in out
assert "b = 2" not in out
assert "## export" not in out.lower() # Marker should be stripped
assert '__all__' in out
assert "'a'" in out


def test_custom_output_name(tmp_path):
"""The user can pick any file name for the output."""
out_file = tmp_path / "my_module.py"
export(input_file="tests/proj1/__init__.py", output_path=out_file)
out = out_file.read_text()
assert "a = 1" in out
assert '__all__' in out


def test_selective_export(tmp_path):
"""Only cells marked with ## EXPORT should appear in the output."""
out_dir = tmp_path / "proj2_out"
out_dir.mkdir()
export(input_file="tests/proj2/__init__.py", output_folder=out_dir)
out = (out_dir / "__init__.py").read_text()
out_file = tmp_path / "proj2_out" / "__init__.py"
export(input_file="tests/proj2/__init__.py", output_path=out_file)
out = out_file.read_text()

# Exported definitions should be present
assert "def greet" in out
Expand All @@ -42,6 +51,14 @@ def test_selective_export(tmp_path):



def test_folder_is_not_sufficient(tmp_path):
"""Passing a directory instead of a file path should fail."""
out_dir = tmp_path / "some_folder"
out_dir.mkdir()
with pytest.raises(click.exceptions.Exit):
export(input_file="tests/proj1/__init__.py", output_path=out_dir)


def test_init(tmp_path):
out_dir = tmp_path / "output"
init(name="output", output_folder=tmp_path)
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading