Skip to content

Commit 8645656

Browse files
committed
Add support for flexible Ruff configuration modes
- Introduced `g:pymode_ruff_config_mode` to control how Ruff configuration is resolved. - Added three modes: - `"local"`: Uses only local project config files, ignoring pymode settings. - `"local_override"`: Local config takes priority, falling back to pymode settings if none exist (default). - `"global"`: Uses only pymode settings, ignoring local configs. - Updated documentation to reflect new configuration options and their usage. - Enhanced tests to verify behavior across different configuration modes.
1 parent 7da85fe commit 8645656

File tree

6 files changed

+406
-25
lines changed

6 files changed

+406
-25
lines changed

doc/MIGRATION_GUIDE.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,29 @@ ignore = ["E501"]
9595

9696
Python-mode will automatically use these files if they exist in your project root.
9797

98+
### Configuration Precedence
99+
100+
Python-mode now supports flexible configuration precedence via `g:pymode_ruff_config_mode`:
101+
102+
**Default Behavior (`"local_override"`):**
103+
- If your project has a local `ruff.toml` or `pyproject.toml` with `[tool.ruff]` section, it will be used
104+
- If no local config exists, python-mode settings serve as fallback
105+
- This ensures project-specific configs are respected while providing defaults
106+
107+
**Using Only Local Config (`"local"`):**
108+
```vim
109+
let g:pymode_ruff_config_mode = "local"
110+
```
111+
Use this when you want python-mode to completely respect your project's Ruff configuration and ignore all python-mode settings.
112+
113+
**Using Only Global Config (`"global"`):**
114+
```vim
115+
let g:pymode_ruff_config_mode = "global"
116+
```
117+
Use this to restore the previous behavior where python-mode settings always override local configs. Local config files will be ignored.
118+
119+
**Note:** The default `"local_override"` mode is recommended for most users as it respects project standards while providing sensible defaults.
120+
98121
## Step-by-Step Migration
99122

100123
### Step 1: Verify Ruff Installation

