#!/usr/bin/env python3 """ Fenrir PEP8 Validation and Auto-Fix Tool Validates Python code style using flake8 and applies safe automatic fixes. Designed to work with Fenrir's existing codebase while respecting timing-critical code. Usage: python3 tools/validate_pep8.py # Check all Python files python3 tools/validate_pep8.py --fix-safe # Auto-fix safe issues python3 tools/validate_pep8.py --check-only # Exit with error if issues found python3 tools/validate_pep8.py --staged # Check only staged files """ import os import sys import argparse import subprocess import tempfile from pathlib import Path class PEP8Validator: def __init__(self, verbose=True): self.verbose = verbose self.errors = [] self.warnings = [] self.fixes_applied = [] 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 check_flake8_available(self): """Check if flake8 is available.""" try: result = subprocess.run(["flake8", "--version"], capture_output=True, text=True, timeout=5) if result.returncode == 0: version = result.stdout.strip().split('\n')[0] self.log(f"Using flake8: {version}") return True else: return False except (subprocess.TimeoutExpired, FileNotFoundError): return False def get_python_files(self, directory=None, staged_only=False): """Get list of Python files to check.""" if staged_only: try: result = subprocess.run([ "git", "diff", "--cached", "--name-only", "--diff-filter=ACM" ], capture_output=True, text=True, timeout=10) if result.returncode == 0: files = [f for f in result.stdout.strip().split('\n') if f.endswith('.py') and Path(f).exists()] return [Path(f) for f in files if f] else: self.warnings.append("Could not get staged files, checking all files") staged_only = False except subprocess.TimeoutExpired: self.warnings.append("Git command timed out, checking all files") staged_only = False if not staged_only: directory = Path(directory or "src/fenrirscreenreader") if not directory.exists(): self.errors.append(f"Directory {directory} does not exist") return [] python_files = list(directory.rglob("*.py")) # Filter out cache and build directories python_files = [f for f in python_files if not any( part.startswith(('__pycache__', '.git', 'build', 'dist')) for part in f.parts)] return python_files return [] def run_flake8(self, files, select=None, ignore=None): """Run flake8 on the given files.""" if not files: return True, "" cmd = ["flake8"] if select: cmd.extend(["--select", select]) if ignore: cmd.extend(["--ignore", ignore]) # Add files cmd.extend([str(f) for f in files]) try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) return result.returncode == 0, result.stdout except subprocess.TimeoutExpired: self.errors.append("flake8 command timed out") return False, "" except Exception as e: self.errors.append(f"Failed to run flake8: {e}") return False, "" def categorize_issues(self, flake8_output): """Categorize flake8 issues by severity and safety for auto-fixing.""" lines = flake8_output.strip().split('\n') issues = {'critical': [], 'safe_fixable': [], 'manual': []} for line in lines: if not line.strip(): continue # Parse flake8 output: filename:line:col: code message parts = line.split(':', 3) if len(parts) < 4: continue filename = parts[0] line_num = parts[1] col = parts[2] code_msg = parts[3].strip() code = code_msg.split()[0] if code_msg else "" # Categorize by error code if code.startswith('E9') or code.startswith('F'): # Critical syntax/import errors issues['critical'].append(line) elif code in ['E111', 'E114', 'E117', 'E121', 'E122', 'E123', 'E124', 'E125', 'E126', 'E127', 'E128', 'E129', 'E131', 'E133', 'W291', 'W292', 'W293']: # Safe indentation and whitespace fixes # But skip timing-critical files if not any(critical in filename.lower() for critical in ['evdevdriver', 'vcsadriver', 'screenmanager', 'inputmanager']): issues['safe_fixable'].append(line) else: issues['manual'].append(line) else: # Everything else needs manual review issues['manual'].append(line) return issues def apply_safe_fixes(self, files): """Apply safe automatic fixes using autopep8.""" try: # Check if autopep8 is available result = subprocess.run(["autopep8", "--version"], capture_output=True, text=True, timeout=5) if result.returncode != 0: self.warnings.append("autopep8 not available for auto-fixing") return False except (subprocess.TimeoutExpired, FileNotFoundError): self.warnings.append("autopep8 not available for auto-fixing") return False fixed_count = 0 for file_path in files: # Skip timing-critical files if any(critical in str(file_path).lower() for critical in ['evdevdriver', 'vcsadriver', 'screenmanager', 'inputmanager']): self.log(f"Skipping timing-critical file: {file_path}", "WARNING") continue try: # Apply safe fixes only cmd = [ "autopep8", "--in-place", "--select", "E111,E114,E117,E121,E122,E123,E124,E125,E126,E127,E128,E129,E131,E133,W291,W292,W293", str(file_path) ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) if result.returncode == 0: self.fixes_applied.append(f"Applied safe PEP8 fixes to {file_path}") fixed_count += 1 else: self.warnings.append(f"Could not auto-fix {file_path}: {result.stderr}") except subprocess.TimeoutExpired: self.warnings.append(f"Auto-fix timed out for {file_path}") except Exception as e: self.warnings.append(f"Error auto-fixing {file_path}: {e}") return fixed_count > 0 def validate_files(self, files, fix_safe=False): """Validate Python files for PEP8 compliance.""" if not files: self.log("No Python files to validate") return True self.log(f"Validating {len(files)} Python files") # Run comprehensive flake8 check success, output = self.run_flake8(files) if not output.strip(): self.log("All files pass PEP8 validation", "SUCCESS") return True # Categorize issues issues = self.categorize_issues(output) # Report critical issues if issues['critical']: self.log(f"Critical issues found ({len(issues['critical'])}):", "ERROR") for issue in issues['critical'][:10]: # Limit output self.log(f" {issue}", "ERROR") if len(issues['critical']) > 10: self.log(f" ... and {len(issues['critical']) - 10} more", "ERROR") # Handle safe fixable issues if issues['safe_fixable']: if fix_safe: self.log(f"Auto-fixing {len(issues['safe_fixable'])} safe issues...", "INFO") # Get unique files from safe_fixable issues fix_files = set() for issue in issues['safe_fixable']: filename = issue.split(':')[0] fix_files.add(Path(filename)) if self.apply_safe_fixes(fix_files): self.log("Safe auto-fixes applied", "SUCCESS") # Re-run flake8 to see remaining issues success, output = self.run_flake8(files) if output.strip(): remaining_issues = self.categorize_issues(output) issues = remaining_issues else: issues = {'critical': [], 'safe_fixable': [], 'manual': []} else: self.log(f"Safe fixable issues found ({len(issues['safe_fixable'])}):", "WARNING") for issue in issues['safe_fixable'][:5]: self.log(f" {issue}", "WARNING") if len(issues['safe_fixable']) > 5: self.log(f" ... and {len(issues['safe_fixable']) - 5} more", "WARNING") self.log("Run with --fix-safe to auto-fix these", "INFO") # Report manual issues if issues['manual']: self.log(f"Manual review needed ({len(issues['manual'])}):", "WARNING") for issue in issues['manual'][:5]: self.log(f" {issue}", "WARNING") if len(issues['manual']) > 5: self.log(f" ... and {len(issues['manual']) - 5} more", "WARNING") # Return success if only manual issues remain (non-critical) return len(issues['critical']) == 0 def generate_report(self): """Generate final validation report.""" total_issues = len(self.errors) + len(self.warnings) if self.fixes_applied: self.log(f"\nAUTO-FIXES APPLIED ({len(self.fixes_applied)}):", "HEADER") for fix in self.fixes_applied: self.log(fix, "SUCCESS") if self.errors: self.log(f"\nERRORS ({len(self.errors)}):", "HEADER") for error in self.errors: self.log(error, "ERROR") if self.warnings: self.log(f"\nWARNINGS ({len(self.warnings)}):", "HEADER") for warning in self.warnings: self.log(warning, "WARNING") if len(self.errors) == 0: self.log("\n✅ PEP8 VALIDATION PASSED", "SUCCESS") if self.warnings: self.log("Non-critical style issues found - consider manual review", "INFO") return True else: self.log("\n❌ PEP8 VALIDATION FAILED", "ERROR") self.log("Critical issues must be fixed", "ERROR") return False def main(): parser = argparse.ArgumentParser(description='Validate and fix PEP8 compliance in Fenrir') parser.add_argument('--fix-safe', action='store_true', help='Apply safe automatic fixes (avoids timing-critical files)') parser.add_argument('--check-only', action='store_true', help='Exit with non-zero code if issues found') parser.add_argument('--staged', action='store_true', help='Check only staged files') parser.add_argument('--quiet', action='store_true', help='Reduce output verbosity') parser.add_argument('--directory', default='src/fenrirscreenreader', help='Directory to scan (default: src/fenrirscreenreader)') args = parser.parse_args() validator = PEP8Validator(verbose=not args.quiet) validator.log("FENRIR PEP8 VALIDATION", "HEADER") # Check if flake8 is available if not validator.check_flake8_available(): validator.log("flake8 is required but not available", "ERROR") validator.log("Install with: pip install flake8", "INFO") if args.fix_safe: validator.log("For auto-fixing, also install: pip install autopep8", "INFO") sys.exit(1) # Get files to validate files = validator.get_python_files( directory=args.directory if not args.staged else None, staged_only=args.staged ) if not files: validator.log("No Python files found to validate") sys.exit(0) # Validate files success = validator.validate_files(files, fix_safe=args.fix_safe) # Generate report validation_passed = validator.generate_report() if args.check_only and not validation_passed: sys.exit(1) elif validation_passed: sys.exit(0) else: sys.exit(1) if __name__ == '__main__': main()