Files
TinyTorch/tito/commands/status.py
Vijay Janapa Reddi a13b4f7244 Improve SIGCSE paper with reviewer feedback and clean up repository
Paper improvements:
- Add differentiated time estimates (60-80h experienced, 100-120h typical, 140-180h struggling)
- Moderate cognitive load claims with hedging language and empirical validation notes
- Add ML Systems Research subsection with citations (Baydin AD survey, Chen gradient checkpointing, TVM, FlashAttention)
- Add comprehensive Threats to Validity section (selection bias, single institution, demand characteristics, no control group, maturation, assessment validity)
- Define jargon (monkey-patching) at first use with clear explanation

Documentation updates:
- Restructure TITO CLI docs into dedicated section (overview, modules, milestones, data, troubleshooting)
- Update student workflow guide and quickstart guide
- Remove deprecated files (testing-framework.md, tito-essentials.md)
- Update module template and testing architecture docs

Repository cleanup:
- Remove temporary review files (ADDITIONAL_REVIEWS.md, EDTECH_OPENSOURCE_REVIEWS.md, TA_STRUGGLING_STUDENT_REVIEWS.md, etc.)
- Remove temporary development planning docs
- Update demo GIFs and configurations
2025-11-16 23:46:38 -05:00

572 lines
24 KiB
Python

