Coverage for lintro / tools / implementations / pytest / pytest_handlers.py: 100%

118 statements  

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

1"""Handler functions for pytest tool special modes. 

2 

3This module contains handler functions extracted from PytestTool to improve 

4maintainability and reduce file size. These handlers implement special modes 

5like listing plugins, collecting tests, listing fixtures, etc. 

6""" 

7 

8import re 

9import shlex 

10from typing import TYPE_CHECKING 

11 

12from loguru import logger 

13 

14from lintro.models.core.tool_result import ToolResult 

15from lintro.tools.implementations.pytest.markers import ( 

16 check_plugin_installed, 

17 get_pytest_version_info, 

18 list_installed_plugins, 

19) 

20 

21if TYPE_CHECKING: 

22 from lintro.tools.definitions.pytest import PytestPlugin 

23 

24 

25def handle_list_plugins(tool: "PytestPlugin") -> ToolResult: 

26 """Handle list plugins mode. 

27 

28 Args: 

29 tool: PytestTool instance. 

30 

31 Returns: 

32 ToolResult: Results with plugin list. 

33 """ 

34 plugins = list_installed_plugins() 

35 version_info = get_pytest_version_info() 

36 

37 output_lines = [version_info, ""] 

38 if plugins: 

39 output_lines.append(f"Installed pytest plugins ({len(plugins)}):") 

40 for plugin in plugins: 

41 output_lines.append(f" - {plugin['name']} ({plugin['version']})") 

42 else: 

43 output_lines.append("No pytest plugins found.") 

44 

45 return ToolResult( 

46 name=tool.definition.name, 

47 success=True, 

48 issues=[], 

49 output="\n".join(output_lines), 

50 issues_count=0, 

51 ) 

52 

53 

54def handle_check_plugins( 

55 tool: "PytestPlugin", 

56 required_plugins: str | None, 

57) -> ToolResult: 

58 """Handle check plugins mode. 

59 

60 Args: 

61 tool: PytestTool instance. 

62 required_plugins: Comma-separated list of required plugin names. 

63 

64 Returns: 

65 ToolResult: Results with plugin check status. 

66 """ 

67 if not required_plugins: 

68 return ToolResult( 

69 name=tool.definition.name, 

70 success=False, 

71 issues=[], 

72 output=( 

73 "Error: required_plugins must be specified when check_plugins=True" 

74 ), 

75 issues_count=0, 

76 ) 

77 

78 plugin_list = [p.strip() for p in required_plugins.split(",") if p.strip()] 

79 missing_plugins: list[str] = [] 

80 installed_plugins: list[str] = [] 

81 

82 for plugin in plugin_list: 

83 if check_plugin_installed(plugin): 

84 installed_plugins.append(plugin) 

85 else: 

86 missing_plugins.append(plugin) 

87 

88 output_lines = [] 

89 if installed_plugins: 

90 output_lines.append(f"✓ Installed plugins ({len(installed_plugins)}):") 

91 for plugin in installed_plugins: 

92 output_lines.append(f" - {plugin}") 

93 

94 if missing_plugins: 

95 output_lines.append(f"\n✗ Missing plugins ({len(missing_plugins)}):") 

96 for plugin in missing_plugins: 

97 output_lines.append(f" - {plugin}") 

98 output_lines.append("\nInstall missing plugins with:") 

99 quoted_plugins = " ".join(shlex.quote(plugin) for plugin in missing_plugins) 

100 output_lines.append(f" pip install {quoted_plugins}") 

101 

102 success = len(missing_plugins) == 0 

103 

104 return ToolResult( 

105 name=tool.definition.name, 

106 success=success, 

107 issues=[], 

108 output="\n".join(output_lines) if output_lines else "No plugins specified.", 

109 issues_count=len(missing_plugins), 

110 ) 

111 

112 

113def handle_collect_only( 

114 tool: "PytestPlugin", 

115 target_files: list[str], 

116) -> ToolResult: 

117 """Handle collect-only mode. 

118 

119 Args: 

120 tool: PytestTool instance. 

121 target_files: Files or directories to collect tests from. 

122 

123 Returns: 

124 ToolResult: Results with collected test list. 

125 """ 

126 try: 

