diff --git a/pyproject.toml b/pyproject.toml index 0156ebf..fdbadd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } diff --git a/src/mobuild/__init__.py b/src/mobuild/__init__.py index 75c2e01..dc24382 100644 --- a/src/mobuild/__init__.py +++ b/src/mobuild/__init__.py @@ -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`` @@ -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/``. + writes the result to ``out_path``. 5. Prepends an ``__all__`` so the generated module only exposes the public names that were explicitly exported. @@ -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) @@ -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) diff --git a/src/mobuild/static/cookiecutter/{{cookiecutter.project_name}}/Makefile b/src/mobuild/static/cookiecutter/{{cookiecutter.project_name}}/Makefile index 05f3e7f..cb3b69b 100644 --- a/src/mobuild/static/cookiecutter/{{cookiecutter.project_name}}/Makefile +++ b/src/mobuild/static/cookiecutter/{{cookiecutter.project_name}}/Makefile @@ -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 diff --git a/tests/test_basics.py b/tests/test_basics.py index 85e195d..c5663a2 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -1,11 +1,12 @@ +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 @@ -13,12 +14,20 @@ def test_basics(tmp_path): 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 @@ -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) diff --git a/uv.lock b/uv.lock index cc310b2..ee7dbe2 100644 --- a/uv.lock +++ b/uv.lock @@ -357,7 +357,7 @@ wheels = [ [[package]] name = "mobuild" -version = "0.1.1" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "cookiecutter" },