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
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Handler functions for pytest tool special modes.
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"""
8import re
9import shlex
10from typing import TYPE_CHECKING
12from loguru import logger
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)
21if TYPE_CHECKING:
22 from lintro.tools.definitions.pytest import PytestPlugin
25def handle_list_plugins(tool: "PytestPlugin") -> ToolResult:
26 """Handle list plugins mode.
28 Args:
29 tool: PytestTool instance.
31 Returns:
32 ToolResult: Results with plugin list.
33 """
34 plugins = list_installed_plugins()
35 version_info = get_pytest_version_info()
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.")
45 return ToolResult(
46 name=tool.definition.name,
47 success=True,
48 issues=[],
49 output="\n".join(output_lines),
50 issues_count=0,
51 )
54def handle_check_plugins(
55 tool: "PytestPlugin",
56 required_plugins: str | None,
57) -> ToolResult:
58 """Handle check plugins mode.
60 Args:
61 tool: PytestTool instance.
62 required_plugins: Comma-separated list of required plugin names.
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 )
78 plugin_list = [p.strip() for p in required_plugins.split(",") if p.strip()]
79 missing_plugins: list[str] = []
80 installed_plugins: list[str] = []
82 for plugin in plugin_list:
83 if check_plugin_installed(plugin):
84 installed_plugins.append(plugin)
85 else:
86 missing_plugins.append(plugin)
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}")
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}")
102 success = len(missing_plugins) == 0
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 )
113def handle_collect_only(
114 tool: "PytestPlugin",
115 target_files: list[str],
116) -> ToolResult:
117 """Handle collect-only mode.
119 Args:
120 tool: PytestTool instance.
121 target_files: Files or directories to collect tests from.
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)
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 )
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))
157 output_lines = [f"Collected {len(test_list)} test(s):", ""]
158 for test in test_list:
159 output_lines.append(f" - {test}")
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 )
179def handle_list_fixtures(
180 tool: "PytestPlugin",
181 target_files: list[str],
182) -> ToolResult:
183 """Handle list fixtures mode.
185 Args:
186 tool: PytestTool instance.
187 target_files: Files or directories to collect fixtures from.
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)
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 )
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 )
225def handle_fixture_info(
226 tool: "PytestPlugin",
227 fixture_name: str,
228 target_files: list[str],
229) -> ToolResult:
230 """Handle fixture info mode.
232 Args:
233 tool: PytestTool instance.
234 fixture_name: Name of fixture to get info for.
235 target_files: Files or directories to search.
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)
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 )
255 # Extract fixture info for the specific fixture
256 lines = output.splitlines()
257 fixture_info_lines: list[str] = []
258 in_fixture = False
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)
275 if fixture_info_lines:
276 output_text = "\n".join(fixture_info_lines)
277 else:
278 output_text = f"Fixture '{fixture_name}' not found."
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 )
298def handle_list_markers(tool: "PytestPlugin") -> ToolResult:
299 """Handle list markers mode.
301 Args:
302 tool: PytestTool instance.
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"])
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 )
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 )
339def handle_parametrize_help(tool: "PytestPlugin") -> ToolResult:
340 """Handle parametrize help mode.
342 Args:
343 tool: PytestTool instance.
345 Returns:
346 ToolResult: Results with parametrization help.
347 """
348 help_text = """Pytest Parametrization Help
350Parametrization allows you to run the same test with different inputs.
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.
357Example:
358@pytest.mark.parametrize("input,expected", [(1, 2), (2, 4), (3, 6)])
359def test_multiply(input, expected):
360 assert input * 2 == expected
362Multiple Parameters:
363--------------------
364You can parametrize multiple parameters at once by providing tuples of values.
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.
371Multiple Parametrizations:
372--------------------------
373You can stack multiple @pytest.mark.parametrize decorators to create a cartesian
374product of all parameter combinations.
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 )