lint.py•9.26 kB
#!/usr/bin/env python3
"""Script to run linters with auto-fix capabilities.
Run with: python lint.py [--autofix]
"""
import argparse
import importlib.util
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_cmd = "flake8 --exclude=.venv,venv,__pycache__,.git,build,dist,*.egg-info,node_modules"
flake8_result = run_command(flake8_cmd, 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())