lintro 0.6.2__py3-none-any.whl → 0.17.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. lintro/__init__.py +1 -1
  2. lintro/cli.py +230 -14
  3. lintro/cli_utils/commands/__init__.py +8 -1
  4. lintro/cli_utils/commands/check.py +1 -0
  5. lintro/cli_utils/commands/config.py +325 -0
  6. lintro/cli_utils/commands/format.py +2 -2
  7. lintro/cli_utils/commands/init.py +361 -0
  8. lintro/cli_utils/commands/list_tools.py +180 -42
  9. lintro/cli_utils/commands/test.py +316 -0
  10. lintro/cli_utils/commands/versions.py +81 -0
  11. lintro/config/__init__.py +62 -0
  12. lintro/config/config_loader.py +420 -0
  13. lintro/config/lintro_config.py +189 -0
  14. lintro/config/tool_config_generator.py +403 -0
  15. lintro/enums/__init__.py +1 -0
  16. lintro/enums/darglint_strictness.py +10 -0
  17. lintro/enums/hadolint_enums.py +22 -0
  18. lintro/enums/tool_name.py +2 -0
  19. lintro/enums/tool_type.py +2 -0
  20. lintro/enums/yamllint_format.py +11 -0
  21. lintro/exceptions/__init__.py +1 -0
  22. lintro/formatters/__init__.py +1 -0
  23. lintro/formatters/core/__init__.py +1 -0
  24. lintro/formatters/core/output_style.py +11 -0
  25. lintro/formatters/core/table_descriptor.py +8 -0
  26. lintro/formatters/styles/csv.py +2 -0
  27. lintro/formatters/styles/grid.py +2 -0
  28. lintro/formatters/styles/html.py +2 -0
  29. lintro/formatters/styles/json.py +2 -0
  30. lintro/formatters/styles/markdown.py +2 -0
  31. lintro/formatters/styles/plain.py +2 -0
  32. lintro/formatters/tools/__init__.py +12 -0
  33. lintro/formatters/tools/black_formatter.py +27 -5
  34. lintro/formatters/tools/darglint_formatter.py +16 -1
  35. lintro/formatters/tools/eslint_formatter.py +108 -0
  36. lintro/formatters/tools/hadolint_formatter.py +13 -0
  37. lintro/formatters/tools/markdownlint_formatter.py +88 -0
  38. lintro/formatters/tools/prettier_formatter.py +15 -0
  39. lintro/formatters/tools/pytest_formatter.py +201 -0
  40. lintro/formatters/tools/ruff_formatter.py +26 -5
  41. lintro/formatters/tools/yamllint_formatter.py +14 -1
  42. lintro/models/__init__.py +1 -0
  43. lintro/models/core/__init__.py +1 -0
  44. lintro/models/core/tool_config.py +11 -7
  45. lintro/parsers/__init__.py +69 -9
  46. lintro/parsers/actionlint/actionlint_parser.py +1 -1
  47. lintro/parsers/bandit/__init__.py +6 -0
  48. lintro/parsers/bandit/bandit_issue.py +49 -0
  49. lintro/parsers/bandit/bandit_parser.py +99 -0
  50. lintro/parsers/black/black_issue.py +4 -0
  51. lintro/parsers/darglint/__init__.py +1 -0
  52. lintro/parsers/darglint/darglint_issue.py +11 -0
  53. lintro/parsers/eslint/__init__.py +6 -0
  54. lintro/parsers/eslint/eslint_issue.py +26 -0
  55. lintro/parsers/eslint/eslint_parser.py +63 -0
  56. lintro/parsers/markdownlint/__init__.py +6 -0
  57. lintro/parsers/markdownlint/markdownlint_issue.py +22 -0
  58. lintro/parsers/markdownlint/markdownlint_parser.py +113 -0
  59. lintro/parsers/prettier/__init__.py +1 -0
  60. lintro/parsers/prettier/prettier_issue.py +12 -0
  61. lintro/parsers/prettier/prettier_parser.py +1 -1
  62. lintro/parsers/pytest/__init__.py +21 -0
  63. lintro/parsers/pytest/pytest_issue.py +28 -0
  64. lintro/parsers/pytest/pytest_parser.py +483 -0
  65. lintro/parsers/ruff/ruff_parser.py +6 -2
  66. lintro/parsers/yamllint/__init__.py +1 -0
  67. lintro/tools/__init__.py +3 -1
  68. lintro/tools/core/__init__.py +1 -0
  69. lintro/tools/core/timeout_utils.py +112 -0
  70. lintro/tools/core/tool_base.py +286 -50
  71. lintro/tools/core/tool_manager.py +77 -24
  72. lintro/tools/core/version_requirements.py +482 -0
  73. lintro/tools/implementations/__init__.py +1 -0
  74. lintro/tools/implementations/pytest/pytest_command_builder.py +311 -0
  75. lintro/tools/implementations/pytest/pytest_config.py +200 -0
  76. lintro/tools/implementations/pytest/pytest_error_handler.py +128 -0
  77. lintro/tools/implementations/pytest/pytest_executor.py +122 -0
  78. lintro/tools/implementations/pytest/pytest_handlers.py +375 -0
  79. lintro/tools/implementations/pytest/pytest_option_validators.py +212 -0
  80. lintro/tools/implementations/pytest/pytest_output_processor.py +408 -0
  81. lintro/tools/implementations/pytest/pytest_result_processor.py +113 -0
  82. lintro/tools/implementations/pytest/pytest_utils.py +697 -0
  83. lintro/tools/implementations/tool_actionlint.py +106 -16
  84. lintro/tools/implementations/tool_bandit.py +34 -29
  85. lintro/tools/implementations/tool_black.py +236 -29
  86. lintro/tools/implementations/tool_darglint.py +183 -22
  87. lintro/tools/implementations/tool_eslint.py +374 -0
  88. lintro/tools/implementations/tool_hadolint.py +94 -25
  89. lintro/tools/implementations/tool_markdownlint.py +354 -0
  90. lintro/tools/implementations/tool_prettier.py +317 -24
  91. lintro/tools/implementations/tool_pytest.py +327 -0
  92. lintro/tools/implementations/tool_ruff.py +278 -84
  93. lintro/tools/implementations/tool_yamllint.py +448 -34
  94. lintro/tools/tool_enum.py +8 -0
  95. lintro/utils/__init__.py +1 -0
  96. lintro/utils/ascii_normalize_cli.py +5 -0
  97. lintro/utils/config.py +41 -18
  98. lintro/utils/console_logger.py +211 -25
  99. lintro/utils/path_utils.py +42 -0
  100. lintro/utils/tool_executor.py +339 -45
  101. lintro/utils/tool_utils.py +51 -24
  102. lintro/utils/unified_config.py +926 -0
  103. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/METADATA +172 -30
  104. lintro-0.17.2.dist-info/RECORD +134 -0
  105. lintro-0.6.2.dist-info/RECORD +0 -96
  106. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
  107. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
  108. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
  109. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,327 @@
