| """ |
| 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 |
| |
| |
| 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") |
| |
| |
| if self.config.get("format_with_black", True): |
| self._run_black(python_files, dry_run) |
| |
| |
| if self.config.get("sort_imports", True): |
| self._run_isort(python_files, dry_run) |
| |
| |
| 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): |
| |
| dirs[:] = [d for d in dirs if d not in exclude_dirs] |
| |
| for file in files: |
| if file.endswith('.py'): |
| |
| 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", |
| *[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 |
|
|