Coverage for lintro / utils / config_priority.py: 93%
83 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Configuration priority and ordering functions for Lintro.
3This module provides functions for determining tool execution order,
4priority-based configuration, and effective configuration values.
5"""
7from __future__ import annotations
9from typing import Any
11from loguru import logger
13from lintro.utils.config import (
14 get_tool_order_config,
15 load_lintro_global_config,
16 load_lintro_tool_config,
17 load_pyproject,
18)
19from lintro.utils.config_constants import (
20 DEFAULT_TOOL_PRIORITIES,
21 GLOBAL_SETTINGS,
22 ToolOrderStrategy,
23)
24from lintro.utils.native_parsers import _load_native_tool_config
27def _get_nested_value(config: dict[str, Any], key_path: str) -> Any:
28 """Get a nested value from a config dict using dot notation.
30 Args:
31 config: Configuration dictionary.
32 key_path: Dot-separated key path (e.g., "line-length.max").
34 Returns:
35 Value at path, or None if not found.
36 """
37 keys = key_path.split(".")
38 current = config
39 for key in keys:
40 if isinstance(current, dict) and key in current:
41 current = current[key]
42 else:
43 return None
44 return current
47def get_tool_priority(tool_name: str) -> int:
48 """Get the execution priority for a tool.
50 Lower values run first. Formatters have lower priorities than linters.
52 Args:
53 tool_name: Name of the tool.
55 Returns:
56 Priority value (lower = runs first).
57 """
58 order_config = get_tool_order_config()
59 priority_overrides_raw = order_config.get("priority_overrides", {})
60 priority_overrides = (
61 priority_overrides_raw if isinstance(priority_overrides_raw, dict) else {}
62 )
63 # Normalize priority_overrides keys to lowercase for consistent lookup
64 priority_overrides_normalized: dict[str, int] = {
65 k.lower(): int(v) for k, v in priority_overrides.items() if isinstance(v, int)
66 }
67 tool_name_lower = tool_name.lower()
69 # Check for override first
70 if tool_name_lower in priority_overrides_normalized:
71 return priority_overrides_normalized[tool_name_lower]
73 # Use default priority
74 return int(DEFAULT_TOOL_PRIORITIES.get(tool_name_lower, 50))
77def get_ordered_tools(
78 tool_names: list[str],
79 tool_order: str | list[str] | None = None,
80) -> list[str]:
81 """Get tool names in execution order based on configured strategy.
83 Args:
84 tool_names: List of tool names to order.
85 tool_order: Optional override for tool order strategy. Can be:
86 - "priority": Sort by tool priority (default).
87 - "alphabetical": Sort alphabetically.
88 - list[str]: Custom order (tools in list come first).
89 - None: Read strategy from config.
91 Returns:
92 List of tool names in execution order.
93 """
94 # Determine strategy and custom order
95 strategy: ToolOrderStrategy
96 if tool_order is None:
97 order_config = get_tool_order_config()
98 strategy_str = order_config.get("strategy", "priority")
99 try:
100 strategy = ToolOrderStrategy(strategy_str)
101 except ValueError:
102 logger.warning(
103 f"Invalid tool order strategy '{strategy_str}', using 'priority'",
104 )
105 strategy = ToolOrderStrategy.PRIORITY
106 custom_order = order_config.get("custom_order", [])
107 elif isinstance(tool_order, list):
108 strategy = ToolOrderStrategy.CUSTOM
109 custom_order = tool_order
110 else:
111 try:
112 strategy = ToolOrderStrategy(tool_order)
113 except ValueError:
114 logger.warning(
115 f"Invalid tool order strategy '{tool_order}', using 'priority'",
116 )
117 strategy = ToolOrderStrategy.PRIORITY
118 custom_order = []
120 if strategy == ToolOrderStrategy.ALPHABETICAL:
121 return sorted(tool_names, key=str.lower)
123 if strategy == ToolOrderStrategy.CUSTOM:
124 # Tools in custom_order come first (in that order), then remaining
125 # by priority
126 ordered: list[str] = []
127 remaining = list(tool_names)
129 for tool in custom_order:
130 # Case-insensitive matching for custom order
131 tool_lower = tool.lower()
132 matched = next((t for t in remaining if t.lower() == tool_lower), None)
133 if matched:
134 ordered.append(matched)
135 remaining.remove(matched)
137 # Add remaining tools by priority (consistent with default strategy)
138 ordered.extend(
139 sorted(remaining, key=lambda t: (get_tool_priority(t), t.lower())),
140 )
141 return ordered
143 # Default: priority-based ordering
144 return sorted(tool_names, key=lambda t: (get_tool_priority(t), t.lower()))
147def get_effective_line_length(tool_name: str) -> int | None:
148 """Get the effective line length for a specific tool.
150 Priority:
151 1. [tool.lintro.<tool>] line_length
152 2. [tool.lintro] line_length
153 3. [tool.ruff] line-length (as fallback source of truth)
154 4. Native tool config
155 5. None (use tool default)
157 Args:
158 tool_name: Name of the tool.
160 Returns:
161 Effective line length, or None to use tool default.
162 """
163 # 1. Check tool-specific lintro config
164 lintro_tool = load_lintro_tool_config(tool_name)
165 if "line_length" in lintro_tool and isinstance(lintro_tool["line_length"], int):
166 return lintro_tool["line_length"]
167 if "line-length" in lintro_tool and isinstance(lintro_tool["line-length"], int):
168 return lintro_tool["line-length"]
170 # 2. Check global lintro config
171 lintro_global = load_lintro_global_config()
172 if "line_length" in lintro_global and isinstance(
173 lintro_global["line_length"],
174 int,
175 ):
176 return lintro_global["line_length"]
177 if "line-length" in lintro_global and isinstance(
178 lintro_global["line-length"],
179 int,
180 ):
181 return lintro_global["line-length"]
183 # 3. Fall back to Ruff's line-length as source of truth
184 pyproject = load_pyproject()
185 tool_section_raw = pyproject.get("tool", {})
186 tool_section = tool_section_raw if isinstance(tool_section_raw, dict) else {}
187 ruff_config_raw = tool_section.get("ruff", {})
188 ruff_config = ruff_config_raw if isinstance(ruff_config_raw, dict) else {}
189 if "line-length" in ruff_config and isinstance(ruff_config["line-length"], int):
190 return ruff_config["line-length"]
191 if "line_length" in ruff_config and isinstance(ruff_config["line_length"], int):
192 return ruff_config["line_length"]
194 # 4. Check native tool config (for non-Ruff tools)
195 native = _load_native_tool_config(tool_name)
196 setting_key = GLOBAL_SETTINGS.get("line_length", {}).get("tools", {}).get(tool_name)
197 if setting_key:
198 native_value = _get_nested_value(native, setting_key)
199 if isinstance(native_value, int):
200 return native_value
202 return None