127 collect_cmd = tool._get_executable_command(tool_name="pytest") 

128 collect_cmd.append("--collect-only") 

129 collect_cmd.extend(target_files) 

130 

131 success, output = tool._run_subprocess(collect_cmd) 

132 if not success: 

133 return ToolResult( 

134 name=tool.definition.name, 

135 success=False, 

136 issues=[], 

137 output=output, 

138 issues_count=0, 

139 ) 

140 

141 # Parse collected tests from output 

142 test_list: list[str] = [] 

143 for line in output.splitlines(): 

144 line = line.strip() 

145 # Match test collection lines 

146 # (e.g., "<Function test_example>" or "test_file.py::test_function") 

147 if "<Function" in line or "::" in line: 

148 # Extract test identifier 

149 if "::" in line: 

150 test_list.append(line.split("::")[-1].strip()) 

151 elif "<Function" in line: 

152 # Extract function name from <Function test_name> 

153 match = re.search(r"<Function\s+(\w+)>", line) 

154 if match: 

155 test_list.append(match.group(1)) 

156 

157 output_lines = [f"Collected {len(test_list)} test(s):", ""] 

158 for test in test_list: 

159 output_lines.append(f" - {test}") 

160 

161 return ToolResult( 

162 name=tool.definition.name, 

163 success=True, 

164 issues=[], 

165 output="\n".join(output_lines), 

166 issues_count=0, 

167 ) 

168 except (OSError, ValueError, RuntimeError) as e: 

169 logger.exception(f"Error collecting tests: {e}") 

170 return ToolResult( 

171 name=tool.definition.name, 

172 success=False, 

173 issues=[], 

174 output=f"Error collecting tests: {type(e).__name__}: {e}", 

175 issues_count=0, 

176 ) 

177 

178 

179def handle_list_fixtures( 

180 tool: "PytestPlugin", 

181 target_files: list[str], 

182) -> ToolResult: 

183 """Handle list fixtures mode. 

184 

185 Args: 

186 tool: PytestTool instance. 

187 target_files: Files or directories to collect fixtures from. 

188 

189 Returns: 

190 ToolResult: Results with fixture list. 

191 """ 

192 try: 

193 fixtures_cmd = tool._get_executable_command(tool_name="pytest") 

194 fixtures_cmd.extend(["--fixtures", "-q"]) 

195 fixtures_cmd.extend(target_files) 

196 

197 success, output = tool._run_subprocess(fixtures_cmd) 

198 if not success: 

199 return ToolResult( 

200 name=tool.definition.name, 

201 success=False, 

202 issues=[], 

203 output=output, 

204 issues_count=0, 

205 ) 

206 

207 return ToolResult( 

208 name=tool.definition.name, 

209 success=True, 

210 issues=[], 

211 output=output, 

212 issues_count=0, 

213 ) 

214 except (OSError, ValueError, RuntimeError) as e: 

215 logger.exception(f"Error listing fixtures: {e}") 

216 return ToolResult( 

217 name=tool.definition.name, 

218 success=False, 

219 issues=[], 

220 output=f"Error listing fixtures: {type(e).__name__}: {e}", 

221 issues_count=0, 

222 ) 

223 

224 

225def handle_fixture_info( 

226 tool: "PytestPlugin", 

227 fixture_name: str, 

228 target_files: list[str], 

229) -> ToolResult: 

230 """Handle fixture info mode. 

231 

232 Args: 

233 tool: PytestTool instance. 

234 fixture_name: Name of fixture to get info for. 

235 target_files: Files or directories to search. 

236 

237 Returns: 

238 ToolResult: Results with fixture information. 

239 """ 

240 try: 

241 fixtures_cmd = tool._get_executable_command(tool_name="pytest") 

242 fixtures_cmd.extend(["--fixtures", "-v"]) 

243 fixtures_cmd.extend(target_files) 

244 

245 success, output = tool._run_subprocess(fixtures_cmd) 

246 if not success: 

247 return ToolResult( 

248 name=tool.definition.name, 

249 success=False, 

250 issues=[], 

251 output=output, 

252 issues_count=0, 

253 ) 

254 

255 # Extract fixture info for the specific fixture 

256 lines = output.splitlines() 

