Skip to content

Commit 2cc4341

Browse files
committed
Add migration tools for Ruff integration
- Add scripts/migrate_to_ruff.py: Configuration converter script that parses existing vimrc files and converts old linting tool configs to Ruff equivalents. Generates both VimScript snippets and pyproject.toml configurations. - Add scripts/validate_ruff_migration.sh: Migration validation script that verifies Ruff installation, integration files, submodule cleanup, and test execution. Provides comprehensive validation summary. These tools help users migrate from old linting tools (pylint, pyflakes, pycodestyle, etc.) to Ruff-based configuration.
1 parent a29c84b commit 2cc4341

2 files changed

Lines changed: 497 additions & 0 deletions

File tree

scripts/migrate_to_ruff.py

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
#!/usr/bin/env python3
2+
"""Configuration converter script for migrating python-mode configs to Ruff.
3+
4+
This script helps users migrate their existing python-mode configuration
5+
from the old linting tools (pylint, pyflakes, pycodestyle, etc.) to Ruff.
6+
7+
Usage:
8+
python scripts/migrate_to_ruff.py [--vimrc-file <path>] [--output <path>]
9+
"""
10+
11+
import argparse
12+
import re
13+
import sys
14+
from pathlib import Path
15+
from typing import List, Tuple, Optional
16+
17+
18+
# Mapping of old linter names to Ruff rule categories
19+
LINTER_TO_RUFF_RULES = {
20+
'pyflakes': ['F'],
21+
'pycodestyle': ['E', 'W'],
22+
'pep8': ['E', 'W'],
23+
'mccabe': ['C90'],
24+
'pylint': ['PLE', 'PLR', 'PLW'],
25+
'pydocstyle': ['D'],
26+
'pep257': ['D'],
27+
'autopep8': ['E', 'W'],
28+
}
29+
30+
31+
def find_vimrc_files() -> List[Path]:
32+
"""Find common vimrc file locations."""
33+
candidates = [
34+
Path.home() / '.vimrc',
35+
Path.home() / '.vim' / 'vimrc',
36+
Path.home() / '.config' / 'nvim' / 'init.vim',
37+
Path.home() / '.config' / 'nvim' / 'init.lua',
38+
]
39+
return [p for p in candidates if p.exists()]
40+
41+
42+
def parse_vimrc_config(file_path: Path) -> dict:
43+
"""Parse vimrc file and extract python-mode configuration."""
44+
config = {
45+
'lint_checkers': [],
46+
'lint_ignore': [],
47+
'lint_select': [],
48+
'ruff_enabled': None,
49+
'ruff_format_enabled': None,
50+
'ruff_ignore': [],
51+
'ruff_select': [],
52+
'max_line_length': None,
53+
'mccabe_complexity': None,
54+
}
55+
56+
if not file_path.exists():
57+
return config
58+
59+
content = file_path.read_text()
60+
61+
# Extract g:pymode_lint_checkers
62+
checkers_match = re.search(r'let\s+g:pymode_lint_checkers\s*=\s*\[(.*?)\]', content)
63+
if checkers_match:
64+
checkers_str = checkers_match.group(1)
65+
config['lint_checkers'] = [
66+
c.strip().strip("'\"")
67+
for c in re.findall(r"['\"]([^'\"]+)['\"]", checkers_str)
68+
]
69+
70+
# Extract g:pymode_lint_ignore
71+
ignore_match = re.search(r'let\s+g:pymode_lint_ignore\s*=\s*\[(.*?)\]', content)
72+
if ignore_match:
73+
ignore_str = ignore_match.group(1)
74+
config['lint_ignore'] = [
75+
i.strip().strip("'\"")
76+
for i in re.findall(r"['\"]([^'\"]+)['\"]", ignore_str)
77+
]
78+
79+
# Extract g:pymode_lint_select
80+
select_match = re.search(r'let\s+g:pymode_lint_select\s*=\s*\[(.*?)\]', content)
81+
if select_match:
82+
select_str = select_match.group(1)
83+
config['lint_select'] = [
84+
s.strip().strip("'\"")
85+
for s in re.findall(r"['\"]([^'\"]+)['\"]", select_str)
86+
]
87+
88+
# Extract g:pymode_ruff_enabled
89+
ruff_enabled_match = re.search(r'let\s+g:pymode_ruff_enabled\s*=\s*(\d+)', content)
90+
if ruff_enabled_match:
91+
config['ruff_enabled'] = ruff_enabled_match.group(1) == '1'
92+
93+
# Extract g:pymode_ruff_format_enabled
94+
ruff_format_match = re.search(r'let\s+g:pymode_ruff_format_enabled\s*=\s*(\d+)', content)
95+
if ruff_format_match:
96+
config['ruff_format_enabled'] = ruff_format_match.group(1) == '1'
97+
98+
# Extract g:pymode_ruff_ignore
99+
ruff_ignore_match = re.search(r'let\s+g:pymode_ruff_ignore\s*=\s*\[(.*?)\]', content)
100+
if ruff_ignore_match:
101+
ruff_ignore_str = ruff_ignore_match.group(1)
102+
config['ruff_ignore'] = [
103+
i.strip().strip("'\"")
104+
for i in re.findall(r"['\"]([^'\"]+)['\"]", ruff_ignore_str)
105+
]
106+
107+
# Extract g:pymode_ruff_select
108+
ruff_select_match = re.search(r'let\s+g:pymode_ruff_select\s*=\s*\[(.*?)\]', content)
109+
if ruff_select_match:
110+
ruff_select_str = ruff_select_match.group(1)
111+
config['ruff_select'] = [
112+
s.strip().strip("'\"")
113+
for s in re.findall(r"['\"]([^'\"]+)['\"]", ruff_select_str)
114+
]
115+
116+
# Extract g:pymode_options_max_line_length
117+
max_line_match = re.search(r'let\s+g:pymode_options_max_line_length\s*=\s*(\d+)', content)
118+
if max_line_match:
119+
config['max_line_length'] = int(max_line_match.group(1))
120+
121+
# Extract g:pymode_lint_options_mccabe_complexity
122+
mccabe_match = re.search(r'let\s+g:pymode_lint_options_mccabe_complexity\s*=\s*(\d+)', content)
123+
if mccabe_match:
124+
config['mccabe_complexity'] = int(mccabe_match.group(1))
125+
126+
return config
127+
128+
129+
def convert_to_ruff_config(old_config: dict) -> dict:
130+
"""Convert old python-mode config to Ruff-specific config."""
131+
ruff_config = {
132+
'ruff_enabled': True,
133+
'ruff_format_enabled': old_config.get('lint_checkers') and 'autopep8' in old_config['lint_checkers'],
134+
'ruff_select': [],
135+
'ruff_ignore': old_config.get('lint_ignore', []).copy(),
136+
'max_line_length': old_config.get('max_line_length'),
137+
'mccabe_complexity': old_config.get('mccabe_complexity'),
138+
}
139+
140+
# Convert lint_checkers to ruff_select rules
141+
select_rules = set()
142+
143+
# Add rules from explicit select
144+
if old_config.get('lint_select'):
145+
select_rules.update(old_config['lint_select'])
146+
147+
# Add rules from enabled linters
148+
for linter in old_config.get('lint_checkers', []):
149+
if linter in LINTER_TO_RUFF_RULES:
150+
select_rules.update(LINTER_TO_RUFF_RULES[linter])
151+
152+
# If no specific rules selected, use a sensible default
153+
if not select_rules:
154+
select_rules = {'F', 'E', 'W'} # Pyflakes + pycodestyle by default
155+
156+
ruff_config['ruff_select'] = sorted(list(select_rules))
157+
158+
# If ruff-specific config already exists, preserve it
159+
if old_config.get('ruff_enabled') is not None:
160+
ruff_config['ruff_enabled'] = old_config['ruff_enabled']
161+
if old_config.get('ruff_format_enabled') is not None:
162+
ruff_config['ruff_format_enabled'] = old_config['ruff_format_enabled']
163+
if old_config.get('ruff_ignore'):
164+
ruff_config['ruff_ignore'] = old_config['ruff_ignore']
165+
if old_config.get('ruff_select'):
166+
ruff_config['ruff_select'] = old_config['ruff_select']
167+
168+
return ruff_config
169+
170+
171+
def generate_vimrc_snippet(config: dict) -> str:
172+
"""Generate VimScript configuration snippet."""
173+
lines = [
174+
'" Ruff configuration for python-mode',
175+
'" Generated by migrate_to_ruff.py',
176+
'',
177+
]
178+
179+
if config.get('ruff_enabled'):
180+
lines.append('let g:pymode_ruff_enabled = 1')
181+
182+
if config.get('ruff_format_enabled'):
183+
lines.append('let g:pymode_ruff_format_enabled = 1')
184+
185+
if config.get('ruff_select'):
186+
select_str = ', '.join(f'"{r}"' for r in config['ruff_select'])
187+
lines.append(f'let g:pymode_ruff_select = [{select_str}]')
188+
189+
if config.get('ruff_ignore'):
190+
ignore_str = ', '.join(f'"{i}"' for i in config['ruff_ignore'])
191+
lines.append(f'let g:pymode_ruff_ignore = [{ignore_str}]')
192+
193+
if config.get('max_line_length'):
194+
lines.append(f'let g:pymode_options_max_line_length = {config["max_line_length"]}')
195+
196+
if config.get('mccabe_complexity'):
197+
lines.append(f'let g:pymode_lint_options_mccabe_complexity = {config["mccabe_complexity"]}')
198+
199+
lines.append('')
200+
return '\n'.join(lines)
201+
202+
203+
def generate_pyproject_toml(config: dict) -> str:
204+
"""Generate pyproject.toml configuration snippet."""
205+
lines = [
206+
'[tool.ruff]',
207+
]
208+
209+
if config.get('max_line_length'):
210+
lines.append(f'line-length = {config["max_line_length"]}')
211+
212+
if config.get('ruff_select'):
213+
select_str = ', '.join(f'"{r}"' for r in config['ruff_select'])
214+
lines.append(f'select = [{select_str}]')
215+
216+
if config.get('ruff_ignore'):
217+
ignore_str = ', '.join(f'"{i}"' for i in config['ruff_ignore'])
218+
lines.append(f'ignore = [{ignore_str}]')
219+
220+
if config.get('mccabe_complexity'):
221+
lines.append('')
222+
lines.append('[tool.ruff.lint.mccabe]')
223+
lines.append(f'max-complexity = {config["mccabe_complexity"]}')
224+
225+
lines.append('')
226+
return '\n'.join(lines)
227+
228+
229+
def main():
230+
parser = argparse.ArgumentParser(
231+
description='Convert python-mode configuration to Ruff',
232+
formatter_class=argparse.RawDescriptionHelpFormatter,
233+
epilog="""
234+
Examples:
235+
# Analyze default vimrc file
236+
python scripts/migrate_to_ruff.py
237+
238+
# Analyze specific vimrc file
239+
python scripts/migrate_to_ruff.py --vimrc-file ~/.vimrc
240+
241+
# Generate migration output to file
242+
python scripts/migrate_to_ruff.py --output migration.txt
243+
"""
244+
)
245+
parser.add_argument(
246+
'--vimrc-file',
247+
type=Path,
248+
help='Path to vimrc file (default: auto-detect)'
249+
)
250+
parser.add_argument(
251+
'--output',
252+
type=Path,
253+
help='Output file for migration suggestions (default: stdout)'
254+
)
255+
parser.add_argument(
256+
'--format',
257+
choices=['vimrc', 'pyproject', 'both'],
258+
default='both',
259+
help='Output format (default: both)'
260+
)
261+
262+
args = parser.parse_args()
263+
264+
# Find vimrc file
265+
if args.vimrc_file:
266+
vimrc_path = args.vimrc_file
267+
if not vimrc_path.exists():
268+
print(f"Error: File not found: {vimrc_path}", file=sys.stderr)
269+
sys.exit(1)
270+
else:
271+
vimrc_files = find_vimrc_files()
272+
if not vimrc_files:
273+
print("Error: Could not find vimrc file. Please specify with --vimrc-file", file=sys.stderr)
274+
sys.exit(1)
275+
vimrc_path = vimrc_files[0]
276+
print(f"Found vimrc file: {vimrc_path}", file=sys.stderr)
277+
278+
# Parse configuration
279+
old_config = parse_vimrc_config(vimrc_path)
280+
281+
# Check if already using Ruff
282+
if old_config.get('ruff_enabled'):
283+
print("Note: Ruff is already enabled in your configuration.", file=sys.stderr)
284+
285+
# Convert to Ruff config
286+
ruff_config = convert_to_ruff_config(old_config)
287+
288+
# Generate output
289+
output_lines = [
290+
f"# Migration suggestions for {vimrc_path}",
291+
"#",
292+
"# Old configuration detected:",
293+
f"# lint_checkers: {old_config.get('lint_checkers', [])}",
294+
f"# lint_ignore: {old_config.get('lint_ignore', [])}",
295+
f"# lint_select: {old_config.get('lint_select', [])}",
296+
"#",
297+
"# Recommended Ruff configuration:",
298+
"",
299+
]
300+
301+
if args.format in ('vimrc', 'both'):
302+
output_lines.append("## VimScript Configuration (.vimrc)")
303+
output_lines.append("")
304+
output_lines.append(generate_vimrc_snippet(ruff_config))
305+
306+
if args.format in ('pyproject', 'both'):
307+
output_lines.append("## pyproject.toml Configuration")
308+
output_lines.append("")
309+
output_lines.append("Add this to your pyproject.toml:")
310+
output_lines.append("")
311+
output_lines.append(generate_pyproject_toml(ruff_config))
312+
313+
output_text = '\n'.join(output_lines)
314+
315+
# Write output
316+
if args.output:
317+
args.output.write_text(output_text)
318+
print(f"Migration suggestions written to: {args.output}", file=sys.stderr)
319+
else:
320+
print(output_text)
321+
322+
323+
if __name__ == '__main__':
324+
main()
325+

0 commit comments

Comments
 (0)