File size: 8,467 Bytes
d2426db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
"""
Code Cleaner Module - Automated code formatting and cleaning.

This module handles:
- Code formatting with Black
- Import sorting with isort
- Removing unused imports with autoflake
- Fixing trailing whitespace and line endings
"""

import os
import subprocess
import logging
from pathlib import Path
from typing import List, Dict, Any

logger = logging.getLogger(__name__)


class CodeCleaner:
    """Automated code cleaning and formatting."""
    
    def __init__(self, config: Dict[str, Any], project_root: str):
        """
        Initialize the code cleaner.
        
        Args:
            config: Configuration dictionary for code cleaner
            project_root: Root directory of the project
        """
        self.config = config
        self.project_root = Path(project_root)
        self.results = {
            "formatted_files": [],
            "sorted_imports": [],
            "removed_unused": [],
            "errors": [],
            "total_files_processed": 0
        }
    
    def run(self, dry_run: bool = False) -> Dict[str, Any]:
        """
        Run all code cleaning operations.
        
        Args:
            dry_run: If True, preview changes without applying them
            
        Returns:
            Dictionary containing results of cleaning operations
        """
        logger.info("Starting code cleaning...")
        
        if not self.config.get("enabled", True):
            logger.info("Code cleaner is disabled in configuration")
            return self.results
        
        # Find all Python files
        python_files = self._find_python_files()
        self.results["total_files_processed"] = len(python_files)
        
        logger.info(f"Found {len(python_files)} Python files to process")
        
        # Run Black formatter
        if self.config.get("format_with_black", True):
            self._run_black(python_files, dry_run)
        
        # Sort imports with isort
        if self.config.get("sort_imports", True):
            self._run_isort(python_files, dry_run)
        
        # Remove unused imports
        if self.config.get("remove_unused_imports", True):
            self._run_autoflake(python_files, dry_run)
        
        logger.info("Code cleaning completed")
        return self.results
    
    def _find_python_files(self) -> List[Path]:
        """Find all Python files in the project."""
        python_files = []
        exclude_dirs = set(self.config.get("exclude_dirs", []))
        exclude_files = self.config.get("exclude_files", [])
        
        for root, dirs, files in os.walk(self.project_root):
            # Remove excluded directories from search
            dirs[:] = [d for d in dirs if d not in exclude_dirs]
            
            for file in files:
                if file.endswith('.py'):
                    # Check if file matches exclude patterns
                    if not any(file.endswith(pattern.replace('*', '')) for pattern in exclude_files):
                        python_files.append(Path(root) / file)
        
        return python_files
    
    def _run_black(self, files: List[Path], dry_run: bool):
        """Run Black formatter on Python files."""
        logger.info("Running Black formatter...")
        
        try:
            line_length = self.config.get("line_length", 100)
            cmd = ["black"]
            
            if dry_run:
                cmd.append("--check")
            
            cmd.extend([
                "--line-length", str(line_length),
                *[str(f) for f in files]
            ])
            
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                cwd=self.project_root
            )
            
            if result.returncode == 0:
                if not dry_run:
                    self.results["formatted_files"] = [str(f) for f in files]
                    logger.info(f"Successfully formatted {len(files)} files with Black")
                else:
                    logger.info("Black check passed - all files are properly formatted")
            else:
                if dry_run:
                    logger.warning("Some files would be reformatted by Black")
                    logger.debug(result.stdout)
                else:
                    logger.error(f"Black formatting failed: {result.stderr}")
                    self.results["errors"].append(f"Black error: {result.stderr}")
                    
        except FileNotFoundError:
            error_msg = "Black is not installed. Install with: pip install black"
            logger.error(error_msg)
            self.results["errors"].append(error_msg)
        except Exception as e:
            logger.error(f"Error running Black: {str(e)}")
            self.results["errors"].append(f"Black error: {str(e)}")
    
    def _run_isort(self, files: List[Path], dry_run: bool):
        """Run isort to sort imports."""
        logger.info("Running isort for import sorting...")
        
        try:
            cmd = ["isort"]
            
            if dry_run:
                cmd.append("--check-only")
            
            cmd.extend([
                "--profile", "black",  # Compatible with Black
                *[str(f) for f in files]
            ])
            
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                cwd=self.project_root
            )
            
            if result.returncode == 0:
                if not dry_run:
                    self.results["sorted_imports"] = [str(f) for f in files]
                    logger.info(f"Successfully sorted imports in {len(files)} files")
                else:
                    logger.info("isort check passed - all imports are properly sorted")
            else:
                if dry_run:
                    logger.warning("Some imports would be reordered by isort")
                    logger.debug(result.stdout)
                    
        except FileNotFoundError:
            error_msg = "isort is not installed. Install with: pip install isort"
            logger.error(error_msg)
            self.results["errors"].append(error_msg)
        except Exception as e:
            logger.error(f"Error running isort: {str(e)}")
            self.results["errors"].append(f"isort error: {str(e)}")
    
    def _run_autoflake(self, files: List[Path], dry_run: bool):
        """Run autoflake to remove unused imports."""
        logger.info("Running autoflake to remove unused imports...")
        
        try:
            cmd = ["autoflake"]
            
            if not dry_run:
                cmd.append("--in-place")
            
            cmd.extend([
                "--remove-all-unused-imports",
                "--remove-unused-variables",
                *[str(f) for f in files]
            ])
            
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                cwd=self.project_root
            )
            
            if result.returncode == 0:
                if not dry_run:
                    self.results["removed_unused"] = [str(f) for f in files]
                    logger.info(f"Successfully cleaned {len(files)} files with autoflake")
                else:
                    logger.info("autoflake check completed")
                    if result.stdout:
                        logger.debug(f"Would remove unused imports:\n{result.stdout}")
                        
        except FileNotFoundError:
            error_msg = "autoflake is not installed. Install with: pip install autoflake"
            logger.error(error_msg)
            self.results["errors"].append(error_msg)
        except Exception as e:
            logger.error(f"Error running autoflake: {str(e)}")
            self.results["errors"].append(f"autoflake error: {str(e)}")
    
    def get_summary(self) -> str:
        """Get a summary of cleaning results."""
        summary = f"""
Code Cleaning Summary:
- Total files processed: {self.results['total_files_processed']}
- Files formatted: {len(self.results['formatted_files'])}
- Files with sorted imports: {len(self.results['sorted_imports'])}
- Files with removed unused code: {len(self.results['removed_unused'])}
- Errors encountered: {len(self.results['errors'])}
"""
        return summary