257 fixture_info_lines: list[str] = [] 

258 in_fixture = False 

259 

260 for line in lines: 

261 # Check if line starts with fixture name (pytest format) 

262 stripped_line = line.strip() 

263 if stripped_line.startswith(fixture_name) and ( 

264 len(stripped_line) == len(fixture_name) 

265 or stripped_line[len(fixture_name)] in (" ", ":", "\n") 

266 ): 

267 in_fixture = True 

268 fixture_info_lines.append(line) 

269 elif in_fixture: 

270 if line.strip() and not line.startswith(" "): 

271 # New fixture or section, stop 

272 break 

273 fixture_info_lines.append(line) 

274 

275 if fixture_info_lines: 

276 output_text = "\n".join(fixture_info_lines) 

277 else: 

278 output_text = f"Fixture '{fixture_name}' not found." 

279 

280 return ToolResult( 

281 name=tool.definition.name, 

282 success=len(fixture_info_lines) > 0, 

283 issues=[], 

284 output=output_text, 

285 issues_count=0, 

286 ) 

287 except (OSError, ValueError, RuntimeError) as e: 

288 logger.exception(f"Error getting fixture info: {e}") 

289 return ToolResult( 

290 name=tool.definition.name, 

291 success=False, 

292 issues=[], 

293 output=f"Error getting fixture info: {type(e).__name__}: {e}", 

294 issues_count=0, 

295 ) 

296 

297 

298def handle_list_markers(tool: "PytestPlugin") -> ToolResult: 

299 """Handle list markers mode. 

300 

301 Args: 

302 tool: PytestTool instance. 

303 

304 Returns: 

305 ToolResult: Results with marker list. 

306 """ 

307 try: 

308 markers_cmd = tool._get_executable_command(tool_name="pytest") 

309 markers_cmd.extend(["--markers"]) 

310 

311 success, output = tool._run_subprocess(markers_cmd) 

312 if not success: 

313 return ToolResult( 

314 name=tool.definition.name, 

315 success=False, 

316 issues=[], 

317 output=output, 

318 issues_count=0, 

319 ) 

320 

321 return ToolResult( 

322 name=tool.definition.name, 

323 success=True, 

324 issues=[], 

325 output=output, 

326 issues_count=0, 

327 ) 

328 except (OSError, ValueError, RuntimeError) as e: 

329 logger.exception(f"Error listing markers: {e}") 

330 return ToolResult( 

331 name=tool.definition.name, 

332 success=False, 

333 issues=[], 

334 output=f"Error listing markers: {type(e).__name__}: {e}", 

335 issues_count=0, 

336 ) 

337 

338 

339def handle_parametrize_help(tool: "PytestPlugin") -> ToolResult: 

340 """Handle parametrize help mode. 

341 

342 Args: 

343 tool: PytestTool instance. 

344 

345 Returns: 

346 ToolResult: Results with parametrization help. 

347 """ 

348 help_text = """Pytest Parametrization Help 

349 

350Parametrization allows you to run the same test with different inputs. 

351 

352Basic Usage: 

353----------- 

354Use @pytest.mark.parametrize to provide multiple input values for a test function. 

355The test will run once for each set of parameters. 

356 

357Example: 

358@pytest.mark.parametrize("input,expected", [(1, 2), (2, 4), (3, 6)]) 

359def test_multiply(input, expected): 

360 assert input * 2 == expected 

361 

362Multiple Parameters: 

363-------------------- 

364You can parametrize multiple parameters at once by providing tuples of values. 

365 

366Using Fixtures with Parametrization: 

367------------------------------------- 

368Parametrized tests can use fixtures. The parametrization runs for each fixture 

369instance, creating a cartesian product of parameters and fixtures. 

370 

371Multiple Parametrizations: 

372-------------------------- 

373You can stack multiple @pytest.mark.parametrize decorators to create a cartesian 

374product of all parameter combinations. 

375 

376For detailed examples and advanced usage, see: 

377https://docs.pytest.org/en/stable/how-to/parametrize.html 

378""" 

379 return ToolResult( 

380 name=tool.definition.name, 

381 success=True, 

382 issues=[], 

383 output=help_text, 

384 issues_count=0, 

385 )