Coverage for lintro / plugins / execution_preparation.py: 96%

89 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Execution preparation utilities for tool plugins. 

2 

3This module provides execution preparation, version checking, and config injection. 

4""" 

5 

6from __future__ import annotations 

7 

8import os 

9from typing import Any 

10 

11from loguru import logger 

12 

13from lintro.config.lintro_config import LintroConfig 

14from lintro.models.core.tool_result import ToolResult 

15from lintro.plugins.file_discovery import discover_files, get_cwd, validate_paths 

16from lintro.plugins.protocol import ToolDefinition 

17 

18# Constants for default values 

19DEFAULT_TIMEOUT: int = 30 

20 

21 

22def get_effective_timeout( 

23 timeout: int | float | None, 

24 options: dict[str, object], 

25 default_timeout: int, 

26) -> float: 

27 """Get the effective timeout value. 

28 

29 Args: 

30 timeout: Override timeout value, or None to use default. 

31 options: Options dict that may contain timeout. 

32 default_timeout: Default timeout from definition. 

33 

34 Returns: 

35 Timeout value in seconds. 

36 """ 

37 if timeout is not None: 

38 return float(timeout) 

39 

40 raw_timeout = options.get("timeout", default_timeout) 

41 if isinstance(raw_timeout, (int, float)): 

42 return float(raw_timeout) 

43 

44 # Warn about invalid timeout value 

45 if raw_timeout is not None: 

46 type_name = type(raw_timeout).__name__ 

47 logger.warning( 

48 f"Invalid timeout value {raw_timeout!r} (type {type_name}), " 

49 f"using default {default_timeout}s", 

50 ) 

51 return float(default_timeout) 

52 

53 

54def get_executable_command(tool_name: str) -> list[str]: 

55 """Get the command prefix to execute a tool. 

56 

57 Delegates to CommandBuilderRegistry for language-specific logic. 

58 

59 Args: 

60 tool_name: Name of the tool executable. 

61 

62 Returns: 

63 Command prefix list. 

64 """ 

65 from lintro.enums.tool_name import normalize_tool_name 

66 from lintro.tools.core.command_builders import CommandBuilderRegistry 

67 

68 try: 

69 tool_name_enum = normalize_tool_name(tool_name) 

70 except ValueError: 

71 tool_name_enum = None 

72 

73 result: list[str] = CommandBuilderRegistry.get_command(tool_name, tool_name_enum) 

74 return result 

75 

76 

77def verify_tool_version(definition: ToolDefinition) -> ToolResult | None: 

78 """Verify that the tool meets minimum version requirements. 

79 

80 Args: 

81 definition: Tool definition with name. 

82 

83 Returns: 

84 None if version check passes, or a skip result if it fails. 

85 """ 

86 from lintro.tools.core.version_requirements import check_tool_version 

87 

88 command = get_executable_command(definition.name) 

89 version_info = check_tool_version(definition.name, command) 

90 

91 if version_info.version_check_passed: 

92 return None 

93 

94 skip_message = ( 

95 f"Skipping {definition.name}: {version_info.error_message}. " 

96 f"Minimum required: {version_info.min_version}. " 

97 f"{version_info.install_hint}" 

98 ) 

99 

100 return ToolResult( 

101 name=definition.name, 

102 success=True, 

103 output=skip_message, 

104 issues_count=0, 

105 skipped=True, 

106 skip_reason=version_info.error_message, 

107 ) 

108 

109 

110def prepare_execution( 

111 paths: list[str], 

112 options: dict[str, object], 

113 definition: ToolDefinition, 

114 exclude_patterns: list[str], 

115 include_venv: bool, 

116 current_options: dict[str, object], 

117 no_files_message: str = "No files to check.", 

118) -> dict[str, Any]: 

119 """Prepare execution context with common boilerplate steps. 

120 

121 This function consolidates repeated patterns: 

122 - Merge options with defaults 

123 - Validate input paths 

124 - Discover files matching patterns (returns early if none found) 

125 - Verify tool version requirements (skipped when no files match) 

126 - Compute working directory and relative paths 

127 - Compute timeout for execution 

128 

129 Args: 

130 paths: Input paths to process. 

131 options: Runtime options to merge with defaults. 

132 definition: Tool definition. 

133 exclude_patterns: Patterns to exclude. 

134 include_venv: Whether to include venv files. 

135 current_options: Current plugin options. 

136 no_files_message: Message when no files are found. 

137 

138 Returns: 

139 Dictionary with files, rel_files, cwd, timeout, and optional early_result. 