doc/RUFF_CONFIGURATION_MAPPING.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,30 @@ let g:pymode_ruff_config_file = '/path/to/pyproject.toml'
116116
" Use specific Ruff configuration file
117117
```
118118

119+
#### `g:pymode_ruff_config_mode`
120+
**Default:** `"local_override"`
121+
122+
Controls how Ruff configuration is resolved. This option determines whether local project configuration files (`ruff.toml`, `pyproject.toml`) or python-mode settings take precedence.
123+
124+
**Modes:**
125+
- `"local"`: Use only the project's local Ruff config. Python-mode settings are ignored. Ruff will auto-discover configuration files in the project hierarchy.
126+
- `"local_override"`: Local config takes priority. If a local Ruff config file exists, it will be used. If no local config exists, python-mode settings serve as fallback.
127+
- `"global"`: Use only python-mode settings. Local config files are ignored (uses `--isolated` flag). This restores the previous behavior where python-mode settings always override local configs.
128+
129+
**Example:**
130+
```vim
131+
" Respect project's local Ruff config (recommended for team projects)
132+
let g:pymode_ruff_config_mode = "local"
133+
134+
" Use local config if available, otherwise use pymode defaults (default)
135+
let g:pymode_ruff_config_mode = "local_override"
136+
137+
" Always use pymode settings, ignore project configs
138+
let g:pymode_ruff_config_mode = "global"
139+
```
140+
141+
**Note:** The default `"local_override"` mode provides the best user experience by respecting project-specific configurations while providing sensible defaults when no local config exists.
142+
119143
## Migration Examples
120144

121145
### Example 1: Basic Configuration

doc/pymode.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,33 @@ If empty, Ruff will look for pyproject.toml or ruff.toml automatically.
409409
>
410410
let g:pymode_ruff_config_file = ""
411411
412+
Ruff configuration mode *'g:pymode_ruff_config_mode'*
413+
Controls how Ruff configuration is resolved. Determines whether local project
414+
configuration files (ruff.toml, pyproject.toml) or python-mode settings take
415+
precedence.
416+
417+
Modes:
418+
"local" Use only project's local Ruff config. Python-mode settings
419+
are ignored. Ruff will auto-discover configuration files in
420+
the project hierarchy.
421+
422+
"local_override" Local config takes priority. If a local Ruff config file
423+
exists, it will be used. If no local config exists,
424+
python-mode settings serve as fallback. (default)
425+
426+
"global" Use only python-mode settings. Local config files are
427+
ignored (uses --isolated flag). This restores the previous
428+
behavior where python-mode settings always override local
429+
configs.
430+
431+
Default: "local_override"
432+
>
433+
let g:pymode_ruff_config_mode = "local_override"
434+
" Respect project's local Ruff config (recommended for team projects)
435+
let g:pymode_ruff_config_mode = "local"
436+
" Always use pymode settings, ignore project configs
437+
let g:pymode_ruff_config_mode = "global"
438+
412439
For more information about Ruff rules and configuration, see:
413440
https://docs.astral.sh/ruff/rules/
414441

plugin/pymode.vim

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ call pymode#default("g:pymode_ruff_ignore", [])
154154
" If empty, Ruff will use default configuration or search for config files
155155
call pymode#default("g:pymode_ruff_config_file", "")
156156

157+
" Ruff configuration mode: 'local', 'local_override', or 'global'
158+
" 'local': Use only project's local Ruff config. Pymode settings are ignored.
159+
" 'local_override': Local config takes priority. Pymode settings serve as fallback when no local config exists.
160+
" 'global': Use only pymode settings. Local config files are ignored (uses --isolated).
161+
call pymode#default("g:pymode_ruff_config_mode", "local_override")
162+
157163
" }}}
158164

159165
" Auto open cwindow if any errors has been finded

pymode/ruff_integration.py

Lines changed: 173 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,55 @@ def _get_ruff_executable() -> str:
5454
raise RuntimeError("Ruff executable not found")
5555

5656

57+
def _find_local_ruff_config(file_path: str) -> Optional[str]:
58+
"""Find local Ruff configuration file starting from file's directory.
59+
60+
Ruff searches for config files in this order (highest priority first):
61+
1. .ruff.toml
62+
2. ruff.toml
63+
3. pyproject.toml (with [tool.ruff] section)
64+
65+
Args:
66+
file_path: Path to the Python file being checked
67+
68+
Returns:
69+
Path to the first Ruff config file found, or None if none found
70+
"""
71+
# Start from the file's directory
72+
current_dir = os.path.dirname(os.path.abspath(file_path))
73+
74+
# Config file names in priority order
75+
config_files = ['.ruff.toml', 'ruff.toml', 'pyproject.toml']
76+
77+
# Walk up the directory tree
78+
while True:
79+
# Check for config files in current directory
80+
for config_file in config_files:
81+
config_path = os.path.join(current_dir, config_file)
82+
if os.path.exists(config_path):
83+
# For pyproject.toml, check if it contains [tool.ruff] section
84+
if config_file == 'pyproject.toml':
85+
try:
86+
with open(config_path, 'r', encoding='utf-8') as f:
87+
content = f.read()
88+
if '[tool.ruff]' in content:
89+
return config_path
90+
except (IOError, UnicodeDecodeError):
91+
# If we can't read it, let Ruff handle it
92+
pass
93+
else:
94+
return config_path
95+
96+
# Move to parent directory
97+
parent_dir = os.path.dirname(current_dir)
98+
if parent_dir == current_dir:
99+
# Reached root directory
100+
break
101+
current_dir = parent_dir
102+
103+
return None
104+
105+
57106
def _build_ruff_config(linters: List[str], ignore: List[str], select: List[str]) -> Dict[str, Any]:
58107
"""Build ruff configuration from pymode settings."""
59108
config = {}
@@ -223,31 +272,97 @@ def run_ruff_check(file_path: str, content: str = None) -> List[RuffError]:
223272
except RuntimeError:
224273
return []
225274

226-
# Get configuration from vim variables
227-
# Use Ruff-specific options if set, otherwise fall back to legacy options
228-
ruff_select = env.var('g:pymode_ruff_select', silence=True, default=[])
229-
ruff_ignore = env.var('g:pymode_ruff_ignore', silence=True, default=[])
230-
231-
if ruff_select or ruff_ignore:
232-
# Use Ruff-specific configuration
233-
linters = env.var('g:pymode_lint_checkers', default=['pyflakes', 'pycodestyle'])
234-
ignore = ruff_ignore if ruff_ignore else env.var('g:pymode_lint_ignore', default=[])
235-
select = ruff_select if ruff_select else env.var('g:pymode_lint_select', default=[])
236-
else:
237-
# Use legacy configuration (backward compatibility)
238-
linters = env.var('g:pymode_lint_checkers', default=['pyflakes', 'pycodestyle'])
239-
ignore = env.var('g:pymode_lint_ignore', default=[])
240-
select = env.var('g:pymode_lint_select', default=[])
241-
242-
# Build ruff configuration
243-
config = _build_ruff_config(linters, ignore, select)
275+
# Get configuration mode
276+
config_mode = env.var('g:pymode_ruff_config_mode', silence=True, default='local_override')
244277

245278
# Prepare command
246279
cmd = [ruff_path, 'check', '--output-format=json']
247280

248-
# Add configuration arguments
249-
if config:
250-
cmd.extend(_build_ruff_args(config))
281+
# Check for local config file (used in multiple modes)
282+
local_config = _find_local_ruff_config(file_path)
283+
284+
# Determine which config to use based on mode
285+
if config_mode == 'local':
286+
# Use only local config - don't pass any CLI config args
287+
# If local config exists and we'll use a temp file, explicitly point to it
288+
if local_config and content is not None:
289+
cmd.extend(['--config', local_config])
290+
# Otherwise, Ruff will auto-discover local config files
291+
elif config_mode == 'local_override':
292+
# Check if local config exists
293+
if local_config:
294+
# Local config found - use it
295+
# If we'll use a temp file, explicitly point to the config
296+
if content is not None:
297+
cmd.extend(['--config', local_config])
298+
# Otherwise, Ruff will auto-discover and use local config
299+
else:
300+
# No local config - use pymode settings as fallback
301+
ruff_select = env.var('g:pymode_ruff_select', silence=True, default=[])
302+
ruff_ignore = env.var('g:pymode_ruff_ignore', silence=True, default=[])
303+
304+
if ruff_select or ruff_ignore:
305+
# Use Ruff-specific configuration
306+
linters = env.var('g:pymode_lint_checkers', default=['pyflakes', 'pycodestyle'])
307+
ignore = ruff_ignore if ruff_ignore else env.var('g:pymode_lint_ignore', default=[])
308+
select = ruff_select if ruff_select else env.var('g:pymode_lint_select', default=[])
309+
else:
310+
# Use legacy configuration (backward compatibility)
311+
linters = env.var('g:pymode_lint_checkers', default=['pyflakes', 'pycodestyle'])
312+
ignore = env.var('g:pymode_lint_ignore', default=[])
313+
select = env.var('g:pymode_lint_select', default=[])
314+
315+
# Build ruff configuration
316+
config = _build_ruff_config(linters, ignore, select)
317+
318+
# Add configuration arguments
319+
if config:
320+
cmd.extend(_build_ruff_args(config))
321+
elif config_mode == 'global':
322+
# Use only pymode settings - ignore local configs
323+
cmd.append('--isolated')
324+
325+
# Get pymode configuration
326+
ruff_select = env.var('g:pymode_ruff_select', silence=True, default=[])
327+
ruff_ignore = env.var('g:pymode_ruff_ignore', silence=True, default=[])
328+
329+
if ruff_select or ruff_ignore:
330+
# Use Ruff-specific configuration
331+
linters = env.var('g:pymode_lint_checkers', default=['pyflakes', 'pycodestyle'])
332+
ignore = ruff_ignore if ruff_ignore else env.var('g:pymode_lint_ignore', default=[])
333+
select = ruff_select if ruff_select else env.var('g:pymode_lint_select', default=[])
334+
else:
335+
# Use legacy configuration (backward compatibility)
336+
linters = env.var('g:pymode_lint_checkers', default=['pyflakes', 'pycodestyle'])
337+
ignore = env.var('g:pymode_lint_ignore', default=[])
338+
select = env.var('g:pymode_lint_select', default=[])
339+
340+
# Build ruff configuration
341+
config = _build_ruff_config(linters, ignore, select)
342+
343+
# Add configuration arguments
344+
if config:
345+
cmd.extend(_build_ruff_args(config))
346+
else:
347+
# Invalid mode - default to local_override behavior
348+
env.debug(f"Invalid g:pymode_ruff_config_mode: {config_mode}, using 'local_override'")
349+
if not local_config:
350+
# No local config - use pymode settings
351+
ruff_select = env.var('g:pymode_ruff_select', silence=True, default=[])
352+
ruff_ignore = env.var('g:pymode_ruff_ignore', silence=True, default=[])
353+
354+
if ruff_select or ruff_ignore:
355+
linters = env.var('g:pymode_lint_checkers', default=['pyflakes', 'pycodestyle'])
356+
ignore = ruff_ignore if ruff_ignore else env.var('g:pymode_lint_ignore', default=[])
357+
select = ruff_select if ruff_select else env.var('g:pymode_lint_select', default=[])
358+
else:
359+
linters = env.var('g:pymode_lint_checkers', default=['pyflakes', 'pycodestyle'])
360+
ignore = env.var('g:pymode_lint_ignore', default=[])
361+
select = env.var('g:pymode_lint_select', default=[])
362+
363+
config = _build_ruff_config(linters, ignore, select)
364+
if config:
365+
cmd.extend(_build_ruff_args(config))
251366

252367
# Handle content checking (for unsaved buffers)
253368
temp_file_path = None
@@ -329,13 +444,46 @@ def run_ruff_format(file_path: str, content: str = None) -> Optional[str]:
329444
if not env.var('g:pymode_ruff_format_enabled', silence=True, default=True):
330445
return None
331446

447+
# Get configuration mode
448+
config_mode = env.var('g:pymode_ruff_config_mode', silence=True, default='local_override')
449+
450+
# Check for local config file (used in multiple modes)
451+
local_config = _find_local_ruff_config(file_path)
452+
332453
# Prepare command
333454
cmd = [ruff_path, 'format', '--stdin-filename', file_path]
334455

335-
# Get configuration file if specified
336-
config_file = env.var('g:pymode_ruff_config_file', silence=True, default='')
337-
if config_file and os.path.exists(config_file):
338-
cmd.extend(['--config', config_file])
456+
# Determine which config to use based on mode
457+
if config_mode == 'local':
458+
# Use only local config - Ruff will use --stdin-filename to discover config
459+
# If local config exists, explicitly point to it for consistency
460+
if local_config:
461+
cmd.extend(['--config', local_config])
462+
elif config_mode == 'local_override':
463+
# Check if local config exists
464+
if local_config:
465+
# Local config found - explicitly use it
466+
cmd.extend(['--config', local_config])
467+
else:
468+
# No local config - use pymode config file if specified
469+
config_file = env.var('g:pymode_ruff_config_file', silence=True, default='')
470+
if config_file and os.path.exists(config_file):
471+
cmd.extend(['--config', config_file])
472+
elif config_mode == 'global':
473+
# Use only pymode settings - ignore local configs
474+
cmd.append('--isolated')
475+
476+
# Use pymode config file if specified
477+
config_file = env.var('g:pymode_ruff_config_file', silence=True, default='')
478+
if config_file and os.path.exists(config_file):
479+
cmd.extend(['--config', config_file])
480+
else:
481+
# Invalid mode - default to local_override behavior
482+
env.debug(f"Invalid g:pymode_ruff_config_mode: {config_mode}, using 'local_override'")
483+
if not local_config:
484+
config_file = env.var('g:pymode_ruff_config_file', silence=True, default='')
485+
if config_file and os.path.exists(config_file):
486+
cmd.extend(['--config', config_file])
339487

340488
try:
341489
with silence_stderr():

0 commit comments

Comments
 (0)