Remove redundant py_to_notebook tool in favor of Jupytext

- Delete bin/py_to_notebook.py and tito/tools/py_to_notebook.py
- Update notebooks command to use Jupytext directly
- Jupytext is already configured in all *_dev.py files
- Simpler, more standard workflow using established tools
- Better integration with NBDev ecosystem

Benefits:
- Eliminates duplicate conversion tools
- Uses industry-standard Jupytext instead of custom tool
- Reduces maintenance burden
- Better error handling and compatibility
This commit is contained in:
Vijay Janapa Reddi
2025-07-11 23:31:55 -04:00
parent 3198100eb9
commit b2794273f6
6 changed files with 691 additions and 944 deletions

View File

@@ -54,7 +54,7 @@ tito/
│ └── notebooks.py # Notebooks command │ └── notebooks.py # Notebooks command
└── tools/ # CLI tools └── tools/ # CLI tools
├── __init__.py ├── __init__.py
└── py_to_notebook.py # Conversion tool └── tito # Main CLI script
``` ```
## 🎯 Design Patterns Applied ## 🎯 Design Patterns Applied

View File

@@ -1,122 +0,0 @@
#!/usr/bin/env python3
"""
Convert Python files with cell markers to Jupyter notebooks.
Usage:
python3 bin/py_to_notebook.py modules/tensor/tensor_dev.py
python3 bin/py_to_notebook.py modules/tensor/tensor_dev.py --output custom_name.ipynb
"""
import argparse
import json
import re
import sys
from pathlib import Path
def convert_py_to_notebook(py_file: Path, output_file: Path = None):
"""Convert Python file with cell markers to notebook."""
if not py_file.exists():
print(f"❌ File not found: {py_file}")
return False
# Read the Python file
with open(py_file, 'r') as f:
content = f.read()
# Split into cells based on # %% markers
cells = re.split(r'^# %%.*$', content, flags=re.MULTILINE)
cells = [cell.strip() for cell in cells if cell.strip()]
# Create notebook structure
notebook = {
'cells': [],
'metadata': {
'kernelspec': {
'display_name': 'Python 3',
'language': 'python',
'name': 'python3'
},
'language_info': {
'name': 'python',
'version': '3.8.0'
}
},
'nbformat': 4,
'nbformat_minor': 4
}
for i, cell_content in enumerate(cells):
if not cell_content:
continue
# Check if this is a markdown cell
if cell_content.startswith('# ') and '\n' in cell_content:
lines = cell_content.split('\n')
if lines[0].startswith('# ') and not any(line.strip() and not line.startswith('#') for line in lines[:5]):
# This looks like a markdown cell
cell = {
'cell_type': 'markdown',
'metadata': {},
'source': []
}
for line in lines:
if line.startswith('# '):
cell['source'].append(line[2:] + '\n')
elif line.startswith('#'):
cell['source'].append(line[1:] + '\n')
elif line.strip() == '':
cell['source'].append('\n')
notebook['cells'].append(cell)
continue
# Code cell
cell = {
'cell_type': 'code',
'execution_count': None,
'metadata': {},
'outputs': [],
'source': []
}
for line in cell_content.split('\n'):
cell['source'].append(line + '\n')
# Remove trailing newline from last line
if cell['source'] and cell['source'][-1].endswith('\n'):
cell['source'][-1] = cell['source'][-1][:-1]
notebook['cells'].append(cell)
# Determine output file
if output_file is None:
output_file = py_file.with_suffix('.ipynb')
# Write notebook
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, 'w') as f:
json.dump(notebook, f, indent=2)
print(f"✅ Converted {py_file}{output_file}")
return True
def main():
parser = argparse.ArgumentParser(description="Convert Python files to Jupyter notebooks")
parser.add_argument('input_file', type=Path, help='Input Python file')
parser.add_argument('--output', '-o', type=Path, help='Output notebook file')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
args = parser.parse_args()
success = convert_py_to_notebook(args.input_file, args.output)
if not success:
sys.exit(1)
if args.verbose:
print("🎉 Conversion complete!")
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
""" """
Notebooks command for building Jupyter notebooks from Python files. Notebooks command for building Jupyter notebooks from Python files using Jupytext.
""" """
import subprocess import subprocess
@@ -15,7 +15,7 @@ from .base import BaseCommand
from ..core.exceptions import ExecutionError, ModuleNotFoundError from ..core.exceptions import ExecutionError, ModuleNotFoundError
class NotebooksCommand(BaseCommand): class NotebooksCommand(BaseCommand):
"""Command to build Jupyter notebooks from Python files.""" """Command to build Jupyter notebooks from Python files using Jupytext."""
@property @property
def name(self) -> str: def name(self) -> str:
@@ -62,33 +62,31 @@ class NotebooksCommand(BaseCommand):
return dev_files return dev_files
def _convert_file(self, dev_file: Path) -> Tuple[bool, str]: def _convert_file(self, dev_file: Path) -> Tuple[bool, str]:
"""Convert a single Python file to notebook.""" """Convert a single Python file to notebook using Jupytext."""
try: try:
py_to_notebook_tool = self.config.bin_dir / "py_to_notebook.py" # Use Jupytext to convert Python file to notebook
result = subprocess.run([ result = subprocess.run([
sys.executable, str(py_to_notebook_tool), str(dev_file) "jupytext", "--to", "notebook", str(dev_file)
], capture_output=True, text=True, timeout=30) ], capture_output=True, text=True, timeout=30, cwd=dev_file.parent)
if result.returncode == 0: if result.returncode == 0:
# Extract success message from the tool output notebook_file = dev_file.with_suffix('.ipynb')
output_lines = result.stdout.strip().split('\n') return True, f"{dev_file.name}{notebook_file.name}"
success_msg = output_lines[-1] if output_lines else f"{dev_file.name}{dev_file.with_suffix('.ipynb').name}"
# Clean up the message
clean_msg = success_msg.replace('', '').replace('Converted ', '')
return True, clean_msg
else: else:
error_msg = result.stderr.strip() if result.stderr.strip() else "Conversion failed" error_msg = result.stderr.strip() if result.stderr.strip() else "Conversion failed"
return False, error_msg return False, error_msg
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
return False, "Conversion timed out" return False, "Conversion timed out"
except FileNotFoundError:
return False, "Jupytext not found. Install with: pip install jupytext"
except Exception as e: except Exception as e:
return False, f"Error: {str(e)}" return False, f"Error: {str(e)}"
def run(self, args: Namespace) -> int: def run(self, args: Namespace) -> int:
"""Execute the notebooks command.""" """Execute the notebooks command."""
self.console.print(Panel( self.console.print(Panel(
"📓 Building Notebooks from Python Files", "📓 Building Notebooks from Python Files (using Jupytext)",
title="Notebook Generation", title="Notebook Generation",
border_style="bright_cyan" border_style="bright_cyan"
)) ))
@@ -149,8 +147,8 @@ class NotebooksCommand(BaseCommand):
summary_text.append("\n💡 Next steps:\n", style="bold yellow") summary_text.append("\n💡 Next steps:\n", style="bold yellow")
summary_text.append(" • Open notebooks with: jupyter lab\n", style="white") 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(" • Work interactively in the notebooks\n", style="white")
summary_text.append(" • Export code with: tito sync\n", style="white") summary_text.append(" • Export code with: tito package sync\n", style="white")
summary_text.append(" • Run tests with: tito test\n", style="white") summary_text.append(" • Run tests with: tito module test\n", style="white")
border_style = "green" if error_count == 0 else "yellow" border_style = "green" if error_count == 0 else "yellow"
self.console.print(Panel( self.console.print(Panel(

View File

@@ -4,6 +4,6 @@ CLI Tools package.
Contains utility tools used by the CLI commands. Contains utility tools used by the CLI commands.
""" """
from .py_to_notebook import convert_py_to_notebook # No tools currently - py_to_notebook removed in favor of Jupytext
__all__ = ['convert_py_to_notebook'] __all__ = []

View File

@@ -1,122 +0,0 @@
#!/usr/bin/env python3
"""
Convert Python files with cell markers to Jupyter notebooks.
Usage:
python3 bin/py_to_notebook.py modules/tensor/tensor_dev.py
python3 bin/py_to_notebook.py modules/tensor/tensor_dev.py --output custom_name.ipynb
"""
import argparse
import json
import re
import sys
from pathlib import Path
def convert_py_to_notebook(py_file: Path, output_file: Path = None):
"""Convert Python file with cell markers to notebook."""
if not py_file.exists():
print(f"❌ File not found: {py_file}")
return False
# Read the Python file
with open(py_file, 'r') as f:
content = f.read()
# Split into cells based on # %% markers
cells = re.split(r'^# %%.*$', content, flags=re.MULTILINE)
cells = [cell.strip() for cell in cells if cell.strip()]
# Create notebook structure
notebook = {
'cells': [],
'metadata': {
'kernelspec': {
'display_name': 'Python 3',
'language': 'python',
'name': 'python3'
},
'language_info': {
'name': 'python',
'version': '3.8.0'
}
},
'nbformat': 4,
'nbformat_minor': 4
}
for i, cell_content in enumerate(cells):
if not cell_content:
continue
# Check if this is a markdown cell
if cell_content.startswith('# ') and '\n' in cell_content:
lines = cell_content.split('\n')
if lines[0].startswith('# ') and not any(line.strip() and not line.startswith('#') for line in lines[:5]):
# This looks like a markdown cell
cell = {
'cell_type': 'markdown',
'metadata': {},
'source': []
}
for line in lines:
if line.startswith('# '):
cell['source'].append(line[2:] + '\n')
elif line.startswith('#'):
cell['source'].append(line[1:] + '\n')
elif line.strip() == '':
cell['source'].append('\n')
notebook['cells'].append(cell)
continue
# Code cell
cell = {
'cell_type': 'code',
'execution_count': None,
'metadata': {},
'outputs': [],
'source': []
}
for line in cell_content.split('\n'):
cell['source'].append(line + '\n')
# Remove trailing newline from last line
if cell['source'] and cell['source'][-1].endswith('\n'):
cell['source'][-1] = cell['source'][-1][:-1]
notebook['cells'].append(cell)
# Determine output file
if output_file is None:
output_file = py_file.with_suffix('.ipynb')
# Write notebook
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, 'w') as f:
json.dump(notebook, f, indent=2)
print(f"✅ Converted {py_file}{output_file}")
return True
def main():
parser = argparse.ArgumentParser(description="Convert Python files to Jupyter notebooks")
parser.add_argument('input_file', type=Path, help='Input Python file')
parser.add_argument('--output', '-o', type=Path, help='Output notebook file')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
args = parser.parse_args()
success = convert_py_to_notebook(args.input_file, args.output)
if not success:
sys.exit(1)
if args.verbose:
print("🎉 Conversion complete!")
if __name__ == "__main__":
main()