#!/usr/bin/env python3
"""Configuration converter script for migrating python-mode configs to Ruff.
This script helps users migrate their existing python-mode configuration
from the old linting tools (pylint, pyflakes, pycodestyle, etc.) to Ruff.
Usage:
python scripts/migrate_to_ruff.py [--vimrc-file ] [--output ]
"""
import argparse
import re
import sys
from pathlib import Path
from typing import List, Tuple, Optional
# Mapping of old linter names to Ruff rule categories
LINTER_TO_RUFF_RULES = {
'pyflakes': ['F'],
'pycodestyle': ['E', 'W'],
'pep8': ['E', 'W'],
'mccabe': ['C90'],
'pylint': ['PLE', 'PLR', 'PLW'],
'pydocstyle': ['D'],
'pep257': ['D'],
'autopep8': ['E', 'W'],
}
def find_vimrc_files() -> List[Path]:
"""Find common vimrc file locations."""
candidates = [
Path.home() / '.vimrc',
Path.home() / '.vim' / 'vimrc',
Path.home() / '.config' / 'nvim' / 'init.vim',
Path.home() / '.config' / 'nvim' / 'init.lua',
]
return [p for p in candidates if p.exists()]
def parse_vimrc_config(file_path: Path) -> dict:
"""Parse vimrc file and extract python-mode configuration."""
config = {
'lint_checkers': [],
'lint_ignore': [],
'lint_select': [],
'ruff_enabled': None,
'ruff_format_enabled': None,
'ruff_ignore': [],
'ruff_select': [],
'max_line_length': None,
'mccabe_complexity': None,
}
if not file_path.exists():
return config
content = file_path.read_text()
# Extract g:pymode_lint_checkers
checkers_match = re.search(r'let\s+g:pymode_lint_checkers\s*=\s*\[(.*?)\]', content)
if checkers_match:
checkers_str = checkers_match.group(1)
config['lint_checkers'] = [
c.strip().strip("'\"")
for c in re.findall(r"['\"]([^'\"]+)['\"]", checkers_str)
]
# Extract g:pymode_lint_ignore
ignore_match = re.search(r'let\s+g:pymode_lint_ignore\s*=\s*\[(.*?)\]', content)
if ignore_match:
ignore_str = ignore_match.group(1)
config['lint_ignore'] = [
i.strip().strip("'\"")
for i in re.findall(r"['\"]([^'\"]+)['\"]", ignore_str)
]
# Extract g:pymode_lint_select
select_match = re.search(r'let\s+g:pymode_lint_select\s*=\s*\[(.*?)\]', content)
if select_match:
select_str = select_match.group(1)
config['lint_select'] = [
s.strip().strip("'\"")
for s in re.findall(r"['\"]([^'\"]+)['\"]", select_str)
]
# Extract g:pymode_ruff_enabled
ruff_enabled_match = re.search(r'let\s+g:pymode_ruff_enabled\s*=\s*(\d+)', content)
if ruff_enabled_match:
config['ruff_enabled'] = ruff_enabled_match.group(1) == '1'
# Extract g:pymode_ruff_format_enabled
ruff_format_match = re.search(r'let\s+g:pymode_ruff_format_enabled\s*=\s*(\d+)', content)
if ruff_format_match:
config['ruff_format_enabled'] = ruff_format_match.group(1) == '1'
# Extract g:pymode_ruff_ignore
ruff_ignore_match = re.search(r'let\s+g:pymode_ruff_ignore\s*=\s*\[(.*?)\]', content)
if ruff_ignore_match:
ruff_ignore_str = ruff_ignore_match.group(1)
config['ruff_ignore'] = [
i.strip().strip("'\"")
for i in re.findall(r"['\"]([^'\"]+)['\"]", ruff_ignore_str)
]
# Extract g:pymode_ruff_select
ruff_select_match = re.search(r'let\s+g:pymode_ruff_select\s*=\s*\[(.*?)\]', content)
if ruff_select_match:
ruff_select_str = ruff_select_match.group(1)
config['ruff_select'] = [
s.strip().strip("'\"")
for s in re.findall(r"['\"]([^'\"]+)['\"]", ruff_select_str)
]
# Extract g:pymode_options_max_line_length
max_line_match = re.search(r'let\s+g:pymode_options_max_line_length\s*=\s*(\d+)', content)
if max_line_match:
config['max_line_length'] = int(max_line_match.group(1))
# Extract g:pymode_lint_options_mccabe_complexity
mccabe_match = re.search(r'let\s+g:pymode_lint_options_mccabe_complexity\s*=\s*(\d+)', content)
if mccabe_match:
config['mccabe_complexity'] = int(mccabe_match.group(1))
return config
def convert_to_ruff_config(old_config: dict) -> dict:
"""Convert old python-mode config to Ruff-specific config."""
ruff_config = {
'ruff_enabled': True,
'ruff_format_enabled': old_config.get('lint_checkers') and 'autopep8' in old_config['lint_checkers'],
'ruff_select': [],
'ruff_ignore': old_config.get('lint_ignore', []).copy(),
'max_line_length': old_config.get('max_line_length'),
'mccabe_complexity': old_config.get('mccabe_complexity'),
}
# Convert lint_checkers to ruff_select rules
select_rules = set()
# Add rules from explicit select
if old_config.get('lint_select'):
select_rules.update(old_config['lint_select'])
# Add rules from enabled linters
for linter in old_config.get('lint_checkers', []):
if linter in LINTER_TO_RUFF_RULES:
select_rules.update(LINTER_TO_RUFF_RULES[linter])
# If no specific rules selected, use a sensible default
if not select_rules:
select_rules = {'F', 'E', 'W'} # Pyflakes + pycodestyle by default
ruff_config['ruff_select'] = sorted(list(select_rules))
# If ruff-specific config already exists, preserve it
if old_config.get('ruff_enabled') is not None:
ruff_config['ruff_enabled'] = old_config['ruff_enabled']
if old_config.get('ruff_format_enabled') is not None:
ruff_config['ruff_format_enabled'] = old_config['ruff_format_enabled']
if old_config.get('ruff_ignore'):
ruff_config['ruff_ignore'] = old_config['ruff_ignore']
if old_config.get('ruff_select'):
ruff_config['ruff_select'] = old_config['ruff_select']
return ruff_config
def generate_vimrc_snippet(config: dict) -> str:
"""Generate VimScript configuration snippet."""
lines = [
'" Ruff configuration for python-mode',
'" Generated by migrate_to_ruff.py',
'',
]
if config.get('ruff_enabled'):
lines.append('let g:pymode_ruff_enabled = 1')
if config.get('ruff_format_enabled'):
lines.append('let g:pymode_ruff_format_enabled = 1')
if config.get('ruff_select'):
select_str = ', '.join(f'"{r}"' for r in config['ruff_select'])
lines.append(f'let g:pymode_ruff_select = [{select_str}]')
if config.get('ruff_ignore'):
ignore_str = ', '.join(f'"{i}"' for i in config['ruff_ignore'])
lines.append(f'let g:pymode_ruff_ignore = [{ignore_str}]')
if config.get('max_line_length'):
lines.append(f'let g:pymode_options_max_line_length = {config["max_line_length"]}')
if config.get('mccabe_complexity'):
lines.append(f'let g:pymode_lint_options_mccabe_complexity = {config["mccabe_complexity"]}')
lines.append('')
return '\n'.join(lines)
def generate_pyproject_toml(config: dict) -> str:
"""Generate pyproject.toml configuration snippet."""
lines = [
'[tool.ruff]',
]
if config.get('max_line_length'):
lines.append(f'line-length = {config["max_line_length"]}')
if config.get('ruff_select'):
select_str = ', '.join(f'"{r}"' for r in config['ruff_select'])
lines.append(f'select = [{select_str}]')
if config.get('ruff_ignore'):
ignore_str = ', '.join(f'"{i}"' for i in config['ruff_ignore'])
lines.append(f'ignore = [{ignore_str}]')
if config.get('mccabe_complexity'):
lines.append('')
lines.append('[tool.ruff.lint.mccabe]')
lines.append(f'max-complexity = {config["mccabe_complexity"]}')
lines.append('')
return '\n'.join(lines)
def main():
parser = argparse.ArgumentParser(
description='Convert python-mode configuration to Ruff',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Analyze default vimrc file
python scripts/migrate_to_ruff.py
# Analyze specific vimrc file
python scripts/migrate_to_ruff.py --vimrc-file ~/.vimrc
# Generate migration output to file
python scripts/migrate_to_ruff.py --output migration.txt
"""
)
parser.add_argument(
'--vimrc-file',
type=Path,
help='Path to vimrc file (default: auto-detect)'
)
parser.add_argument(
'--output',
type=Path,
help='Output file for migration suggestions (default: stdout)'
)
parser.add_argument(
'--format',
choices=['vimrc', 'pyproject', 'both'],
default='both',
help='Output format (default: both)'
)
args = parser.parse_args()
# Find vimrc file
if args.vimrc_file:
vimrc_path = args.vimrc_file
if not vimrc_path.exists():
print(f"Error: File not found: {vimrc_path}", file=sys.stderr)
sys.exit(1)
else:
vimrc_files = find_vimrc_files()
if not vimrc_files:
print("Error: Could not find vimrc file. Please specify with --vimrc-file", file=sys.stderr)
sys.exit(1)
vimrc_path = vimrc_files[0]
print(f"Found vimrc file: {vimrc_path}", file=sys.stderr)
# Parse configuration
old_config = parse_vimrc_config(vimrc_path)
# Check if already using Ruff
if old_config.get('ruff_enabled'):
print("Note: Ruff is already enabled in your configuration.", file=sys.stderr)
# Convert to Ruff config
ruff_config = convert_to_ruff_config(old_config)
# Generate output
output_lines = [
f"# Migration suggestions for {vimrc_path}",
"#",
"# Old configuration detected:",
f"# lint_checkers: {old_config.get('lint_checkers', [])}",
f"# lint_ignore: {old_config.get('lint_ignore', [])}",
f"# lint_select: {old_config.get('lint_select', [])}",
"#",
"# Recommended Ruff configuration:",
"",
]
if args.format in ('vimrc', 'both'):
output_lines.append("## VimScript Configuration (.vimrc)")
output_lines.append("")
output_lines.append(generate_vimrc_snippet(ruff_config))
if args.format in ('pyproject', 'both'):
output_lines.append("## pyproject.toml Configuration")
output_lines.append("")
output_lines.append("Add this to your pyproject.toml:")
output_lines.append("")
output_lines.append(generate_pyproject_toml(ruff_config))
output_text = '\n'.join(output_lines)
# Write output
if args.output:
args.output.write_text(output_text)
print(f"Migration suggestions written to: {args.output}", file=sys.stderr)
else:
print(output_text)
if __name__ == '__main__':
main()