#!/usr/bin/env python3 """ Fenrir Cache Cleanup Tool Removes Python cache files and directories from the repository. These files should never be committed and can cause issues. Usage: python3 tools/cleanup_cache.py # Show what would be removed python3 tools/cleanup_cache.py --remove # Actually remove cache files python3 tools/cleanup_cache.py --check # Exit with error if cache files found """ import os import sys import argparse import shutil from pathlib import Path class CacheCleanup: def __init__(self, verbose=True): self.verbose = verbose self.cache_dirs = [] self.cache_files = [] def log(self, message, level="INFO"): """Log a message with appropriate formatting.""" if not self.verbose and level == "INFO": return colors = { "INFO": "\033[0;36m", # Cyan "SUCCESS": "\033[0;32m", # Green "WARNING": "\033[1;33m", # Yellow "ERROR": "\033[0;31m", # Red "HEADER": "\033[1;34m", # Bold Blue } reset = "\033[0m" color = colors.get(level, "") if level == "HEADER": print(f"\n{color}{'='*60}") print(f"{message}") print(f"{'='*60}{reset}") else: symbol = { "SUCCESS": "✓", "ERROR": "✗", "WARNING": "⚠", "INFO": "•" }.get(level, "•") print(f"{color}{symbol} {message}{reset}") def find_cache_files(self, directory): """Find all Python cache files and directories.""" directory = Path(directory) for root, dirs, files in os.walk(directory): root_path = Path(root) # Skip .git directory entirely if '.git' in root_path.parts: continue # Find __pycache__ directories if '__pycache__' in dirs: cache_dir = root_path / '__pycache__' self.cache_dirs.append(cache_dir) # Don't traverse into __pycache__ directories dirs.remove('__pycache__') # Find .pyc files outside of __pycache__ for file in files: if file.endswith('.pyc'): cache_file = root_path / file self.cache_files.append(cache_file) def show_findings(self): """Display what cache files were found.""" total_items = len(self.cache_dirs) + len(self.cache_files) if total_items == 0: self.log("No Python cache files found", "SUCCESS") return True self.log(f"Found {total_items} cache items:", "WARNING") if self.cache_dirs: self.log(f"\n__pycache__ directories ({len(self.cache_dirs)}):", "WARNING") for cache_dir in sorted(self.cache_dirs): # Show size of directory size = self.get_directory_size(cache_dir) self.log(f" {cache_dir} ({size} files)", "WARNING") if self.cache_files: self.log(f"\nLoose .pyc files ({len(self.cache_files)}):", "WARNING") for cache_file in sorted(self.cache_files): # Show file size try: size = cache_file.stat().st_size size_str = self.format_size(size) self.log(f" {cache_file} ({size_str})", "WARNING") except OSError: self.log(f" {cache_file} (size unknown)", "WARNING") return False def get_directory_size(self, directory): """Get the number of files in a directory.""" try: return len(list(directory.rglob('*'))) except OSError: return 0 def format_size(self, size_bytes): """Format file size in human-readable format.""" if size_bytes < 1024: return f"{size_bytes} B" elif size_bytes < 1024 * 1024: return f"{size_bytes // 1024} KB" else: return f"{size_bytes // (1024 * 1024)} MB" def remove_cache_files(self): """Actually remove the cache files and directories.""" removed_count = 0 errors = [] # Remove __pycache__ directories for cache_dir in self.cache_dirs: try: if cache_dir.exists(): shutil.rmtree(cache_dir) self.log(f"Removed directory: {cache_dir}", "SUCCESS") removed_count += 1 except OSError as e: error_msg = f"Failed to remove {cache_dir}: {e}" errors.append(error_msg) self.log(error_msg, "ERROR") # Remove .pyc files for cache_file in self.cache_files: try: if cache_file.exists(): cache_file.unlink() self.log(f"Removed file: {cache_file}", "SUCCESS") removed_count += 1 except OSError as e: error_msg = f"Failed to remove {cache_file}: {e}" errors.append(error_msg) self.log(error_msg, "ERROR") if errors: self.log(f"Encountered {len(errors)} errors during cleanup", "ERROR") return False else: self.log(f"Successfully removed {removed_count} cache items", "SUCCESS") return True def check_gitignore(self): """Check if .gitignore properly excludes cache files.""" gitignore_path = Path('.gitignore') if not gitignore_path.exists(): self.log("Warning: No .gitignore file found", "WARNING") return False try: with open(gitignore_path, 'r') as f: content = f.read() has_pycache = '__pycache__' in content or '__pycache__/' in content has_pyc = '*.pyc' in content if has_pycache and has_pyc: self.log("✓ .gitignore properly excludes Python cache files", "SUCCESS") return True else: missing = [] if not has_pycache: missing.append("__pycache__/") if not has_pyc: missing.append("*.pyc") self.log(f"Warning: .gitignore missing: {', '.join(missing)}", "WARNING") return False except OSError as e: self.log(f"Could not read .gitignore: {e}", "ERROR") return False def suggest_gitignore_additions(self): """Suggest additions to .gitignore.""" self.log("\nRecommended .gitignore entries for Python:", "INFO") print(""" # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg """) def main(): parser = argparse.ArgumentParser(description='Clean Python cache files from Fenrir repository') parser.add_argument('--remove', action='store_true', help='Actually remove cache files (default is dry-run)') parser.add_argument('--check', action='store_true', help='Exit with non-zero code if cache files found') parser.add_argument('--quiet', action='store_true', help='Reduce output verbosity') parser.add_argument('--directory', default='.', help='Directory to scan (default: current directory)') args = parser.parse_args() # Ensure we're in the project root if not Path("src/fenrirscreenreader").exists(): print("Error: Must be run from Fenrir project root directory") sys.exit(1) cleanup = CacheCleanup(verbose=not args.quiet) cleanup.log("FENRIR CACHE CLEANUP", "HEADER") cleanup.log(f"Scanning directory: {Path(args.directory).absolute()}") # Find cache files cleanup.find_cache_files(args.directory) # Show what we found no_cache_found = cleanup.show_findings() if no_cache_found: # Check .gitignore anyway cleanup.check_gitignore() cleanup.log("\n✅ Repository is clean of Python cache files", "SUCCESS") sys.exit(0) # Check .gitignore gitignore_ok = cleanup.check_gitignore() if not gitignore_ok: cleanup.suggest_gitignore_additions() # Handle different modes if args.remove: cleanup.log("\n🧹 REMOVING CACHE FILES", "HEADER") success = cleanup.remove_cache_files() if success: cleanup.log("\n✅ Cache cleanup completed successfully", "SUCCESS") sys.exit(0) else: cleanup.log("\n❌ Cache cleanup completed with errors", "ERROR") sys.exit(1) elif args.check: cleanup.log("\n❌ Cache files found - validation failed", "ERROR") cleanup.log("Run with --remove to clean up cache files", "INFO") sys.exit(1) else: # Dry run mode cleanup.log("\n💡 DRY RUN MODE", "HEADER") cleanup.log("Add --remove to actually delete these files", "INFO") cleanup.log("Add --check to fail if cache files are present", "INFO") sys.exit(0) if __name__ == '__main__': main()