"""
Status command for TinyTorch CLI: checks status of all modules in modules/ directory.
Supports both basic status checking and comprehensive system analysis.
"""
import subprocess
import sys
import yaml
import re
import time
from argparse import ArgumentParser, Namespace
from pathlib import Path
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from typing import Union, Dict, Any, Optional
from .base import BaseCommand
from ..core.status_analyzer import TinyTorchStatusAnalyzer
class StatusCommand(BaseCommand):
@property
def name(self) -> str:
return "status"
@property
def description(self) -> str:
return "Check status of all modules"
def add_arguments(self, parser: ArgumentParser) -> None:
parser.add_argument("--progress", action="store_true", help="Show user progress (modules + milestones) - DEFAULT")
parser.add_argument("--files", action="store_true", help="Show file structure and module status")
parser.add_argument("--details", action="store_true", help="Show detailed file structure")
parser.add_argument("--metadata", action="store_true", help="Show module metadata information")
parser.add_argument("--test-status", action="store_true", help="Include test execution status (slower)")
parser.add_argument("--comprehensive", action="store_true", help="Run comprehensive system health dashboard (environment + compliance + testing)")
def _get_export_target(self, module_path: Path) -> str:
"""
Read the actual export target from the dev file's #| default_exp directive.
Same logic as the export command.
"""
# Extract short name from module directory name for dev file
module_name = module_path.name
if module_name.startswith(tuple(f"{i:02d}_" for i in range(100))):
short_name = module_name[3:] # Remove "00_" prefix
else:
short_name = module_name
dev_file = module_path / f"{short_name}.py"
if not dev_file.exists():
return "not_found"
try:
with open(dev_file, 'r', encoding='utf-8') as f:
content = f.read()
# Look for #| default_exp directive
match = re.search(r'#\|\s*default_exp\s+([^\n\r]+)', content)
if match:
return match.group(1).strip()
return "no_export"
except Exception:
return "read_error"
def _count_test_functions(self, dev_file: Path) -> int:
"""Count the number of test functions in a dev file."""
try:
with open(dev_file, 'r', encoding='utf-8') as f:
content = f.read()
# Count lines that start with "def test_"
lines = content.split('\n')
test_functions = [line for line in lines if line.strip().startswith('def test_')]
return len(test_functions)
except Exception:
return 0
def _count_export_functions(self, dev_file: Path) -> int:
"""Count the number of exported functions/classes in a dev file."""
try:
with open(dev_file, 'r', encoding='utf-8') as f:
content = f.read()
# Count lines that have #| export directive
lines = content.split('\n')
export_lines = [line for line in lines if line.strip().startswith('#| export')]
return len(export_lines)
except Exception:
return 0
def run(self, args: Namespace) -> int:
console = self.console
# Handle comprehensive analysis mode
if args.comprehensive:
return self._run_comprehensive_analysis()
# Handle progress view (default if no flags, or --progress)
if not args.files and not args.details and not args.metadata and not args.test_status:
return self._run_progress_view()
if args.progress:
return self._run_progress_view()
# Standard file status check mode
return self._run_standard_status(args)
def _run_progress_view(self) -> int:
"""Show unified user progress view (modules + milestones)."""
console = self.console
import json
from datetime import datetime
# Load progress data
progress_file = Path(".tito") / "progress.json"
milestones_file = Path(".tito") / "milestones.json"
# Load module progress
if progress_file.exists():
progress_data = json.loads(progress_file.read_text())
completed_modules = progress_data.get("completed_modules", [])
completion_dates = progress_data.get("completion_dates", {})
else:
completed_modules = []
completion_dates = {}
# Load milestone achievements
if milestones_file.exists():
milestones_data = json.loads(milestones_file.read_text())
completed_milestones = milestones_data.get("completed_milestones", [])
milestone_dates = milestones_data.get("completion_dates", {})
else:
completed_milestones = []
milestone_dates = {}
# Calculate progress percentages
total_modules = 20
total_milestones = 6
modules_percent = int((len(completed_modules) / total_modules) * 100)
milestones_percent = int((len(completed_milestones) / total_milestones) * 100)
# Create summary panel
summary_text = Text()
summary_text.append(f"📦 Modules Completed: ", style="bold")
summary_text.append(f"{len(completed_modules)}/{total_modules} ({modules_percent}%)\n", style="cyan")
summary_text.append(f"🏆 Milestones Achieved: ", style="bold")
summary_text.append(f"{len(completed_milestones)}/{total_milestones} ({milestones_percent}%)\n\n", style="magenta")
# Last activity
all_dates = list(completion_dates.values()) + list(milestone_dates.values())
if all_dates:
latest_date = max(all_dates)
summary_text.append("📍 Last Activity: ", style="bold")
summary_text.append(f"{latest_date}\n", style="dim")
console.print(Panel(
summary_text,
title="📊 TinyTorch Progress",
border_style="bright_cyan"
))
# Module Progress Table
if completed_modules:
console.print("\n[bold]Module Progress:[/bold]")
for i in range(1, total_modules + 1):
mod_num = i
if mod_num in completed_modules:
module_name = self._get_module_name(mod_num)
console.print(f" [green]✅ {mod_num:02d} {module_name}[/green]")
elif i <= len(completed_modules) + 3: # Show next few modules
module_name = self._get_module_name(mod_num)
console.print(f" [dim]🔒 {mod_num:02d} {module_name}[/dim]")
# Milestone Achievements
if completed_milestones or (completed_modules and len(completed_modules) >= 1):
console.print("\n[bold]Milestone Achievements:[/bold]")
milestone_names = {
"01": "Perceptron (1957)",
"02": "Backpropagation (1986)",
"03": "MLP Revival (1986)",
"04": "CNN Revolution (1998)",
"05": "Transformer Era (2017)",
"06": "MLPerf (2018)"
}
for mid in ["01", "02", "03", "04", "05", "06"]:
if mid in completed_milestones:
console.print(f" [magenta]✅ {mid} - {milestone_names[mid]}[/magenta]")
else:
# Check if ready
prereqs_met = self._check_milestone_prereqs(mid, completed_modules)
if prereqs_met:
console.print(f" [yellow]🎯 {mid} - {milestone_names[mid]} [Ready!][/yellow]")
else:
console.print(f" [dim]🔒 {mid} - {milestone_names[mid]}[/dim]")
console.print()
return 0
def _get_module_name(self, module_num: int) -> str:
"""Get module name from number."""
module_names = {
1: "Tensor", 2: "Activations", 3: "Layers", 4: "Losses",
5: "Autograd", 6: "Optimizers", 7: "Training", 8: "DataLoader",
9: "Convolutions", 10: "Normalization", 11: "Tokenization",
12: "Embeddings", 13: "Attention", 14: "Transformers",
15: "Profiling", 16: "Quantization", 17: "Compression",
18: "Memoization", 19: "Benchmarking", 20: "Capstone"
}
return module_names.get(module_num, "Unknown")
def _check_milestone_prereqs(self, milestone_id: str, completed_modules: list) -> bool:
"""Check if milestone prerequisites are met."""
prereqs = {
"01": [1],
"02": [1, 2, 3, 4, 5],
"03": [1, 2, 3, 4, 5, 6, 7],
"04": [1, 2, 3, 4, 5, 6, 7, 8, 9],
"05": [1, 2, 3, 4, 5, 6, 7, 11, 12, 13, 14],
"06": [1, 2, 3, 4, 5, 6, 7, 8, 9, 15, 16, 19]
}
required = prereqs.get(milestone_id, [])
return all(mod in completed_modules for mod in required)
def _run_comprehensive_analysis(self) -> int:
"""Run comprehensive system health dashboard."""
console = self.console
start_time = time.time()
console.print("🚀 Starting TinyTorch Comprehensive Status Check...", style="bold green")
# Initialize analyzer
analyzer = TinyTorchStatusAnalyzer()
# Run full analysis
result = analyzer.run_full_analysis()
# Generate comprehensive report
analyzer.generate_comprehensive_report(console)
# Summary
total_time = time.time() - start_time
console.print(f"\n⏱️ Comprehensive analysis completed in {total_time:.1f}s", style="dim")
# Return appropriate exit code
if result['summary']['environment_healthy'] and result['summary']['working_modules'] >= result['summary']['total_modules'] * 0.8:
return 0 # Success
else:
return 1 # Issues found
def _run_standard_status(self, args: Namespace) -> int:
"""Run standard status check mode."""
console = self.console
# Scan modules directory
modules_dir = Path("modules")
if not modules_dir.exists():
console.print(Panel("[red]❌ modules/ directory not found[/red]",
title="Error", border_style="red"))
return 1
# Find all module directories (exclude special directories)
exclude_dirs = {'.quarto', '__pycache__', '.git', '.pytest_cache'}
module_dirs = [d for d in modules_dir.iterdir()
if d.is_dir() and d.name not in exclude_dirs]
if not module_dirs:
console.print(Panel("[yellow]⚠️ No modules found in modules/ directory[/yellow]",
title="Warning", border_style="yellow"))
return 0
console.print(Panel(f"📋 Found {len(module_dirs)} modules in modules directory",
title="Module Status Check", border_style="bright_cyan"))
# Create status table
status_table = Table(title="Module Status Overview", show_header=True, header_style="bold blue")
status_table.add_column("Module", style="bold cyan", width=17)
status_table.add_column("Status", width=12, justify="center")
status_table.add_column("Dev File", width=12, justify="center")
status_table.add_column("Inline Tests", width=12, justify="center")
status_table.add_column("External Tests", width=12, justify="center")
status_table.add_column("README", width=12, justify="center")
if args.metadata:
status_table.add_column("Export Target", width=20, justify="center")
status_table.add_column("Prerequisites", width=15, justify="center")
# Check each module
modules_status = []
for module_dir in sorted(module_dirs):
module_name = module_dir.name
status = self._check_module_status(module_dir, args.test_status)
modules_status.append((module_name, status))
# Add to table
row = [
module_name,
self._format_status(status['overall_status']),
self._format_file_status(status['dev_file'], status.get('export_count', 0)),
self._format_inline_tests(status['inline_test_count']),
self._format_external_tests(status['external_tests'], status.get('external_test_status')),
"" if status['readme'] else ""
]
# Add metadata columns if requested
if args.metadata:
metadata = status.get('metadata', {})
export_target = status.get('export_target', 'unknown')
row.append(export_target if export_target not in ['not_found', 'no_export', 'read_error'] else export_target)
# Show prerequisites from dependencies
deps = metadata.get('dependencies', {})
prereqs = deps.get('prerequisites', [])
row.append(', '.join(prereqs) if prereqs else 'none')
status_table.add_row(*row)
console.print(status_table)
# Summary with better logic
total_modules = len(modules_status)
# A module is "working" if it has a dev file with implementations
working_modules = sum(1 for _, status in modules_status
if status['dev_file'] and status.get('export_count', 0) > 0)
# A module is "complete" if it has everything
complete_modules = sum(1 for _, status in modules_status
if status['dev_file'] and status['external_tests'] and status['readme'] and status.get('export_count', 0) > 0)
console.print(f"\n📊 Summary:")
console.print(f" 🏗️ Working modules: {working_modules}/{total_modules} (have implementations)")
console.print(f" ✅ Complete modules: {complete_modules}/{total_modules} (have implementations, tests, docs)")
# Helpful commands
console.print(f"\n💡 Quick commands:")
console.print(f" [bold cyan]tito status --comprehensive[/bold cyan] # Full system health dashboard")
console.print(f" [bold cyan]tito module test --all[/bold cyan] # Test all modules")
console.print(f" [bold cyan]tito module test MODULE_NAME[/bold cyan] # Test specific module")
console.print(f" [bold cyan]pytest modules/*/ -k test_[/bold cyan] # Run pytest on inline tests")
console.print(f" [bold cyan]pytest tests/test_*.py[/bold cyan] # Run external tests")
# Detailed view
if args.details:
console.print("\n" + "="*60)
console.print("📁 Detailed Module Structure")
console.print("="*60)
for module_name, status in modules_status:
self._print_module_details(module_name, status)
# Metadata view
if args.metadata:
console.print("\n" + "="*60)
console.print("📊 Module Metadata")
console.print("="*60)
for module_name, status in modules_status:
if status.get('metadata'):
self._print_module_metadata(module_name, status['metadata'])
return 0
def _check_module_status(self, module_dir: Path, check_tests: bool = False) -> dict:
"""Check the status of a single module."""
module_name = module_dir.name
# Check for required files
# Extract short name from module directory name for dev file
if module_name.startswith(tuple(f"{i:02d}_" for i in range(100))):
short_name = module_name[3:] # Remove "00_" prefix
else:
short_name = module_name
dev_file = module_dir / f"{short_name}.py"
readme_file = module_dir / "README.md"
metadata_file = module_dir / "module.yaml"
# Check for tests in main tests directory
# Extract short name from module directory name (e.g., "01_tensor" -> "tensor")
if module_name.startswith(tuple(f"{i:02d}_" for i in range(100))):
short_name = module_name[3:] # Remove "00_" prefix
else:
short_name = module_name
main_test_file = Path("tests") / f"test_{short_name}.py"
status = {
'dev_file': dev_file.exists(),
'readme': readme_file.exists(),
'metadata_file': metadata_file.exists(),
'external_tests': main_test_file.exists(),
'inline_test_count': 0,
'export_count': 0,
'export_target': 'not_found',
'external_test_status': None,
'overall_status': 'unknown',
'metadata': None
}
# Count inline tests and exports if dev file exists
if dev_file.exists():
status['inline_test_count'] = self._count_test_functions(dev_file)
status['export_count'] = self._count_export_functions(dev_file)
status['export_target'] = self._get_export_target(module_dir)
# Run external tests if requested (slower)
if check_tests and main_test_file.exists():
status['external_test_status'] = self._check_external_tests(main_test_file)
# Determine overall status
status['overall_status'] = self._determine_overall_status(status)
# Load metadata if available
if metadata_file.exists():
try:
with open(metadata_file, 'r') as f:
metadata = yaml.safe_load(f)
status['metadata'] = metadata
except Exception as e:
status['metadata'] = {'error': str(e)}
return status
def _determine_overall_status(self, status: dict) -> str:
"""Determine overall module status based on files and implementation."""
# If no dev file, module is not started
if not status['dev_file']:
return 'not_started'
# If dev file exists but no implementations, module is empty
if status.get('export_count', 0) == 0:
return 'empty'
# If has implementations but no tests, module is in progress
if status.get('inline_test_count', 0) == 0 and not status.get('external_tests', False):
return 'no_tests'
# If has implementations and tests, module is working
if status.get('export_count', 0) > 0 and (status.get('inline_test_count', 0) > 0 or status.get('external_tests', False)):
return 'working'
return 'unknown'
def _check_external_tests(self, test_file: Path) -> str:
"""Check if external tests pass (used only when --test-status is specified)."""
try:
result = subprocess.run(
[sys.executable, "-m", "pytest", str(test_file), "-q", "--tb=no"],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
return 'passing'
else:
return 'failing'
except (subprocess.TimeoutExpired, FileNotFoundError):
return 'error'
def _format_status(self, status: str) -> str:
"""Format overall module status with appropriate emoji and color."""
status_map = {
'working': '', # Has implementations and tests
'no_tests': '🚧', # Has implementations but no tests
'empty': '📝', # Has dev file but no implementations
'not_started': '', # No dev file
'unknown': ''
}
return status_map.get(status, '')
def _format_file_status(self, exists: bool, export_count: int) -> str:
"""Format dev file status showing if it has implementations."""
if not exists:
return ""
if export_count == 0:
return "📝" # File exists but empty
return f"✅({export_count})" # File exists with implementations
def _format_inline_tests(self, test_count: int) -> str:
"""Format inline test count."""
if test_count == 0:
return ""
return f"✅({test_count})"
def _format_external_tests(self, exists: bool, test_status: Optional[str] = None) -> str:
"""Format external test status."""
if not exists:
return ""
if test_status == 'passing':
return ""
elif test_status == 'failing':
return "🔴"
elif test_status == 'error':
return "⚠️"
else:
return "" # Exists but not tested
def _print_module_details(self, module_name: str, status: dict) -> None:
"""Print detailed information about a module."""
console = self.console
# Module header
console.print(f"\n📦 {module_name.upper()}", style="bold cyan")
console.print("-" * 40)
# File structure
files_table = Table(show_header=False, box=None, padding=(0, 2))
files_table.add_column("File", style="dim")
files_table.add_column("Status")
dev_status = "✅ Found" if status['dev_file'] else "❌ Missing"
if status['dev_file']:
dev_status += f" ({status.get('export_count', 0)} exports, {status.get('inline_test_count', 0)} inline tests)"
files_table.add_row(f"{module_name}.py", dev_status)
files_table.add_row("tests/test_*.py", "✅ Found" if status['external_tests'] else "❌ Missing")
files_table.add_row("README.md", "✅ Found" if status['readme'] else "❌ Missing")
console.print(files_table)
# Pytest commands
if status['dev_file'] or status['external_tests']:
console.print("\n[dim]💡 Test commands:[/dim]")
if status['dev_file']:
console.print(f"[dim] pytest modules/{module_name}/{module_name}.py -k test_[/dim]")
if status['external_tests']:
short_name = module_name[3:] if module_name.startswith(tuple(f"{i:02d}_" for i in range(100))) else module_name
console.print(f"[dim] pytest tests/test_{short_name}.py -v[/dim]")
def _print_module_metadata(self, module_name: str, metadata: dict) -> None:
"""Print detailed metadata information about a module."""
console = self.console
# Module header
title = metadata.get('title', module_name.title())
console.print(f"\n📦 {title}", style="bold cyan")
console.print("-" * (len(title) + 4))
# Basic info
if metadata.get('description'):
console.print(f"📝 {metadata['description']}")
# Export info (read from dev file - source of truth)
module_path = Path(f"modules/{module_name}")
export_target = self._get_export_target(module_path)
if export_target not in ['not_found', 'no_export', 'read_error']:
console.print(f"📦 Exports to: {export_target}")
# Dependencies
if metadata.get('dependencies'):
deps = metadata['dependencies']
console.print("\n🔗 Dependencies:")
if deps.get('prerequisites'):
console.print(f" Prerequisites: {', '.join(deps['prerequisites'])}")
if deps.get('enables'):
console.print(f" Enables: {', '.join(deps['enables'])}")
# Components
if metadata.get('components'):
console.print("\n🧩 Components:")
for component in metadata['components']:
console.print(f"{component}")
# Files
if metadata.get('files'):
files = metadata['files']
console.print("\n📁 Files:")
if files.get('dev_file'):
console.print(f" • Dev: {files['dev_file']}")
if files.get('test_file'):
console.print(f" • Test: {files['test_file']}")
if files.get('readme'):
console.print(f" • README: {files['readme']}")