| |
| """ |
| Print to Logger Migration Script |
| |
| Automatically replaces print() statements with logger.info/error/debug calls. |
| |
| Usage: |
| python scripts/migrate_print_to_logging.py [--dry-run] [--file path/to/file.py] |
| |
| Options: |
| --dry-run: Show changes without applying them |
| --file: Migrate a single file (default: migrate all non-test files) |
| """ |
|
|
| import argparse |
| import ast |
| from pathlib import Path |
| import re |
| from typing import List, Tuple |
|
|
|
|
| class PrintToLoggerMigrator: |
| """Migrate print() statements to logger calls""" |
|
|
| def __init__(self, dry_run: bool = False): |
| self.dry_run = dry_run |
| self.changes_made = 0 |
|
|
| def migrate_file(self, filepath: str) -> Tuple[bool, List[str]]: |
| """Migrate a single Python file""" |
| try: |
| with open(filepath, 'r') as f: |
| content = f.read() |
|
|
| changes = [] |
| original_content = content |
|
|
| |
| has_logger = 'logger = logging.getLogger(__name__)' in content |
|
|
| |
| if not has_logger: |
| content, import_changes = self._add_logger_import(content) |
| changes.extend(import_changes) |
|
|
| |
| content, print_changes = self._replace_print_statements(content) |
| changes.extend(print_changes) |
|
|
| |
| if content != original_content and changes: |
| if self.dry_run: |
| print(f"\n{'='*60}") |
| print(f"File: {filepath}") |
| print(f"{'='*60}") |
| for change in changes: |
| print(f" - {change}") |
| else: |
| with open(filepath, 'w') as f: |
| f.write(content) |
| self.changes_made += 1 |
|
|
| return True, changes |
|
|
| return False, ["No print() statements found"] |
|
|
| except Exception as e: |
| print(f"Error migrating {filepath}: {e}") |
| return False, [f"Error: {e}"] |
|
|
| def _add_logger_import(self, content: str) -> Tuple[str, List[str]]: |
| """Add logging import and logger initialization""" |
| changes = [] |
|
|
| |
| if 'import logging' in content: |
| |
| if 'logger = logging.getLogger(__name__)' not in content: |
| |
| import_end = content.find('\n\n') |
| if import_end == -1: |
| import_end = 0 |
|
|
| |
| content = content[:import_end] + '\n\nlogger = logging.getLogger(__name__)' + content[import_end:] |
| changes.append("Added logger initialization") |
| else: |
| |
| content = 'import logging\n\n' + content |
|
|
| |
| if 'logger = logging.getLogger(__name__)' not in content: |
| content = 'import logging\n\nlogger = logging.getLogger(__name__)\\n\\n' + content[content.find('\\n')+1:] |
|
|
| changes.append("Added logging import and logger initialization") |
|
|
| return content, changes |
|
|
| def _replace_print_statements(self, content: str) -> Tuple[str, List[str]]: |
| """Replace print() statements with logger calls""" |
| changes = [] |
|
|
| |
| |
| |
|
|
| |
| patterns = [ |
| |
| (r'print\("([^"]+)"\)', r'logger.info("\1")'), |
|
|
| |
| (r"print\('([^']+)'\)", r"logger.info('\1')"), |
|
|
| |
| (r'print\(f"([^"]+)"\)', r'logger.info(f"\1")'), |
| (r"print\(f'([^']+)'\)", r"logger.info(f'\1')"), |
|
|
| |
| (r'print\((\w+)\)', r'logger.info(str(\1))'), |
| ] |
|
|
| for pattern, replacement in patterns: |
| matches = re.findall(pattern, content) |
| if matches: |
| content = re.sub(pattern, replacement, content) |
| changes.append(f"Replaced {len(matches)} print() statement(s)") |
|
|
| return content, changes |
|
|
|
|
| def find_python_files() -> List[str]: |
| """Find all Python files (excluding tests)""" |
| base_dir = Path(__file__).parent.parent |
| python_files = [] |
|
|
| |
| for py_file in base_dir.rglob('*.py'): |
| |
| if ( |
| not py_file.name.startswith('test_') |
| and py_file.name != '__init__.py' |
| and 'venv' not in py_file.parts |
| and '.venv' not in py_file.parts |
| and 'site-packages' not in py_file.parts |
| ): |
| python_files.append(str(py_file)) |
|
|
| return sorted(python_files) |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description='Migrate print() to logger') |
| parser.add_argument('--dry-run', action='store_true', help='Show changes without applying') |
| parser.add_argument('--file', type=str, help='Migrate a single file') |
| args = parser.parse_args() |
|
|
| migrator = PrintToLoggerMigrator(dry_run=args.dry_run) |
|
|
| if args.file: |
| files = [args.file] |
| else: |
| files = find_python_files() |
|
|
| print(f"Found {len(files)} Python files to check\n") |
|
|
| results = { |
| 'success': 0, |
| 'no_changes': 0, |
| 'errors': 0 |
| } |
|
|
| for filepath in files: |
| success, changes = migrator.migrate_file(filepath) |
|
|
| if success: |
| results['success'] += 1 |
| if not args.dry_run: |
| print(f"✓ {filepath}") |
| for change in changes: |
| print(f" - {change}") |
| elif 'No print()' in changes[0]: |
| results['no_changes'] += 1 |
| else: |
| results['errors'] += 1 |
| print(f"✗ {filepath}: {changes[0]}") |
|
|
| print(f"\n{'='*60}") |
| print("Migration Summary:") |
| print(f" Files migrated: {results['success']}") |
| print(f" Files unchanged: {results['no_changes']}") |
| print(f" Files with errors: {results['errors']}") |
| print(f"{'='*60}") |
|
|
| if args.dry_run: |
| print("\nDRY RUN MODE - No changes were applied") |
| print("Run without --dry-run to apply changes") |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|