mirror of
https://github.com/MLSysBook/TinyTorch.git
synced 2026-05-06 17:57:31 -05:00
- Rename SyncCommand to ExportCommand and sync.py to export.py - Update all CLI references from 'tito package sync' to 'tito package export' - Update help text and internal messages to use 'Export' terminology - Update imports across all command files - Update help text in main CLI, reset, clean, info, and notebooks commands - Command now clearly communicates that it exports notebook code to Python package - Maintains same functionality but with clearer naming for user experience
158 lines
6.0 KiB
Python
158 lines
6.0 KiB
Python
"""
|
|
Notebooks command for building Jupyter notebooks from Python files using Jupytext.
|
|
"""
|
|
|
|
import subprocess
|
|
import sys
|
|
from argparse import ArgumentParser, Namespace
|
|
from pathlib import Path
|
|
from typing import List, Tuple
|
|
|
|
from rich.panel import Panel
|
|
from rich.text import Text
|
|
|
|
from .base import BaseCommand
|
|
from ..core.exceptions import ExecutionError, ModuleNotFoundError
|
|
|
|
class NotebooksCommand(BaseCommand):
|
|
"""Command to build Jupyter notebooks from Python files using Jupytext."""
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "notebooks"
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
return "Build notebooks from Python files"
|
|
|
|
def add_arguments(self, parser: ArgumentParser) -> None:
|
|
"""Add notebooks command arguments."""
|
|
parser.add_argument(
|
|
'--module',
|
|
help='Build notebook for specific module'
|
|
)
|
|
parser.add_argument(
|
|
'--force',
|
|
action='store_true',
|
|
help='Force rebuild even if notebook exists'
|
|
)
|
|
parser.add_argument(
|
|
'--dry-run',
|
|
action='store_true',
|
|
help='Show what would be built without actually building'
|
|
)
|
|
|
|
def validate_args(self, args: Namespace) -> None:
|
|
"""Validate notebooks command arguments."""
|
|
if args.module:
|
|
module_file = self.config.modules_dir / args.module / f"{args.module}_dev.py"
|
|
if not module_file.exists():
|
|
raise ModuleNotFoundError(
|
|
f"Module '{args.module}' not found or no {args.module}_dev.py file"
|
|
)
|
|
|
|
def _find_dev_files(self) -> List[Path]:
|
|
"""Find all *_dev.py files in modules directory."""
|
|
dev_files = []
|
|
for module_dir in self.config.modules_dir.iterdir():
|
|
if module_dir.is_dir():
|
|
dev_py = module_dir / f"{module_dir.name}_dev.py"
|
|
if dev_py.exists():
|
|
dev_files.append(dev_py)
|
|
return dev_files
|
|
|
|
def _convert_file(self, dev_file: Path) -> Tuple[bool, str]:
|
|
"""Convert a single Python file to notebook using Jupytext."""
|
|
try:
|
|
# Use Jupytext to convert Python file to notebook
|
|
result = subprocess.run([
|
|
"jupytext", "--to", "notebook", str(dev_file)
|
|
], capture_output=True, text=True, timeout=30, cwd=dev_file.parent)
|
|
|
|
if result.returncode == 0:
|
|
notebook_file = dev_file.with_suffix('.ipynb')
|
|
return True, f"{dev_file.name} → {notebook_file.name}"
|
|
else:
|
|
error_msg = result.stderr.strip() if result.stderr.strip() else "Conversion failed"
|
|
return False, error_msg
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return False, "Conversion timed out"
|
|
except FileNotFoundError:
|
|
return False, "Jupytext not found. Install with: pip install jupytext"
|
|
except Exception as e:
|
|
return False, f"Error: {str(e)}"
|
|
|
|
def run(self, args: Namespace) -> int:
|
|
"""Execute the notebooks command."""
|
|
self.console.print(Panel(
|
|
"📓 Building Notebooks from Python Files (using Jupytext)",
|
|
title="Notebook Generation",
|
|
border_style="bright_cyan"
|
|
))
|
|
|
|
# Find files to convert
|
|
if args.module:
|
|
dev_files = [self.config.modules_dir / args.module / f"{args.module}_dev.py"]
|
|
self.console.print(f"🔄 Building notebook for module: {args.module}")
|
|
else:
|
|
dev_files = self._find_dev_files()
|
|
if not dev_files:
|
|
self.console.print(Panel(
|
|
"[yellow]⚠️ No *_dev.py files found in modules/[/yellow]",
|
|
title="Nothing to Convert",
|
|
border_style="yellow"
|
|
))
|
|
return 0
|
|
self.console.print(f"🔄 Building notebooks for {len(dev_files)} modules...")
|
|
|
|
# Dry run mode
|
|
if args.dry_run:
|
|
self.console.print("\n[cyan]Dry run mode - would convert:[/cyan]")
|
|
for dev_file in dev_files:
|
|
module_name = dev_file.parent.name
|
|
self.console.print(f" • {module_name}: {dev_file.name}")
|
|
return 0
|
|
|
|
# Convert files
|
|
success_count = 0
|
|
error_count = 0
|
|
|
|
for dev_file in dev_files:
|
|
success, message = self._convert_file(dev_file)
|
|
module_name = dev_file.parent.name
|
|
|
|
if success:
|
|
success_count += 1
|
|
self.console.print(f" ✅ {module_name}: {message}")
|
|
else:
|
|
error_count += 1
|
|
self.console.print(f" ❌ {module_name}: {message}")
|
|
|
|
# Summary
|
|
self._print_summary(success_count, error_count)
|
|
|
|
return 0 if error_count == 0 else 1
|
|
|
|
def _print_summary(self, success_count: int, error_count: int) -> None:
|
|
"""Print command execution summary."""
|
|
summary_text = Text()
|
|
|
|
if success_count > 0:
|
|
summary_text.append(f"✅ Successfully built {success_count} notebook(s)\n", style="bold green")
|
|
if error_count > 0:
|
|
summary_text.append(f"❌ Failed to build {error_count} notebook(s)\n", style="bold red")
|
|
|
|
if success_count > 0:
|
|
summary_text.append("\n💡 Next steps:\n", style="bold yellow")
|
|
summary_text.append(" • Open notebooks with: jupyter lab\n", style="white")
|
|
summary_text.append(" • Work interactively in the notebooks\n", style="white")
|
|
summary_text.append(" • Export code with: tito package export\n", style="white")
|
|
summary_text.append(" • Run tests with: tito module test\n", style="white")
|
|
|
|
border_style = "green" if error_count == 0 else "yellow"
|
|
self.console.print(Panel(
|
|
summary_text,
|
|
title="Notebook Generation Complete",
|
|
border_style=border_style
|
|
)) |