1
+ """Pytest test runner integration."""
2
+
3
+ import subprocess # nosec B404 - used safely with shell disabled
4
+ from dataclasses import dataclass, field
5
+
6
+ from loguru import logger
7
+
8
+ from lintro.enums.tool_type import ToolType
9
+ from lintro.models.core.tool import ToolConfig, ToolResult
10
+ from lintro.tools.core.tool_base import BaseTool
11
+ from lintro.tools.implementations.pytest.pytest_command_builder import (
12
+ build_check_command,
13
+ )
14
+ from lintro.tools.implementations.pytest.pytest_config import PytestConfiguration
15
+ from lintro.tools.implementations.pytest.pytest_error_handler import PytestErrorHandler
16
+ from lintro.tools.implementations.pytest.pytest_executor import PytestExecutor
17
+ from lintro.tools.implementations.pytest.pytest_handlers import (
18
+ handle_check_plugins,
19
+ handle_collect_only,
20
+ handle_fixture_info,
21
+ handle_list_fixtures,
22
+ handle_list_markers,
23
+ handle_list_plugins,
24
+ handle_parametrize_help,
25
+ )
26
+ from lintro.tools.implementations.pytest.pytest_output_processor import (
27
+ parse_pytest_output_with_fallback,
28
+ )
29
+ from lintro.tools.implementations.pytest.pytest_result_processor import (
30
+ PytestResultProcessor,
31
+ )
32
+ from lintro.tools.implementations.pytest.pytest_utils import (
33
+ initialize_pytest_tool_config,
34
+ load_lintro_ignore,
35
+ )
36
+
37
+ # Constants for pytest configuration
38
+ PYTEST_DEFAULT_TIMEOUT: int = 300 # 5 minutes for test runs
39
+ PYTEST_DEFAULT_PRIORITY: int = 90
40
+ PYTEST_FILE_PATTERNS: list[str] = ["test_*.py", "*_test.py"]
41
+
42
+
43
+ @dataclass
44
+ class PytestTool(BaseTool):
45
+ """Pytest test runner integration.
46
+
47
+ Pytest is a mature full-featured Python testing tool that helps you write
48
+ better programs. It supports various testing patterns and provides extensive
49
+ configuration options.
50
+
51
+ Attributes:
52
+ name: str: Tool name.
53
+ description: str: Tool description.
54
+ can_fix: bool: Whether the tool can fix issues.
55
+ config: ToolConfig: Tool configuration.
56
+ exclude_patterns: list[str]: List of patterns to exclude.
57
+ include_venv: bool: Whether to include virtual environment files.
58
+ pytest_config: PytestConfiguration: Pytest-specific configuration.
59
+ executor: PytestExecutor: Test execution handler.
60
+ result_processor: PytestResultProcessor: Result processing handler.
61
+ error_handler: PytestErrorHandler: Error handling handler.
62
+ """
63
+
64
+ name: str = "pytest"
65
+ description: str = (
66
+ "Mature full-featured Python testing tool that helps you write better programs"
67
+ )
68
+ can_fix: bool = False # pytest doesn't fix code, it runs tests
69
+ config: ToolConfig = field(
70
+ default_factory=lambda: ToolConfig(
71
+ priority=PYTEST_DEFAULT_PRIORITY,
72
+ conflicts_with=[],
73
+ file_patterns=PYTEST_FILE_PATTERNS,
74
+ tool_type=ToolType.TEST_RUNNER,
75
+ ),
76
+ )
77
+ exclude_patterns: list[str] = field(default_factory=load_lintro_ignore)
78
+ include_venv: bool = False
79
+ _default_timeout: int = PYTEST_DEFAULT_TIMEOUT
80
+
81
+ # New component attributes
82
+ pytest_config: PytestConfiguration = field(default_factory=PytestConfiguration)
83
+ executor: PytestExecutor = field(init=False)
84
+ result_processor: PytestResultProcessor = field(init=False)
85
+ error_handler: PytestErrorHandler = field(init=False)
86
+
87
+ def __post_init__(self) -> None:
88
+ """Initialize the tool after dataclass creation."""
89
+ super().__post_init__()
90
+ initialize_pytest_tool_config(self)
91
+
92
+ # Initialize the new components with tool reference
93
+ self.executor = PytestExecutor(
94
+ config=self.pytest_config,
95
+ tool=self,
96
+ )
97
+ self.result_processor = PytestResultProcessor(self.pytest_config, self.name)
98
+ self.error_handler = PytestErrorHandler(self.name)
99
+
100
+ def set_options(self, **kwargs) -> None:
101
+ """Set pytest-specific options.
102
+
103
+ Args:
104
+ **kwargs: Option key-value pairs to set.
105
+
106
+ Delegates to PytestConfiguration for option management and validation.
107
+ Forwards unrecognized options (like timeout) to the base class.
108
+ """
109
+ # Extract pytest-specific options
110
+ config_fields = {
111
+ field.name for field in self.pytest_config.__dataclass_fields__.values()
112
+ }
113
+ pytest_options = {k: v for k, v in kwargs.items() if k in config_fields}
114
+ base_options = {k: v for k, v in kwargs.items() if k not in config_fields}
115
+
116
+ # Set pytest-specific options
117
+ self.pytest_config.set_options(**pytest_options)
118
+
119
+ # Forward unrecognized options (like timeout) to base class
120
+ if base_options:
121
+ super().set_options(**base_options)
122
+
123
+ # Set pytest options on the parent class (for backward compatibility)
124
+ options_dict = self.pytest_config.get_options_dict()
125
+ super().set_options(**options_dict)
126
+
127
+ def _build_check_command(
128
+ self,
129
+ files: list[str],
130
+ fix: bool = False,
131
+ ) -> list[str]:
132
+ """Build the pytest command.
133
+
134
+ Backward compatibility method that delegates to build_check_command.
135
+
136
+ Args:
137
+ files: list[str]: List of files to test.
138
+ fix: bool: Ignored for pytest (not applicable).
139
+
140
+ Returns:
141
+ list[str]: List of command arguments.
142
+ """
143
+ cmd, _ = build_check_command(self, files, fix)
144
+ return cmd
145
+
146
+ def _parse_output(
147
+ self,
148
+ output: str,
149
+ return_code: int,
150
+ junitxml_path: str | None = None,
151
+ subprocess_start_time: float | None = None,
152
+ ) -> list:
153
+ """Parse pytest output into issues.
154
+
155
+ Backward compatibility method that delegates to
156
+ parse_pytest_output_with_fallback.
157
+
158
+ Args:
159
+ output: Raw output from pytest.
160
+ return_code: Return code from pytest.
161
+ junitxml_path: Optional path to JUnit XML file (from auto_junitxml).
162
+ subprocess_start_time: Optional Unix timestamp when subprocess started.
163
+
164
+ Returns:
165
+ list: Parsed test failures and errors.
166
+ """
167
+ # Build options dict for parser
168
+ # Use self.options but override junitxml if auto-enabled path provided
169
+ options = self.options.copy() if junitxml_path else self.options
170
+ if junitxml_path:
171
+ options["junitxml"] = junitxml_path
172
+
173
+ return parse_pytest_output_with_fallback(
174
+ output=output,
175
+ return_code=return_code,
176
+ options=options,
177
+ subprocess_start_time=subprocess_start_time,
178
+ )
179
+
180
+ def _handle_special_modes(
181
+ self,
182
+ target_files: list[str],
183
+ ) -> ToolResult | None:
184
+ """Handle special modes that don't run tests.
185
+
186
+ Args:
187
+ target_files: Files or directories to operate on.
188
+
189
+ Returns:
190
+ ToolResult | None: Result if a special mode was handled, None otherwise.
191
+ """
192
+ special_mode = self.pytest_config.get_special_mode()
193
+ if special_mode:
194
+ mode_value = self.pytest_config.get_special_mode_value(special_mode)
195
+
196
+ if special_mode == "list_plugins":
197
+ return handle_list_plugins(self)
198
+ elif special_mode == "check_plugins":
199
+ return handle_check_plugins(self, mode_value)
200
+ elif special_mode == "collect_only":
201
+ return handle_collect_only(self, target_files)
202
+ elif special_mode == "list_fixtures":
203
+ return handle_list_fixtures(self, target_files)
204
+ elif special_mode == "fixture_info":
205
+ return handle_fixture_info(self, mode_value, target_files)
206
+ elif special_mode == "list_markers":
207
+ return handle_list_markers(self)
208
+ elif special_mode == "parametrize_help":
209
+ return handle_parametrize_help(self)
210
+
211
+ return None
212
+
213
+ def check(
214
+ self,
215
+ files: list[str] | None = None,
216
+ paths: list[str] | None = None,
217
+ fix: bool = False,
218
+ ) -> ToolResult:
219
+ """Run pytest on specified files.
220
+
221
+ Args:
222
+ files: list[str] | None: Files to test. If None, uses file patterns.
223
+ paths: list[str] | None: Paths to test. If None, uses "tests" directory.
224
+ fix: bool: Ignored for pytest.
225
+
226
+ Returns:
227
+ ToolResult: Results from pytest execution.
228
+ """
229
+ # Check version requirements
230
+ version_result = self._verify_tool_version()
231
+ if version_result is not None:
232
+ return version_result
233
+ # For pytest, when no specific files are provided, use directories to let
234
+ # pytest discover all tests. This allows running all tests by default.
235
+ target_files = paths or files
236
+ if target_files is None:
237
+ # Default to "tests" directory to match pytest conventions
238
+ target_files = ["tests"]
239
+ elif (
240
+ isinstance(target_files, list)
241
+ and len(target_files) == 1
242
+ and target_files[0] == "."
243
+ ):
244
+ # If just "." is provided, also default to "tests" directory
245
+ target_files = ["tests"]
246
+
247
+ # Handle special modes first (these don't run tests)
248
+ special_result = self._handle_special_modes(target_files)
249
+ if special_result is not None:
250
+ return special_result
251
+
252
+ # Normal test execution
253
+ cmd, auto_junitxml_path = build_check_command(self, target_files, fix)
254
+
255
+ logger.debug(f"Running pytest with command: {' '.join(cmd)}")
256
+ logger.debug(f"Target files: {target_files}")
257
+
258
+ # Prepare test execution using executor
259
+ total_available_tests, docker_test_count, original_docker_env = (
260
+ self.executor.prepare_test_execution(target_files)
261
+ )
262
+ run_docker_tests = self.pytest_config.run_docker_tests or False
263
+
264
+ try:
265
+ # Record start time to filter out stale junitxml files
266
+ import time
267
+
268
+ subprocess_start_time = time.time()
269
+
270
+ # Execute tests using executor
271
+ success, output, return_code = self.executor.execute_tests(cmd)
272
+
273
+ # Parse output using _parse_output method
274
+ # Pass auto_junitxml_path so parser knows where to find report.xml
275
+ # Pass subprocess_start_time to filter out stale junitxml files
276
+ issues = self._parse_output(
277
+ output,
278
+ return_code,
279
+ auto_junitxml_path,
280
+ subprocess_start_time,
281
+ )
282
+
283
+ # Process results using result processor
284
+ summary_data, all_issues = self.result_processor.process_test_results(
285
+ output=output,
286
+ return_code=return_code,
287
+ issues=issues,
288
+ total_available_tests=total_available_tests,
289
+ docker_test_count=docker_test_count,
290
+ run_docker_tests=run_docker_tests,
291
+ )
292
+
293
+ # Build result using result processor
294
+ return self.result_processor.build_result(success, summary_data, all_issues)
295
+
296
+ except subprocess.TimeoutExpired:
297
+ timeout_val = self.options.get("timeout", self._default_timeout)
298
+ return self.error_handler.handle_timeout_error(
299
+ timeout_val,
300
+ cmd,
301
+ initial_count=0,
302
+ )
303
+ except Exception as e:
304
+ return self.error_handler.handle_execution_error(e, cmd)
305
+ finally:
306
+ # Restore original environment state
307
+ self.executor.restore_environment(original_docker_env)
308
+
309
+ def fix(
310
+ self,
311
+ paths: list[str],
312
+ ) -> ToolResult:
313
+ """Fix issues in files.
314
+
315
+ Args:
316
+ paths: list[str]: List of file paths to fix.
317
+
318
+ Raises:
319
+ NotImplementedError: pytest does not support fixing issues.
320
+ """
321
+ if not self.can_fix:
322
+ raise NotImplementedError(f"{self.name} does not support fixing issues")
323
+
324
+ # pytest doesn't fix code, it runs tests
325
+ raise NotImplementedError(
326
+ "pytest does not support fixing issues - it only runs tests",
327
+ )