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.
- lintro/__init__.py +1 -1
- lintro/cli.py +230 -14
- lintro/cli_utils/commands/__init__.py +8 -1
- lintro/cli_utils/commands/check.py +1 -0
- lintro/cli_utils/commands/config.py +325 -0
- lintro/cli_utils/commands/format.py +2 -2
- lintro/cli_utils/commands/init.py +361 -0
- lintro/cli_utils/commands/list_tools.py +180 -42
- lintro/cli_utils/commands/test.py +316 -0
- lintro/cli_utils/commands/versions.py +81 -0
- lintro/config/__init__.py +62 -0
- lintro/config/config_loader.py +420 -0
- lintro/config/lintro_config.py +189 -0
- lintro/config/tool_config_generator.py +403 -0
- lintro/enums/__init__.py +1 -0
- lintro/enums/darglint_strictness.py +10 -0
- lintro/enums/hadolint_enums.py +22 -0
- lintro/enums/tool_name.py +2 -0
- lintro/enums/tool_type.py +2 -0
- lintro/enums/yamllint_format.py +11 -0
- lintro/exceptions/__init__.py +1 -0
- lintro/formatters/__init__.py +1 -0
- lintro/formatters/core/__init__.py +1 -0
- lintro/formatters/core/output_style.py +11 -0
- lintro/formatters/core/table_descriptor.py +8 -0
- lintro/formatters/styles/csv.py +2 -0
- lintro/formatters/styles/grid.py +2 -0
- lintro/formatters/styles/html.py +2 -0
- lintro/formatters/styles/json.py +2 -0
- lintro/formatters/styles/markdown.py +2 -0
- lintro/formatters/styles/plain.py +2 -0
- lintro/formatters/tools/__init__.py +12 -0
- lintro/formatters/tools/black_formatter.py +27 -5
- lintro/formatters/tools/darglint_formatter.py +16 -1
- lintro/formatters/tools/eslint_formatter.py +108 -0
- lintro/formatters/tools/hadolint_formatter.py +13 -0
- lintro/formatters/tools/markdownlint_formatter.py +88 -0
- lintro/formatters/tools/prettier_formatter.py +15 -0
- lintro/formatters/tools/pytest_formatter.py +201 -0
- lintro/formatters/tools/ruff_formatter.py +26 -5
- lintro/formatters/tools/yamllint_formatter.py +14 -1
- lintro/models/__init__.py +1 -0
- lintro/models/core/__init__.py +1 -0
- lintro/models/core/tool_config.py +11 -7
- lintro/parsers/__init__.py +69 -9
- lintro/parsers/actionlint/actionlint_parser.py +1 -1
- lintro/parsers/bandit/__init__.py +6 -0
- lintro/parsers/bandit/bandit_issue.py +49 -0
- lintro/parsers/bandit/bandit_parser.py +99 -0
- lintro/parsers/black/black_issue.py +4 -0
- lintro/parsers/darglint/__init__.py +1 -0
- lintro/parsers/darglint/darglint_issue.py +11 -0
- lintro/parsers/eslint/__init__.py +6 -0
- lintro/parsers/eslint/eslint_issue.py +26 -0
- lintro/parsers/eslint/eslint_parser.py +63 -0
- lintro/parsers/markdownlint/__init__.py +6 -0
- lintro/parsers/markdownlint/markdownlint_issue.py +22 -0
- lintro/parsers/markdownlint/markdownlint_parser.py +113 -0
- lintro/parsers/prettier/__init__.py +1 -0
- lintro/parsers/prettier/prettier_issue.py +12 -0
- lintro/parsers/prettier/prettier_parser.py +1 -1
- lintro/parsers/pytest/__init__.py +21 -0
- lintro/parsers/pytest/pytest_issue.py +28 -0
- lintro/parsers/pytest/pytest_parser.py +483 -0
- lintro/parsers/ruff/ruff_parser.py +6 -2
- lintro/parsers/yamllint/__init__.py +1 -0
- lintro/tools/__init__.py +3 -1
- lintro/tools/core/__init__.py +1 -0
- lintro/tools/core/timeout_utils.py +112 -0
- lintro/tools/core/tool_base.py +286 -50
- lintro/tools/core/tool_manager.py +77 -24
- lintro/tools/core/version_requirements.py +482 -0
- lintro/tools/implementations/__init__.py +1 -0
- lintro/tools/implementations/pytest/pytest_command_builder.py +311 -0
- lintro/tools/implementations/pytest/pytest_config.py +200 -0
- lintro/tools/implementations/pytest/pytest_error_handler.py +128 -0
- lintro/tools/implementations/pytest/pytest_executor.py +122 -0
- lintro/tools/implementations/pytest/pytest_handlers.py +375 -0
- lintro/tools/implementations/pytest/pytest_option_validators.py +212 -0
- lintro/tools/implementations/pytest/pytest_output_processor.py +408 -0
- lintro/tools/implementations/pytest/pytest_result_processor.py +113 -0
- lintro/tools/implementations/pytest/pytest_utils.py +697 -0
- lintro/tools/implementations/tool_actionlint.py +106 -16
- lintro/tools/implementations/tool_bandit.py +34 -29
- lintro/tools/implementations/tool_black.py +236 -29
- lintro/tools/implementations/tool_darglint.py +183 -22
- lintro/tools/implementations/tool_eslint.py +374 -0
- lintro/tools/implementations/tool_hadolint.py +94 -25
- lintro/tools/implementations/tool_markdownlint.py +354 -0
- lintro/tools/implementations/tool_prettier.py +317 -24
- lintro/tools/implementations/tool_pytest.py +327 -0
- lintro/tools/implementations/tool_ruff.py +278 -84
- lintro/tools/implementations/tool_yamllint.py +448 -34
- lintro/tools/tool_enum.py +8 -0
- lintro/utils/__init__.py +1 -0
- lintro/utils/ascii_normalize_cli.py +5 -0
- lintro/utils/config.py +41 -18
- lintro/utils/console_logger.py +211 -25
- lintro/utils/path_utils.py +42 -0
- lintro/utils/tool_executor.py +339 -45
- lintro/utils/tool_utils.py +51 -24
- lintro/utils/unified_config.py +926 -0
- {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/METADATA +172 -30
- lintro-0.17.2.dist-info/RECORD +134 -0
- lintro-0.6.2.dist-info/RECORD +0 -96
- {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
- {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
- {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
- {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
|
+
)
|