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

1"""Configuration priority and ordering functions for Lintro. 

2 

3This module provides functions for determining tool execution order, 

4priority-based configuration, and effective configuration values. 

5""" 

6 

7from __future__ import annotations 

8 

9from typing import Any 

10 

11from loguru import logger 

12 

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 

25 

26 

27def _get_nested_value(config: dict[str, Any], key_path: str) -> Any: 

28 """Get a nested value from a config dict using dot notation. 

29 

30 Args: 

31 config: Configuration dictionary. 

32 key_path: Dot-separated key path (e.g., "line-length.max"). 

33 

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 

45 

46 

47def get_tool_priority(tool_name: str) -> int: 

48 """Get the execution priority for a tool. 

49 

50 Lower values run first. Formatters have lower priorities than linters. 

51 

52 Args: 

53 tool_name: Name of the tool. 

54 

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() 

68 

69 # Check for override first 

70 if tool_name_lower in priority_overrides_normalized: 

71 return priority_overrides_normalized[tool_name_lower] 

72 

73 # Use default priority 

74 return int(DEFAULT_TOOL_PRIORITIES.get(tool_name_lower, 50)) 

75 

76 

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. 

82 

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. 

90 

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 = [] 

119 

120 if strategy == ToolOrderStrategy.ALPHABETICAL: 

121 return sorted(tool_names, key=str.lower) 

122 

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) 

128 

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) 

136 

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 

142 

143 # Default: priority-based ordering 

144 return sorted(tool_names, key=lambda t: (get_tool_priority(t), t.lower())) 

145 

146 

147def get_effective_line_length(tool_name: str) -> int | None: 

148 """Get the effective line length for a specific tool. 

149 

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) 

156 

157 Args: 

158 tool_name: Name of the tool. 

159 

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"] 

169 

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"] 

182 

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"] 

193 

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 

201 

202 return None