Files
TinyTorch/bin/generate_student_notebooks.py
Vijay Janapa Reddi 77150be3a6 Module 00_setup migration: Core functionality complete, NBGrader architecture issue discovered
 COMPLETED:
- Instructor solution executes perfectly
- NBDev export works (fixed import directives)
- Package functionality verified
- Student assignment generation works
- CLI integration complete
- Systematic testing framework established

⚠️ CRITICAL DISCOVERY:
- NBGrader requires cell metadata architecture changes
- Current generator creates content correctly but wrong cell types
- Would require major rework of assignment generation pipeline

📊 STATUS:
- Core TinyTorch functionality:  READY FOR STUDENTS
- NBGrader integration: Requires Phase 2 rework
- Ready to continue systematic testing of modules 01-06

🔧 FIXES APPLIED:
- Added #| export directive to imports in enhanced modules
- Fixed generator logic for student scaffolding
- Updated testing framework and documentation
2025-07-12 09:08:45 -04:00

332 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
"""
TinyTorch Student Notebook Generator
Transforms complete implementation notebooks into student exercise versions.
Uses special markers to identify what becomes student exercises.
Usage:
python bin/generate_student_notebooks.py --module tensor
python bin/generate_student_notebooks.py --all
"""
import argparse
import json
import re
from pathlib import Path
from typing import Dict, List, Tuple, Any
import sys
class NotebookGenerator:
"""Transforms complete notebooks into student exercise versions with nbgrader support."""
def __init__(self, use_nbgrader=False):
self.use_nbgrader = use_nbgrader
self.markers = {
# TinyTorch markers (existing)
'exercise_start': '#| exercise_start',
'exercise_end': '#| exercise_end',
'hint': '#| hint:',
'solution_test': '#| solution_test:',
'difficulty': '#| difficulty:',
'keep_imports': '#| keep_imports',
'remove_cell': '#| remove_cell',
# nbgrader markers (new)
'nbgrader_solution_begin': '### BEGIN SOLUTION',
'nbgrader_solution_end': '### END SOLUTION',
'nbgrader_hidden_tests_begin': '### BEGIN HIDDEN TESTS',
'nbgrader_hidden_tests_end': '### END HIDDEN TESTS'
}
def process_notebook(self, notebook_path: Path) -> Dict[str, Any]:
"""Transform a complete notebook into student version."""
print(f"📝 Processing: {notebook_path}")
with open(notebook_path, 'r') as f:
notebook = json.load(f)
processed_cells = []
for cell in notebook['cells']:
processed_cell = self._process_cell(cell)
if processed_cell: # None means remove cell
processed_cells.append(processed_cell)
notebook['cells'] = processed_cells
return notebook
def _process_cell(self, cell: Dict[str, Any]) -> Dict[str, Any]:
"""Process a single notebook cell with both TinyTorch and nbgrader support."""
if cell['cell_type'] != 'code':
return cell # Keep markdown cells as-is
source_lines = cell['source']
if not source_lines:
return cell
# Check for remove_cell marker
if any(self.markers['remove_cell'] in line for line in source_lines):
return None # Remove this cell
# Process nbgrader solution blocks
if any(self.markers['nbgrader_solution_begin'] in line for line in source_lines):
return self._transform_nbgrader_cell(cell)
# Check for TinyTorch exercise markers
if any(self.markers['exercise_start'] in line for line in source_lines):
return self._transform_exercise_cell(cell)
# Check for keep_imports marker
if any(self.markers['keep_imports'] in line for line in source_lines):
return self._clean_markers(cell)
return cell
def _transform_nbgrader_cell(self, cell: Dict[str, Any]) -> Dict[str, Any]:
"""Transform nbgrader solution blocks for student version."""
source_lines = cell['source']
new_lines = []
in_solution = False
in_hidden_tests = False
placeholder_added = False
for line in source_lines:
if self.markers['nbgrader_solution_begin'] in line:
in_solution = True
placeholder_added = False
if self.use_nbgrader:
new_lines.append(line) # Keep marker for nbgrader
# Add placeholder immediately after BEGIN SOLUTION
new_lines.append(" # YOUR CODE HERE\n")
new_lines.append(" raise NotImplementedError()\n")
placeholder_added = True
continue
elif self.markers['nbgrader_solution_end'] in line:
in_solution = False
if self.use_nbgrader:
new_lines.append(line) # Keep marker for nbgrader
continue
elif self.markers['nbgrader_hidden_tests_begin'] in line:
in_hidden_tests = True
if self.use_nbgrader:
new_lines.append(line) # Keep marker for nbgrader
continue
elif self.markers['nbgrader_hidden_tests_end'] in line:
in_hidden_tests = False
if self.use_nbgrader:
new_lines.append(line) # Keep marker for nbgrader
continue
elif in_solution:
# Skip solution lines (placeholder already added)
continue
elif in_hidden_tests:
# Keep hidden tests for nbgrader, remove for regular students
if self.use_nbgrader:
new_lines.append(line)
# Skip for regular students
continue
else:
# Keep non-solution lines
new_lines.append(line)
cell['source'] = new_lines
return cell
def _transform_exercise_cell(self, cell: Dict[str, Any]) -> Dict[str, Any]:
"""Transform a cell with exercise markers into student version."""
source_lines = cell['source']
new_lines = []
in_exercise = False
exercise_header_lines = [] # Store function def, docstring etc.
hints = []
solution_tests = []
difficulty = "medium"
for line in source_lines:
if self.markers['exercise_start'] in line:
in_exercise = True
continue
elif self.markers['exercise_end'] in line:
in_exercise = False
# Add the preserved header + exercise placeholder
new_lines.extend(exercise_header_lines)
new_lines.extend(self._create_exercise_placeholder(hints, solution_tests, difficulty))
# Reset for next exercise
exercise_header_lines = []
hints = []
solution_tests = []
difficulty = "medium"
continue
elif self.markers['hint'] in line:
hint = line.split(self.markers['hint'], 1)[1].strip()
hints.append(hint)
continue
elif self.markers['solution_test'] in line:
test = line.split(self.markers['solution_test'], 1)[1].strip()
solution_tests.append(test)
continue
elif self.markers['difficulty'] in line:
difficulty = line.split(self.markers['difficulty'], 1)[1].strip()
continue
elif in_exercise:
# Preserve function signature and docstring, skip implementation
if self._is_function_signature_or_docstring(line):
exercise_header_lines.append(line)
# Skip implementation lines (but keep signature/docstring)
continue
else:
# Keep non-exercise lines
new_lines.append(line)
cell['source'] = new_lines
return cell
def _is_function_signature_or_docstring(self, line: str) -> bool:
"""Check if line is part of function signature or docstring."""
stripped = line.strip()
# Empty lines
if not stripped:
return False
# Function definition
if (stripped.startswith('def ') or
stripped.startswith('class ') or
stripped.startswith('@')): # decorators
return True
# Function signature continuation (parameters on multiple lines)
if (stripped.endswith(',') or
stripped.endswith('\\') or
stripped.startswith(')') or
'->' in stripped):
return True
# Docstrings (triple quotes)
if ('"""' in stripped or "'''" in stripped):
return True
# Docstring content (common patterns)
if (stripped.startswith('Args:') or
stripped.startswith('Returns:') or
stripped.startswith('Raises:') or
stripped.startswith('Note:') or
stripped.startswith('Example:')):
return True
# Implementation code (skip these)
if (stripped.startswith('self.') or
stripped.startswith('if ') or
stripped.startswith('elif ') or
stripped.startswith('else:') or
stripped.startswith('for ') or
stripped.startswith('while ') or
stripped.startswith('return ') or
stripped.startswith('raise ') or
stripped.startswith('try:') or
stripped.startswith('except ') or
stripped.startswith('with ') or
'=' in stripped and not stripped.startswith('"""') and not stripped.startswith("'''")):
return False
# Comments (keep them as they might be part of docstring)
if stripped.startswith('#'):
return True
# If we're not sure and it's just text, assume it's docstring content
# This catches parameter descriptions, etc.
return True
def _create_exercise_placeholder(self, hints: List[str], tests: List[str], difficulty: str) -> List[str]:
"""Create TODO placeholder for students."""
lines = []
# Add difficulty indicator and description
difficulty_emoji = {"easy": "🟢", "medium": "🟡", "hard": "🔴"}
lines.append(f" # {difficulty_emoji.get(difficulty, '🟡')} TODO: Implement this method ({difficulty})\n")
# Add hints
for hint in hints:
lines.append(f" # HINT: {hint}\n")
# Add test guidance
for test in tests:
lines.append(f" # TEST: {test}\n")
lines.append(" \n")
lines.append(" # Your implementation here\n")
if self.use_nbgrader:
lines.append(" # YOUR CODE HERE\n")
lines.append(" raise NotImplementedError()\n")
else:
lines.append(" pass\n")
return lines
def _clean_markers(self, cell: Dict[str, Any]) -> Dict[str, Any]:
"""Remove generator markers from cell."""
source_lines = cell['source']
cleaned_lines = []
for line in source_lines:
# Skip marker lines
if any(marker in line for marker in self.markers.values()):
continue
cleaned_lines.append(line)
cell['source'] = cleaned_lines
return cell
def save_student_notebook(self, notebook: Dict[str, Any], output_path: Path):
"""Save the student version notebook."""
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w') as f:
json.dump(notebook, f, indent=2)
print(f"✅ Student version saved: {output_path}")
def main():
parser = argparse.ArgumentParser(description="Generate student exercise notebooks")
parser.add_argument('--module', type=str, help='Generate for specific module')
parser.add_argument('--all', action='store_true', help='Generate for all modules')
parser.add_argument('--output-suffix', default='_student', help='Suffix for student notebooks')
parser.add_argument('--nbgrader', action='store_true', help='Generate nbgrader-compatible notebooks')
args = parser.parse_args()
if not args.module and not args.all:
parser.error("Must specify either --module or --all")
generator = NotebookGenerator(use_nbgrader=args.nbgrader)
modules_dir = Path("modules")
if args.module:
modules = [args.module]
else:
modules = [d.name for d in modules_dir.iterdir() if d.is_dir()]
for module in modules:
module_dir = modules_dir / module
dev_notebook = module_dir / f"{module}_dev.ipynb"
if not dev_notebook.exists():
print(f"⚠️ No dev notebook found for {module}: {dev_notebook}")
continue
# Generate student version
notebook = generator.process_notebook(dev_notebook)
if args.nbgrader:
output_path = module_dir / f"{module}_assignment.ipynb"
generator.save_student_notebook(notebook, output_path)
else:
output_path = module_dir / f"{module}{args.output_suffix}.ipynb"
generator.save_student_notebook(notebook, output_path)
if __name__ == "__main__":
main()