Some syntax errors fixed. Syntax checking added. Release checklist created.
This commit is contained in:
288
tools/cleanup_cache.py
Executable file
288
tools/cleanup_cache.py
Executable file
@ -0,0 +1,288 @@
|
||||
#!/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()
|
Reference in New Issue
Block a user