File size: 6,698 Bytes
f84a02d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#!/usr/bin/env python3
"""
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

            # Check if file already has logging
            has_logger = 'logger = logging.getLogger(__name__)' in content

            # Add logger import if not present
            if not has_logger:
                content, import_changes = self._add_logger_import(content)
                changes.extend(import_changes)

            # Replace print() statements
            content, print_changes = self._replace_print_statements(content)
            changes.extend(print_changes)

            # Write 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 = []

        # Check if logging is already imported
        if 'import logging' in content:
            # Just add logger initialization after imports
            if 'logger = logging.getLogger(__name__)' not in content:
                # Find the end of imports
                import_end = content.find('\n\n')
                if import_end == -1:
                    import_end = 0

                # Insert logger initialization
                content = content[:import_end] + '\n\nlogger = logging.getLogger(__name__)' + content[import_end:]
                changes.append("Added logger initialization")
        else:
            # Add import logging at the top
            content = 'import logging\n\n' + content

            # Add logger initialization
            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 = []

        # Pattern 1: print(string) -> logger.info(string)
        # Pattern 2: print(f"...") -> logger.info(f"...")
        # Pattern 3: print("error:", e) -> logger.error(f"error: {e}")

        # Simple print statements
        patterns = [
            # print("message") -> logger.info("message")
            (r'print\("([^"]+)"\)', r'logger.info("\1")'),

            # print('message') -> logger.info('message')
            (r"print\('([^']+)'\)", r"logger.info('\1')"),

            # print(f"...") -> logger.info(f"...")
            (r'print\(f"([^"]+)"\)', r'logger.info(f"\1")'),
            (r"print\(f'([^']+)'\)", r"logger.info(f'\1')"),

            # print(variable) -> logger.info(str(variable))
            (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 = []

    # Find all Python files
    for py_file in base_dir.rglob('*.py'):
        # Exclude test files, __init__.py, and venv
        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()