Coverage for src / marimushka / cli.py: 100%
61 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-28 17:41 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-28 17:41 +0000
1"""Command-line interface for marimushka.
3This module provides the CLI commands for exporting marimo notebooks,
4watching for changes, and displaying version information.
5"""
7import sys
8from pathlib import Path
10import typer
11from loguru import logger
12from rich import print as rich_print
14from . import __version__
16# Global state for debug mode
17_debug_mode = False
19# Configure logger
20logger.configure(extra={"subprocess": ""})
21logger.remove()
24def configure_logging(debug: bool = False) -> None:
25 """Configure logging based on debug mode.
27 Args:
28 debug: Whether to enable debug mode.
30 """
31 global _debug_mode
32 _debug_mode = debug
34 # Remove existing handlers
35 logger.remove()
37 if debug:
38 # Debug mode: show all logs including DEBUG level
39 logger.add(
40 sys.stderr,
41 format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | "
42 "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
43 "<magenta>{extra[subprocess]}</magenta><level>{message}</level>",
44 level="DEBUG",
45 )
46 logger.debug("Debug mode enabled - showing all log messages")
47 else:
48 # Normal mode: show INFO and above
49 logger.add(
50 sys.stderr,
51 format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | "
52 "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
53 "<magenta>{extra[subprocess]}</magenta><level>{message}</level>",
54 level="INFO",
55 )
58# Initial configuration with default settings
59configure_logging(debug=False)
61# Maximum number of changed files to display in watch mode
62_MAX_CHANGED_FILES_TO_DISPLAY = 5
64app = typer.Typer(help=f"Marimushka - Export marimo notebooks in style. Version: {__version__}")
67@app.callback(invoke_without_command=True)
68def callback(ctx: typer.Context) -> None:
69 """Handle the CLI invocation without a subcommand.
71 This callback runs before any subcommand and displays help text if the
72 user invokes marimushka without specifying a subcommand (e.g., just
73 `marimushka` instead of `marimushka export`).
75 Args:
76 ctx: Typer context containing invocation information, including
77 which subcommand (if any) was specified.
79 Raises:
80 typer.Exit: Always raised when no subcommand is provided, with
81 exit code 0 to indicate this is expected behavior.
83 """
84 if ctx.invoked_subcommand is None:
85 print(ctx.get_help())
86 raise typer.Exit()
89@app.command(name="export")
90def export_command(
91 output: str = typer.Option("_site", "--output", "-o", help="Directory where the exported files will be saved"),
92 template: str = typer.Option(
93 str(Path(__file__).parent / "templates" / "tailwind.html.j2"),
94 "--template",
95 "-t",
96 help="Path to the template file",
97 ),
98 notebooks: str = typer.Option("notebooks", "--notebooks", "-n", help="Directory containing marimo notebooks"),
99 apps: str = typer.Option("apps", "--apps", "-a", help="Directory containing marimo apps"),
100 notebooks_wasm: str = typer.Option(
101 "notebooks_wasm", "--notebooks-wasm", "-nw", help="Directory containing marimo notebooks"
102 ),
103 sandbox: bool = typer.Option(True, "--sandbox/--no-sandbox", help="Whether to run the notebook in a sandbox"),
104 bin_path: str | None = typer.Option(None, "--bin-path", "-b", help="The directory where the executable is located"),
105 parallel: bool = typer.Option(True, "--parallel/--no-parallel", help="Whether to export notebooks in parallel"),
106 max_workers: int = typer.Option(4, "--max-workers", "-w", help="Maximum number of parallel workers (1-16)"),
107 timeout: int = typer.Option(300, "--timeout", help="Timeout in seconds for each notebook export"),
108 debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug mode with verbose logging"),
109) -> None:
110 """Export marimo notebooks and build an HTML index page linking to them.
112 This is the main CLI command for exporting notebooks. It scans the specified
113 directories for marimo notebook files (.py), exports each one to HTML or
114 WebAssembly format based on its category, and generates an index.html page
115 that provides navigation links to all exported notebooks.
117 The export process uses marimo's built-in export functionality via uvx,
118 running each notebook in sandbox mode by default for security.
120 Example usage:
121 # Basic export with defaults
122 $ marimushka export
124 # Custom directories
125 $ marimushka export -n my_notebooks -a my_apps -o dist
127 # Disable parallel processing
128 $ marimushka export --no-parallel
130 # Use custom template
131 $ marimushka export -t my_template.html.j2
133 # Enable debug mode for troubleshooting
134 $ marimushka export --debug
136 """
137 # Configure logging based on debug flag
138 configure_logging(debug=debug)
140 from .export import main
142 main(
143 output=output,
144 template=template,
145 notebooks=notebooks,
146 apps=apps,
147 notebooks_wasm=notebooks_wasm,
148 sandbox=sandbox,
149 bin_path=bin_path,
150 parallel=parallel,
151 max_workers=max_workers,
152 timeout=timeout,
153 )
156@app.command(name="watch")
157def watch_command(
158 output: str = typer.Option("_site", "--output", "-o", help="Directory where the exported files will be saved"),
159 template: str = typer.Option(
160 str(Path(__file__).parent / "templates" / "tailwind.html.j2"),
161 "--template",
162 "-t",
163 help="Path to the template file",
164 ),
165 notebooks: str = typer.Option("notebooks", "--notebooks", "-n", help="Directory containing marimo notebooks"),
166 apps: str = typer.Option("apps", "--apps", "-a", help="Directory containing marimo apps"),
167 notebooks_wasm: str = typer.Option(
168 "notebooks_wasm", "--notebooks-wasm", "-nw", help="Directory containing marimo notebooks"
169 ),
170 sandbox: bool = typer.Option(True, "--sandbox/--no-sandbox", help="Whether to run the notebook in a sandbox"),
171 bin_path: str | None = typer.Option(None, "--bin-path", "-b", help="The directory where the executable is located"),
172 parallel: bool = typer.Option(True, "--parallel/--no-parallel", help="Whether to export notebooks in parallel"),
173 max_workers: int = typer.Option(4, "--max-workers", "-w", help="Maximum number of parallel workers (1-16)"),
174 timeout: int = typer.Option(300, "--timeout", help="Timeout in seconds for each notebook export"),
175 debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug mode with verbose logging"),
176) -> None:
177 """Watch for changes and automatically re-export notebooks.
179 This command watches the notebook directories and template file for changes,
180 automatically re-exporting when files are modified.
182 Requires the 'watchfiles' package: uv add watchfiles
184 Example usage:
185 # Basic watch mode
186 $ marimushka watch
188 # Watch with custom directories
189 $ marimushka watch -n my_notebooks -a my_apps
191 # Watch with debug logging
192 $ marimushka watch --debug
193 """
194 # Configure logging based on debug flag
195 configure_logging(debug=debug)
197 try:
198 from watchfiles import watch as watchfiles_watch
199 except ImportError: # pragma: no cover
200 rich_print("[bold red]Error:[/bold red] watchfiles package is required for watch mode.")
201 rich_print("Install it with: [cyan]uv add watchfiles[/cyan]")
202 raise typer.Exit(1) from None
204 from .export import main
206 # Build list of paths to watch
207 watch_paths: list[Path] = []
209 template_path = Path(template)
210 if template_path.exists():
211 watch_paths.append(template_path.parent)
213 for folder in [notebooks, apps, notebooks_wasm]:
214 folder_path = Path(folder)
215 if folder_path.exists() and folder_path.is_dir():
216 watch_paths.append(folder_path)
218 if not watch_paths:
219 rich_print("[bold yellow]Warning:[/bold yellow] No directories to watch!")
220 raise typer.Exit(1)
222 rich_print("[bold green]Watching for changes in:[/bold green]")
223 for p in watch_paths:
224 rich_print(f" [cyan]{p}[/cyan]")
225 rich_print("\n[dim]Press Ctrl+C to stop[/dim]\n")
227 # Initial export
228 rich_print("[bold blue]Running initial export...[/bold blue]")
229 main(
230 output=output,
231 template=template,
232 notebooks=notebooks,
233 apps=apps,
234 notebooks_wasm=notebooks_wasm,
235 sandbox=sandbox,
236 bin_path=bin_path,
237 parallel=parallel,
238 max_workers=max_workers,
239 timeout=timeout,
240 )
241 rich_print("[bold green]Initial export complete![/bold green]\n")
243 # Watch for changes (interactive loop - excluded from coverage)
244 try:
245 for changes in watchfiles_watch(*watch_paths): # pragma: no cover
246 changed_files = [str(change[1]) for change in changes]
247 rich_print("\n[bold yellow]Detected changes:[/bold yellow]")
248 for f in changed_files[:_MAX_CHANGED_FILES_TO_DISPLAY]:
249 rich_print(f" [dim]{f}[/dim]")
250 if len(changed_files) > _MAX_CHANGED_FILES_TO_DISPLAY:
251 rich_print(f" [dim]... and {len(changed_files) - _MAX_CHANGED_FILES_TO_DISPLAY} more[/dim]")
253 rich_print("[bold blue]Re-exporting...[/bold blue]")
254 main(
255 output=output,
256 template=template,
257 notebooks=notebooks,
258 apps=apps,
259 notebooks_wasm=notebooks_wasm,
260 sandbox=sandbox,
261 bin_path=bin_path,
262 parallel=parallel,
263 max_workers=max_workers,
264 timeout=timeout,
265 )
266 rich_print("[bold green]Export complete![/bold green]")
267 except KeyboardInterrupt:
268 rich_print("\n[bold green]Watch mode stopped.[/bold green]")
271@app.command(name="version")
272def version_command() -> None:
273 """Display the current version of Marimushka.
275 Prints the package version with colored formatting using Rich. The version
276 is read from the package metadata at runtime, ensuring it always reflects
277 the installed version.
279 Example:
280 $ marimushka version
281 Marimushka version: 0.1.0
283 """
284 rich_print(f"[bold green]Marimushka[/bold green] version: [bold blue]{__version__}[/bold blue]")
287def cli() -> None:
288 """Entry point for the marimushka command-line interface.
290 This function is registered as the console script entry point in
291 pyproject.toml. It invokes the Typer application which handles
292 argument parsing and command dispatch.
294 The CLI supports the following subcommands:
295 - export: Export notebooks and generate index page
296 - watch: Monitor for changes and auto-export
297 - version: Display the installed version
299 Running without a subcommand displays help text.
301 Example:
302 $ marimushka # Shows help
303 $ marimushka export # Run export command
304 $ marimushka --help # Shows help
306 """
307 app()