140 """ 

141 # Merge runtime options with defaults 

142 merged_options = dict(current_options) 

143 merged_options.update(options) 

144 

145 # Validate paths 

146 validate_paths(paths) 

147 if not paths: 

148 return { 

149 "early_result": ToolResult( 

150 name=definition.name, 

151 success=True, 

152 output=no_files_message, 

153 issues_count=0, 

154 ), 

155 } 

156 

157 # Discover files matching tool patterns 

158 files = discover_files( 

159 paths=paths, 

160 definition=definition, 

161 exclude_patterns=exclude_patterns, 

162 include_venv=include_venv, 

163 ) 

164 

165 if not files: 

166 file_type = "files" 

167 patterns = definition.file_patterns 

168 if patterns: 

169 extensions = [p.replace("*", "") for p in patterns if p.startswith("*.")] 

170 if extensions: 

171 file_type = "/".join(extensions) + " files" 

172 

173 return { 

174 "early_result": ToolResult( 

175 name=definition.name, 

176 success=True, 

177 output=f"No {file_type} found to check.", 

178 issues_count=0, 

179 ), 

180 } 

181 

182 # Check version requirements (only when files exist to check) 

183 version_result = verify_tool_version(definition) 

184 if version_result is not None: 

185 return {"early_result": version_result} 

186 

187 logger.debug(f"Files to process: {files}") 

188 

189 # Compute cwd and relative paths 

190 cwd = get_cwd(files) 

191 rel_files = [os.path.relpath(f, cwd) if cwd else f for f in files] 

192 

193 # Get timeout (keep as float to preserve precision) 

194 timeout_value = merged_options.get("timeout") 

195 timeout = get_effective_timeout( 

196 timeout_value if isinstance(timeout_value, (int, float)) else None, 

197 merged_options, 

198 definition.default_timeout, 

199 ) 

200 

201 logger.debug( 

202 f"Prepared execution: {len(files)} files, cwd={cwd}, timeout={timeout}s", 

203 ) 

204 return { 

205 "files": files, 

206 "rel_files": rel_files, 

207 "cwd": cwd, 

208 "timeout": timeout, 

209 } 

210 

211 

212# ------------------------------------------------------------------------- 

213# Lintro Config Support Functions 

214# ------------------------------------------------------------------------- 

215 

216 

217def get_lintro_config() -> LintroConfig: 

218 """Get the current Lintro configuration. 

219 

220 Returns: 

221 The current LintroConfig instance. 

222 """ 

223 from lintro.tools.core.config_injection import _get_lintro_config 

224 

225 result: LintroConfig = _get_lintro_config() 

226 return result 

227 

228 

229def get_enforced_settings( 

230 lintro_config: LintroConfig | None = None, 

231) -> dict[str, object]: 

232 """Get enforced settings as a dictionary. 

233 

234 Args: 

235 lintro_config: Optional config to use, or None to get current. 

236 

237 Returns: 

238 Dictionary of enforced settings. 

239 """ 

240 from lintro.tools.core.config_injection import _get_enforced_settings 

241 

242 config = lintro_config or get_lintro_config() 

243 result: dict[str, object] = _get_enforced_settings(lintro_config=config) 

244 return result 

245 

246 

247def get_enforce_cli_args( 

248 tool_name: str, 

249 lintro_config: LintroConfig | None = None, 

250) -> list[str]: 

251 """Get CLI arguments for enforced settings. 

252 

253 Args: 

254 tool_name: Name of the tool. 

255 lintro_config: Optional config to use, or None to get current. 

256 

257 Returns: 

258 List of CLI arguments for enforced settings. 

259 """ 

260 from lintro.tools.core.config_injection import _get_enforce_cli_args 

261 

262 config = lintro_config or get_lintro_config() 

263 result: list[str] = _get_enforce_cli_args(tool_name=tool_name, lintro_config=config) 

264 return result 

265 

266 

267def get_defaults_config_args( 

268 tool_name: str, 

269 lintro_config: LintroConfig | None = None, 

270) -> list[str]: 

271 """Get CLI arguments for defaults config injection. 

272 

273 Args: 

274 tool_name: Name of the tool. 

275 lintro_config: Optional config to use, or None to get current. 

276 

277 Returns: 

278 List of CLI arguments for defaults config. 

279 """ 

280 from lintro.tools.core.config_injection import _get_defaults_config_args 

281 

282 config = lintro_config or get_lintro_config() 

283 result: list[str] = _get_defaults_config_args( 

284 tool_name=tool_name, 

285 lintro_config=config, 

286 ) 

287 return result 

288 

289 

290def should_use_lintro_config(tool_name: str) -> bool: 

291 """Check if Lintro config should be used for this tool. 

292 

293 Args: 

294 tool_name: Name of the tool. 

295 

296 Returns: 

297 True if Lintro config should be used. 

298 """ 

299 from lintro.tools.core.config_injection import _should_use_lintro_config 

300 

301 result: bool = _should_use_lintro_config(tool_name=tool_name) 

302 return result 

303 

304 

305def build_config_args( 

306 tool_name: str, 

307 lintro_config: LintroConfig | None = None, 

308) -> list[str]: 

309 """Build combined CLI arguments for config injection. 

310 

311 Args: 

312 tool_name: Name of the tool. 

313 lintro_config: Optional config to use, or None to get current. 

314 

315 Returns: 

316 List of combined CLI arguments for config. 

317 """ 

318 from lintro.tools.core.config_injection import _build_config_args 

319 

320 config = lintro_config or get_lintro_config() 

321 result: list[str] = _build_config_args(tool_name=tool_name, lintro_config=config) 

322 return result