247 lines
9.0 KiB
Python
Executable File
247 lines
9.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Script to run linters with auto-fix capabilities.
|
|
|
|
Run with: python lint.py [--autofix]
|
|
"""
|
|
|
|
import argparse
|
|
import importlib.util
|
|
import os
|
|
import site
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def is_venv():
|
|
"""Check if running in a virtual environment."""
|
|
return (hasattr(sys, 'real_prefix') or
|
|
(hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix))
|
|
|
|
|
|
def check_dependencies():
|
|
"""Check if all required dependencies are installed."""
|
|
missing_deps = []
|
|
|
|
# Check for required modules
|
|
required_modules = ["flake8", "isort", "autopep8", "pyflakes"]
|
|
|
|
# In Python 3.12+, also check for pycodestyle as a fallback
|
|
if sys.version_info >= (3, 12):
|
|
required_modules.append("pycodestyle")
|
|
|
|
for module in required_modules:
|
|
if importlib.util.find_spec(module) is None:
|
|
missing_deps.append(module)
|
|
|
|
# Special check for autopep8 compatibility with Python 3.12+
|
|
if sys.version_info >= (3, 12) and importlib.util.find_spec("autopep8") is not None:
|
|
try:
|
|
import autopep8
|
|
|
|
# Try to access a function that would use lib2to3
|
|
# Will throw an error if lib2to3 is missing and not handled properly
|
|
autopep8_version = autopep8.__version__
|
|
print(f"Using autopep8 version: {autopep8_version}")
|
|
except ImportError as e:
|
|
if "lib2to3" in str(e):
|
|
print("WARNING: You're using Python 3.12+ where lib2to3 is no longer included.")
|
|
print("Your installed version of autopep8 may not work correctly.")
|
|
print("Consider using a version of autopep8 compatible with Python 3.12+")
|
|
print("or run this script with Python 3.11 or earlier.")
|
|
|
|
if missing_deps:
|
|
print("ERROR: Missing required dependencies:")
|
|
for dep in missing_deps:
|
|
print(f" - {dep}")
|
|
|
|
if not is_venv():
|
|
print("\nYou are using the system Python environment.")
|
|
print("It's recommended to use a virtual environment:")
|
|
print("\n1. Create a virtual environment:")
|
|
print(" python3 -m venv .venv")
|
|
print("\n2. Activate the virtual environment:")
|
|
print(" source .venv/bin/activate # On Linux/macOS")
|
|
print(" .venv\\Scripts\\activate # On Windows")
|
|
print("\n3. Install dependencies:")
|
|
print(" pip install -r requirements-dev.txt")
|
|
else:
|
|
print("\nPlease install these dependencies with:")
|
|
print(" pip install -r requirements-dev.txt")
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def run_command(cmd, cwd=None):
|
|
"""Run a shell command and return the exit code."""
|
|
try:
|
|
process = subprocess.run(cmd, shell=True, cwd=cwd)
|
|
return process.returncode
|
|
except Exception as e:
|
|
print(f"Error executing command '{cmd}': {e}")
|
|
return 1
|
|
|
|
|
|
def fix_unused_imports(root_dir):
|
|
"""Fix unused imports using pyflakes and autoflake."""
|
|
try:
|
|
if importlib.util.find_spec("autoflake") is not None:
|
|
print("Running autoflake to remove unused imports...")
|
|
cmd = "autoflake --remove-all-unused-imports --recursive --in-place penpot_mcp/ tests/"
|
|
return run_command(cmd, cwd=root_dir)
|
|
else:
|
|
print("autoflake not found. To automatically remove unused imports, install:")
|
|
print(" pip install autoflake")
|
|
return 0
|
|
except Exception as e:
|
|
print(f"Error with autoflake: {e}")
|
|
return 0
|
|
|
|
|
|
def fix_whitespace_and_docstring_issues(root_dir):
|
|
"""Attempt to fix whitespace and simple docstring issues."""
|
|
# Find Python files that need fixing
|
|
try:
|
|
filelist_cmd = "find penpot_mcp tests setup.py -name '*.py' -type f"
|
|
process = subprocess.run(
|
|
filelist_cmd, shell=True, cwd=root_dir,
|
|
capture_output=True, text=True
|
|
)
|
|
|
|
if process.returncode != 0:
|
|
print("Error finding Python files")
|
|
return 1
|
|
|
|
files = process.stdout.strip().split('\n')
|
|
fixed_count = 0
|
|
|
|
for file_path in files:
|
|
if not file_path:
|
|
continue
|
|
|
|
full_path = Path(root_dir) / file_path
|
|
|
|
try:
|
|
with open(full_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
# Fix trailing whitespace
|
|
fixed_content = '\n'.join(line.rstrip() for line in content.split('\n'))
|
|
|
|
# Ensure final newline
|
|
if not fixed_content.endswith('\n'):
|
|
fixed_content += '\n'
|
|
|
|
# Add basic docstrings to empty modules, classes, functions
|
|
if '__init__.py' in file_path and '"""' not in fixed_content:
|
|
package_name = file_path.split('/')[-2]
|
|
fixed_content = f'"""Package {package_name}."""\n' + fixed_content
|
|
|
|
# Write back if changes were made
|
|
if fixed_content != content:
|
|
with open(full_path, 'w', encoding='utf-8') as f:
|
|
f.write(fixed_content)
|
|
fixed_count += 1
|
|
|
|
except Exception as e:
|
|
print(f"Error processing {file_path}: {e}")
|
|
|
|
if fixed_count > 0:
|
|
print(f"Fixed whitespace and newlines in {fixed_count} files")
|
|
|
|
return 0
|
|
except Exception as e:
|
|
print(f"Error in whitespace fixing: {e}")
|
|
return 0
|
|
|
|
|
|
def main():
|
|
"""Main entry point for the linter script."""
|
|
parser = argparse.ArgumentParser(description="Run linters with optional auto-fix")
|
|
parser.add_argument(
|
|
"--autofix", "-a", action="store_true", help="Automatically fix linting issues"
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
# Verify dependencies before proceeding
|
|
if not check_dependencies():
|
|
return 1
|
|
|
|
root_dir = Path(__file__).parent.absolute()
|
|
|
|
print("Running linters...")
|
|
|
|
# Run isort
|
|
isort_cmd = "isort --profile black ."
|
|
if args.autofix:
|
|
print("Running isort with auto-fix...")
|
|
exit_code = run_command(isort_cmd, cwd=root_dir)
|
|
else:
|
|
print("Checking imports with isort...")
|
|
exit_code = run_command(f"{isort_cmd} --check", cwd=root_dir)
|
|
|
|
if exit_code != 0 and not args.autofix:
|
|
print("isort found issues. Run with --autofix to fix automatically.")
|
|
|
|
# Run additional fixers when in autofix mode
|
|
if args.autofix:
|
|
# Fix unused imports
|
|
fix_unused_imports(root_dir)
|
|
|
|
# Fix whitespace and newline issues
|
|
fix_whitespace_and_docstring_issues(root_dir)
|
|
|
|
# Run autopep8
|
|
print("Running autopep8 with auto-fix...")
|
|
|
|
if sys.version_info >= (3, 12):
|
|
print("Detected Python 3.12+. Using compatible code formatting approach...")
|
|
# Use a more compatible approach for Python 3.12+
|
|
# First try autopep8 (newer versions may have fixed lib2to3 dependency)
|
|
autopep8_cmd = "autopep8 --recursive --aggressive --aggressive --in-place --select E,W penpot_mcp/ tests/ setup.py"
|
|
try:
|
|
exit_code = run_command(autopep8_cmd, cwd=root_dir)
|
|
if exit_code != 0:
|
|
print("Warning: autopep8 encountered issues. Some files may not have been fixed.")
|
|
except Exception as e:
|
|
if "lib2to3" in str(e):
|
|
print("Error with autopep8 due to missing lib2to3 module in Python 3.12+")
|
|
print("Using pycodestyle for checking only (no auto-fix is possible)")
|
|
exit_code = run_command("pycodestyle penpot_mcp/ tests/", cwd=root_dir)
|
|
else:
|
|
raise
|
|
else:
|
|
# Normal execution for Python < 3.12
|
|
autopep8_cmd = "autopep8 --recursive --aggressive --aggressive --in-place --select E,W penpot_mcp/ tests/ setup.py"
|
|
exit_code = run_command(autopep8_cmd, cwd=root_dir)
|
|
if exit_code != 0:
|
|
print("Warning: autopep8 encountered issues. Some files may not have been fixed.")
|
|
|
|
# Run flake8 (check only, no auto-fix)
|
|
print("Running flake8...")
|
|
flake8_result = run_command("flake8", cwd=root_dir)
|
|
|
|
if flake8_result != 0:
|
|
print("flake8 found issues that need to be fixed manually.")
|
|
print("Common issues and how to fix them:")
|
|
print("- F401 (unused import): Remove the import or use it")
|
|
print("- D1XX (missing docstring): Add a docstring to the module/class/function")
|
|
print("- E501 (line too long): Break the line or use line continuation")
|
|
print("- F841 (unused variable): Remove or use the variable")
|
|
|
|
if args.autofix:
|
|
print("Auto-fix completed! Run flake8 again to see if there are any remaining issues.")
|
|
elif exit_code != 0 or flake8_result != 0:
|
|
print("Linting issues found. Run with --autofix to fix automatically where possible.")
|
|
return 1
|
|
else:
|
|
print("All linting checks passed!")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|