lintro 0.13.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 +226 -16
- 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/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/tool_name.py +2 -0
- lintro/enums/tool_type.py +2 -0
- lintro/formatters/tools/__init__.py +12 -0
- lintro/formatters/tools/eslint_formatter.py +108 -0
- lintro/formatters/tools/markdownlint_formatter.py +88 -0
- lintro/formatters/tools/pytest_formatter.py +201 -0
- lintro/parsers/__init__.py +69 -9
- 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/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/pytest/__init__.py +21 -0
- lintro/parsers/pytest/pytest_issue.py +28 -0
- lintro/parsers/pytest/pytest_parser.py +483 -0
- lintro/tools/__init__.py +2 -0
- lintro/tools/core/timeout_utils.py +112 -0
- lintro/tools/core/tool_base.py +255 -45
- lintro/tools/core/tool_manager.py +77 -24
- lintro/tools/core/version_requirements.py +482 -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 +23 -7
- lintro/tools/implementations/tool_black.py +236 -29
- lintro/tools/implementations/tool_darglint.py +180 -21
- 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 +313 -26
- lintro/tools/implementations/tool_pytest.py +327 -0
- lintro/tools/implementations/tool_ruff.py +247 -70
- lintro/tools/implementations/tool_yamllint.py +448 -34
- lintro/tools/tool_enum.py +6 -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 +336 -39
- lintro/utils/tool_utils.py +38 -2
- lintro/utils/unified_config.py +926 -0
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/METADATA +131 -29
- lintro-0.17.2.dist-info/RECORD +134 -0
- lintro-0.13.2.dist-info/RECORD +0 -96
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/top_level.txt +0 -0
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
"""Yamllint YAML linter integration."""
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
4
|
+
import fnmatch
|
|
3
5
|
import os
|
|
4
6
|
import subprocess # nosec B404 - used safely with shell disabled
|
|
5
7
|
from dataclasses import dataclass, field
|
|
6
8
|
|
|
9
|
+
import click
|
|
7
10
|
from loguru import logger
|
|
8
11
|
|
|
12
|
+
try:
|
|
13
|
+
import yaml
|
|
14
|
+
except ImportError:
|
|
15
|
+
yaml = None # type: ignore[assignment]
|
|
16
|
+
|
|
9
17
|
from lintro.enums.tool_type import ToolType
|
|
10
18
|
from lintro.enums.yamllint_format import (
|
|
11
19
|
YamllintFormat,
|
|
@@ -120,15 +128,166 @@ class YamllintTool(BaseTool):
|
|
|
120
128
|
options = {k: v for k, v in options.items() if v is not None}
|
|
121
129
|
super().set_options(**options, **kwargs)
|
|
122
130
|
|
|
131
|
+
def _find_yamllint_config(self, search_dir: str | None = None) -> str | None:
|
|
132
|
+
"""Locate yamllint config file if not explicitly provided.
|
|
133
|
+
|
|
134
|
+
Yamllint searches upward from the file's directory to find config files,
|
|
135
|
+
so we do the same to match native behavior.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
search_dir: Directory to start searching from. If None, searches from
|
|
139
|
+
current working directory.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
str | None: Path to config file if found, None otherwise.
|
|
143
|
+
"""
|
|
144
|
+
# If config_file is explicitly set, use it
|
|
145
|
+
if self.options.get("config_file"):
|
|
146
|
+
return self.options.get("config_file")
|
|
147
|
+
|
|
148
|
+
# Check for config files in order of precedence
|
|
149
|
+
config_paths = [
|
|
150
|
+
".yamllint",
|
|
151
|
+
".yamllint.yml",
|
|
152
|
+
".yamllint.yaml",
|
|
153
|
+
]
|
|
154
|
+
# Search upward from search_dir (or cwd) to find config, just like yamllint does
|
|
155
|
+
start_dir = os.path.abspath(search_dir) if search_dir else os.getcwd()
|
|
156
|
+
current_dir = start_dir
|
|
157
|
+
|
|
158
|
+
# Walk upward from the file's directory to find config
|
|
159
|
+
# Stop at filesystem root to avoid infinite loop
|
|
160
|
+
while True:
|
|
161
|
+
for config_name in config_paths:
|
|
162
|
+
config_path = os.path.join(current_dir, config_name)
|
|
163
|
+
if os.path.exists(config_path):
|
|
164
|
+
logger.debug(
|
|
165
|
+
f"[YamllintTool] Found config file: {config_path} "
|
|
166
|
+
f"(searched from {start_dir})",
|
|
167
|
+
)
|
|
168
|
+
return config_path
|
|
169
|
+
|
|
170
|
+
# Move up one directory
|
|
171
|
+
parent_dir = os.path.dirname(current_dir)
|
|
172
|
+
# Stop if we've reached the filesystem root (parent == current)
|
|
173
|
+
if parent_dir == current_dir:
|
|
174
|
+
break
|
|
175
|
+
current_dir = parent_dir
|
|
176
|
+
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
def _load_yamllint_ignore_patterns(
|
|
180
|
+
self,
|
|
181
|
+
config_file: str | None,
|
|
182
|
+
) -> list[str]:
|
|
183
|
+
"""Load ignore patterns from yamllint config file.
|
|
184
|
+
|
|
185
|
+
Yamllint's ignore patterns only work when scanning directories, not when
|
|
186
|
+
individual files are passed. We need to respect these patterns at the
|
|
187
|
+
lintro level by filtering out ignored files before passing them to yamllint.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
config_file: Path to yamllint config file, or None.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
list[str]: List of ignore patterns from the config file.
|
|
194
|
+
"""
|
|
195
|
+
if not config_file or not os.path.exists(config_file):
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
ignore_patterns: list[str] = []
|
|
199
|
+
if yaml is None:
|
|
200
|
+
logger.debug(
|
|
201
|
+
"[YamllintTool] PyYAML not available, cannot parse ignore patterns",
|
|
202
|
+
)
|
|
203
|
+
return ignore_patterns
|
|
204
|
+
try:
|
|
205
|
+
with open(config_file, encoding="utf-8") as f:
|
|
206
|
+
config_data = yaml.safe_load(f)
|
|
207
|
+
if config_data and isinstance(config_data, dict):
|
|
208
|
+
# Check for ignore patterns in line-length rule
|
|
209
|
+
line_length_config = config_data.get("rules", {}).get(
|
|
210
|
+
"line-length",
|
|
211
|
+
{},
|
|
212
|
+
)
|
|
213
|
+
if isinstance(line_length_config, dict):
|
|
214
|
+
ignore_value = line_length_config.get("ignore")
|
|
215
|
+
if ignore_value:
|
|
216
|
+
if isinstance(ignore_value, str):
|
|
217
|
+
# Multi-line string - split by newlines
|
|
218
|
+
ignore_patterns.extend(
|
|
219
|
+
[
|
|
220
|
+
line.strip()
|
|
221
|
+
for line in ignore_value.split("\n")
|
|
222
|
+
if line.strip()
|
|
223
|
+
],
|
|
224
|
+
)
|
|
225
|
+
elif isinstance(ignore_value, list):
|
|
226
|
+
# List of patterns
|
|
227
|
+
ignore_patterns.extend(ignore_value)
|
|
228
|
+
logger.debug(
|
|
229
|
+
f"[YamllintTool] Loaded {len(ignore_patterns)} ignore patterns "
|
|
230
|
+
f"from {config_file}: {ignore_patterns}",
|
|
231
|
+
)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.debug(
|
|
234
|
+
f"[YamllintTool] Failed to load ignore patterns "
|
|
235
|
+
f"from {config_file}: {e}",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return ignore_patterns
|
|
239
|
+
|
|
240
|
+
def _should_ignore_file(
|
|
241
|
+
self,
|
|
242
|
+
file_path: str,
|
|
243
|
+
ignore_patterns: list[str],
|
|
244
|
+
) -> bool:
|
|
245
|
+
"""Check if a file should be ignored based on yamllint ignore patterns.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
file_path: Path to the file to check.
|
|
249
|
+
ignore_patterns: List of ignore patterns from yamllint config.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
bool: True if the file should be ignored, False otherwise.
|
|
253
|
+
"""
|
|
254
|
+
if not ignore_patterns:
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
# Normalize path separators for cross-platform compatibility
|
|
258
|
+
normalized_path: str = file_path.replace("\\", "/")
|
|
259
|
+
|
|
260
|
+
for pattern in ignore_patterns:
|
|
261
|
+
pattern = pattern.strip()
|
|
262
|
+
if not pattern:
|
|
263
|
+
continue
|
|
264
|
+
# Yamllint ignore patterns are directory paths (like .github/workflows/)
|
|
265
|
+
# Check if the pattern appears as a directory path in the file path
|
|
266
|
+
# Handle both relative paths (./.github/workflows/) and absolute paths
|
|
267
|
+
# Pattern should match if it appears as /pattern/ or at the start
|
|
268
|
+
if normalized_path.startswith(pattern):
|
|
269
|
+
return True
|
|
270
|
+
# Check if pattern appears as a directory in the path
|
|
271
|
+
# (handles ./prefix and absolute paths)
|
|
272
|
+
if f"/{pattern}" in normalized_path:
|
|
273
|
+
return True
|
|
274
|
+
# Also try glob matching for patterns with wildcards
|
|
275
|
+
if fnmatch.fnmatch(normalized_path, pattern):
|
|
276
|
+
return True
|
|
277
|
+
|
|
278
|
+
return False
|
|
279
|
+
|
|
123
280
|
def _build_command(self) -> list[str]:
|
|
124
281
|
"""Build the yamllint command.
|
|
125
282
|
|
|
126
283
|
Returns:
|
|
127
284
|
list[str]: Command arguments for yamllint.
|
|
128
285
|
"""
|
|
129
|
-
cmd: list[str] =
|
|
286
|
+
cmd: list[str] = self._get_executable_command("yamllint")
|
|
130
287
|
format_option: str = self.options.get("format", YAMLLINT_FORMATS[0])
|
|
131
288
|
cmd.extend(["--format", format_option])
|
|
289
|
+
# Note: Config file discovery happens per-file in _process_yaml_file
|
|
290
|
+
# since yamllint discovers configs relative to each file
|
|
132
291
|
config_file: str | None = self.options.get("config_file")
|
|
133
292
|
if config_file:
|
|
134
293
|
cmd.extend(["--config-file", config_file])
|
|
@@ -143,6 +302,162 @@ class YamllintTool(BaseTool):
|
|
|
143
302
|
cmd.append("--no-warnings")
|
|
144
303
|
return cmd
|
|
145
304
|
|
|
305
|
+
def _process_yaml_file(
|
|
306
|
+
self,
|
|
307
|
+
file_path: str,
|
|
308
|
+
timeout: int,
|
|
309
|
+
) -> tuple[int, list, bool, bool, bool, bool]:
|
|
310
|
+
"""Process a single YAML file with yamllint.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
file_path: Path to the YAML file to process.
|
|
314
|
+
timeout: Timeout in seconds for the subprocess call.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
tuple containing:
|
|
318
|
+
- issues_count: Number of issues found
|
|
319
|
+
- issues_list: List of parsed issues
|
|
320
|
+
- skipped_flag: True if file was skipped due to timeout
|
|
321
|
+
- execution_failure_flag: True if there was an execution failure
|
|
322
|
+
- success_flag: False if issues were found
|
|
323
|
+
- should_continue: True if file should be silently skipped
|
|
324
|
+
(missing file)
|
|
325
|
+
"""
|
|
326
|
+
# Use absolute path; run with the file's parent as cwd so that
|
|
327
|
+
# yamllint discovers any local .yamllint config beside the file.
|
|
328
|
+
abs_file: str = os.path.abspath(file_path)
|
|
329
|
+
file_cwd: str = self.get_cwd(paths=[abs_file])
|
|
330
|
+
file_dir: str = os.path.dirname(abs_file)
|
|
331
|
+
# Build command and discover config relative to file's directory
|
|
332
|
+
cmd: list[str] = self._get_executable_command("yamllint")
|
|
333
|
+
format_option: str = self.options.get("format", YAMLLINT_FORMATS[0])
|
|
334
|
+
cmd.extend(["--format", format_option])
|
|
335
|
+
# Discover config file relative to the file being checked
|
|
336
|
+
config_file: str | None = self._find_yamllint_config(search_dir=file_dir)
|
|
337
|
+
if config_file:
|
|
338
|
+
# Ensure config file path is absolute so yamllint can find it
|
|
339
|
+
# even when cwd is a subdirectory
|
|
340
|
+
abs_config_file = os.path.abspath(config_file)
|
|
341
|
+
cmd.extend(["--config-file", abs_config_file])
|
|
342
|
+
logger.debug(
|
|
343
|
+
f"[YamllintTool] Using config file: {abs_config_file} "
|
|
344
|
+
f"(original: {config_file})",
|
|
345
|
+
)
|
|
346
|
+
config_data: str | None = self.options.get("config_data")
|
|
347
|
+
if config_data:
|
|
348
|
+
cmd.extend(["--config-data", config_data])
|
|
349
|
+
if self.options.get("strict", False):
|
|
350
|
+
cmd.append("--strict")
|
|
351
|
+
if self.options.get("relaxed", False):
|
|
352
|
+
cmd.append("--relaxed")
|
|
353
|
+
if self.options.get("no_warnings", False):
|
|
354
|
+
cmd.append("--no-warnings")
|
|
355
|
+
cmd.append(abs_file)
|
|
356
|
+
logger.debug(
|
|
357
|
+
f"[YamllintTool] Processing file: {abs_file} (cwd={file_cwd})",
|
|
358
|
+
)
|
|
359
|
+
logger.debug(f"[YamllintTool] Command: {' '.join(cmd)}")
|
|
360
|
+
try:
|
|
361
|
+
success, output = self._run_subprocess(
|
|
362
|
+
cmd=cmd,
|
|
363
|
+
timeout=timeout,
|
|
364
|
+
cwd=file_cwd,
|
|
365
|
+
)
|
|
366
|
+
issues = parse_yamllint_output(output=output)
|
|
367
|
+
issues_count: int = len(issues)
|
|
368
|
+
# Yamllint returns 1 on errors/warnings unless --no-warnings/relaxed
|
|
369
|
+
# Use parsed issues to determine success and counts reliably.
|
|
370
|
+
# Honor subprocess exit status: if it failed and we have no parsed
|
|
371
|
+
# issues, that's an execution failure (invalid config, crash,
|
|
372
|
+
# missing dependency, etc.)
|
|
373
|
+
# The 'success' flag from _run_subprocess reflects the subprocess
|
|
374
|
+
# exit status
|
|
375
|
+
success_flag: bool = success and issues_count == 0
|
|
376
|
+
# Execution failure occurs when subprocess failed but no lint issues
|
|
377
|
+
# were found
|
|
378
|
+
# This distinguishes execution errors from lint findings
|
|
379
|
+
execution_failure = not success and issues_count == 0
|
|
380
|
+
# Log execution failures with error details for debugging
|
|
381
|
+
if execution_failure and output:
|
|
382
|
+
logger.debug(
|
|
383
|
+
f"Yamllint execution failure for {file_path}: {output}",
|
|
384
|
+
)
|
|
385
|
+
return issues_count, issues, False, execution_failure, success_flag, False
|
|
386
|
+
except subprocess.TimeoutExpired:
|
|
387
|
+
# Count timeout as an execution failure
|
|
388
|
+
return 0, [], True, True, False, False
|
|
389
|
+
except Exception as e:
|
|
390
|
+
# Suppress missing file noise in console output; keep as debug
|
|
391
|
+
err_msg = str(e)
|
|
392
|
+
if "No such file or directory" in err_msg:
|
|
393
|
+
# treat as skipped/missing silently for user; do not fail run
|
|
394
|
+
return 0, [], False, False, True, True
|
|
395
|
+
# Log execution errors for debugging while keeping user output clean
|
|
396
|
+
logger.debug(
|
|
397
|
+
f"Yamllint execution error for {file_path}: {err_msg}",
|
|
398
|
+
)
|
|
399
|
+
# Do not add raw errors to user-facing output; mark failure only
|
|
400
|
+
# Count execution errors as failures
|
|
401
|
+
return 0, [], False, True, False, False
|
|
402
|
+
|
|
403
|
+
def _process_yaml_file_result(
|
|
404
|
+
self,
|
|
405
|
+
issues_count: int,
|
|
406
|
+
issues: list,
|
|
407
|
+
skipped_flag: bool,
|
|
408
|
+
execution_failure_flag: bool,
|
|
409
|
+
success_flag: bool,
|
|
410
|
+
file_path: str,
|
|
411
|
+
all_success: bool,
|
|
412
|
+
all_issues: list,
|
|
413
|
+
skipped_files: list[str],
|
|
414
|
+
timeout_skipped_count: int,
|
|
415
|
+
other_execution_failures: int,
|
|
416
|
+
total_issues: int,
|
|
417
|
+
) -> tuple[bool, list, list[str], int, int, int]:
|
|
418
|
+
"""Process a single file's result and update accumulators.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
issues_count: Number of issues found in the file.
|
|
422
|
+
issues: List of parsed issues.
|
|
423
|
+
skipped_flag: True if file was skipped due to timeout.
|
|
424
|
+
execution_failure_flag: True if there was an execution failure.
|
|
425
|
+
success_flag: False if issues were found.
|
|
426
|
+
file_path: Path to the file being processed.
|
|
427
|
+
all_success: Current overall success flag.
|
|
428
|
+
all_issues: Current list of all issues.
|
|
429
|
+
skipped_files: Current list of skipped files.
|
|
430
|
+
timeout_skipped_count: Current count of timeout skips.
|
|
431
|
+
other_execution_failures: Current count of execution failures.
|
|
432
|
+
total_issues: Current total issue count.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
tuple containing updated accumulators:
|
|
436
|
+
(all_success, all_issues, skipped_files,
|
|
437
|
+
timeout_skipped_count, other_execution_failures, total_issues)
|
|
438
|
+
"""
|
|
439
|
+
if not success_flag:
|
|
440
|
+
all_success = False
|
|
441
|
+
total_issues += issues_count
|
|
442
|
+
if issues:
|
|
443
|
+
all_issues.extend(issues)
|
|
444
|
+
if skipped_flag:
|
|
445
|
+
skipped_files.append(file_path)
|
|
446
|
+
all_success = False
|
|
447
|
+
timeout_skipped_count += 1
|
|
448
|
+
elif execution_failure_flag:
|
|
449
|
+
# Only count execution failures if not already counted as skipped
|
|
450
|
+
all_success = False
|
|
451
|
+
other_execution_failures += 1
|
|
452
|
+
return (
|
|
453
|
+
all_success,
|
|
454
|
+
all_issues,
|
|
455
|
+
skipped_files,
|
|
456
|
+
timeout_skipped_count,
|
|
457
|
+
other_execution_failures,
|
|
458
|
+
total_issues,
|
|
459
|
+
)
|
|
460
|
+
|
|
146
461
|
def check(
|
|
147
462
|
self,
|
|
148
463
|
paths: list[str],
|
|
@@ -155,6 +470,11 @@ class YamllintTool(BaseTool):
|
|
|
155
470
|
Returns:
|
|
156
471
|
ToolResult: Result of the check operation.
|
|
157
472
|
"""
|
|
473
|
+
# Check version requirements
|
|
474
|
+
version_result = self._verify_tool_version()
|
|
475
|
+
if version_result is not None:
|
|
476
|
+
return version_result
|
|
477
|
+
|
|
158
478
|
self._validate_paths(paths=paths)
|
|
159
479
|
if not paths:
|
|
160
480
|
return ToolResult(
|
|
@@ -169,51 +489,145 @@ class YamllintTool(BaseTool):
|
|
|
169
489
|
exclude_patterns=self.exclude_patterns,
|
|
170
490
|
include_venv=self.include_venv,
|
|
171
491
|
)
|
|
492
|
+
logger.debug(
|
|
493
|
+
f"[YamllintTool] Discovered {len(yaml_files)} files matching patterns: "
|
|
494
|
+
f"{self.config.file_patterns}",
|
|
495
|
+
)
|
|
496
|
+
logger.debug(
|
|
497
|
+
f"[YamllintTool] Exclude patterns applied: {self.exclude_patterns}",
|
|
498
|
+
)
|
|
499
|
+
if yaml_files:
|
|
500
|
+
logger.debug(
|
|
501
|
+
f"[YamllintTool] Files to check (first 10): {yaml_files[:10]}",
|
|
502
|
+
)
|
|
503
|
+
# Check for yamllint config files
|
|
504
|
+
config_paths = [
|
|
505
|
+
".yamllint",
|
|
506
|
+
".yamllint.yml",
|
|
507
|
+
".yamllint.yaml",
|
|
508
|
+
"setup.cfg",
|
|
509
|
+
"pyproject.toml",
|
|
510
|
+
]
|
|
511
|
+
found_config = None
|
|
512
|
+
config_file_option = self.options.get("config_file")
|
|
513
|
+
if config_file_option:
|
|
514
|
+
found_config = os.path.abspath(config_file_option)
|
|
515
|
+
logger.debug(
|
|
516
|
+
f"[YamllintTool] Using explicit config file: {found_config}",
|
|
517
|
+
)
|
|
518
|
+
else:
|
|
519
|
+
for config_name in config_paths:
|
|
520
|
+
config_path = os.path.abspath(config_name)
|
|
521
|
+
if os.path.exists(config_path):
|
|
522
|
+
found_config = config_path
|
|
523
|
+
logger.debug(
|
|
524
|
+
f"[YamllintTool] Found config file: {config_path}",
|
|
525
|
+
)
|
|
526
|
+
break
|
|
527
|
+
if not found_config:
|
|
528
|
+
logger.debug(
|
|
529
|
+
"[YamllintTool] No yamllint config file found (using defaults)",
|
|
530
|
+
)
|
|
531
|
+
# Load ignore patterns from yamllint config and filter files
|
|
532
|
+
ignore_patterns: list[str] = self._load_yamllint_ignore_patterns(
|
|
533
|
+
config_file=found_config,
|
|
534
|
+
)
|
|
535
|
+
if ignore_patterns:
|
|
536
|
+
original_count = len(yaml_files)
|
|
537
|
+
yaml_files = [
|
|
538
|
+
f
|
|
539
|
+
for f in yaml_files
|
|
540
|
+
if not self._should_ignore_file(
|
|
541
|
+
file_path=f,
|
|
542
|
+
ignore_patterns=ignore_patterns,
|
|
543
|
+
)
|
|
544
|
+
]
|
|
545
|
+
filtered_count = original_count - len(yaml_files)
|
|
546
|
+
if filtered_count > 0:
|
|
547
|
+
logger.debug(
|
|
548
|
+
f"[YamllintTool] Filtered out {filtered_count} files based on "
|
|
549
|
+
f"yamllint ignore patterns: {ignore_patterns}",
|
|
550
|
+
)
|
|
172
551
|
logger.debug(f"Files to check: {yaml_files}")
|
|
173
552
|
timeout: int = self.options.get("timeout", YAMLLINT_DEFAULT_TIMEOUT)
|
|
174
553
|
# Aggregate parsed issues across files and rely on table renderers upstream
|
|
175
554
|
all_success: bool = True
|
|
176
555
|
all_issues: list = []
|
|
177
556
|
skipped_files: list[str] = []
|
|
557
|
+
timeout_skipped_count: int = 0
|
|
558
|
+
other_execution_failures: int = 0
|
|
178
559
|
total_issues: int = 0
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
# Suppress missing file noise in console output; keep as debug
|
|
204
|
-
err_msg = str(e)
|
|
205
|
-
if "No such file or directory" in err_msg:
|
|
206
|
-
# treat as skipped/missing silently for user; do not fail run
|
|
560
|
+
|
|
561
|
+
# Show progress bar only when processing multiple files
|
|
562
|
+
if len(yaml_files) >= 2:
|
|
563
|
+
files_to_iterate = click.progressbar(
|
|
564
|
+
yaml_files,
|
|
565
|
+
label="Processing files",
|
|
566
|
+
bar_template="%(label)s %(info)s",
|
|
567
|
+
)
|
|
568
|
+
context_mgr = files_to_iterate
|
|
569
|
+
else:
|
|
570
|
+
files_to_iterate = yaml_files
|
|
571
|
+
context_mgr = contextlib.nullcontext()
|
|
572
|
+
|
|
573
|
+
with context_mgr:
|
|
574
|
+
for file_path in files_to_iterate:
|
|
575
|
+
(
|
|
576
|
+
issues_count,
|
|
577
|
+
issues,
|
|
578
|
+
skipped_flag,
|
|
579
|
+
execution_failure_flag,
|
|
580
|
+
success_flag,
|
|
581
|
+
should_continue,
|
|
582
|
+
) = self._process_yaml_file(file_path=file_path, timeout=timeout)
|
|
583
|
+
if should_continue:
|
|
207
584
|
continue
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
585
|
+
(
|
|
586
|
+
all_success,
|
|
587
|
+
all_issues,
|
|
588
|
+
skipped_files,
|
|
589
|
+
timeout_skipped_count,
|
|
590
|
+
other_execution_failures,
|
|
591
|
+
total_issues,
|
|
592
|
+
) = self._process_yaml_file_result(
|
|
593
|
+
issues_count=issues_count,
|
|
594
|
+
issues=issues,
|
|
595
|
+
skipped_flag=skipped_flag,
|
|
596
|
+
execution_failure_flag=execution_failure_flag,
|
|
597
|
+
success_flag=success_flag,
|
|
598
|
+
file_path=file_path,
|
|
599
|
+
all_success=all_success,
|
|
600
|
+
all_issues=all_issues,
|
|
601
|
+
skipped_files=skipped_files,
|
|
602
|
+
timeout_skipped_count=timeout_skipped_count,
|
|
603
|
+
other_execution_failures=other_execution_failures,
|
|
604
|
+
total_issues=total_issues,
|
|
605
|
+
)
|
|
606
|
+
# Build output message if there are skipped files or execution failures
|
|
607
|
+
output: str | None = None
|
|
608
|
+
if timeout_skipped_count > 0 or other_execution_failures > 0:
|
|
609
|
+
output_lines: list[str] = []
|
|
610
|
+
if timeout_skipped_count > 0:
|
|
611
|
+
output_lines.append(
|
|
612
|
+
f"Skipped {timeout_skipped_count} file(s) due to timeout "
|
|
613
|
+
f"({timeout}s limit exceeded):",
|
|
614
|
+
)
|
|
615
|
+
for file in skipped_files:
|
|
616
|
+
output_lines.append(f" - {file}")
|
|
617
|
+
if other_execution_failures > 0:
|
|
618
|
+
output_lines.append(
|
|
619
|
+
f"Failed to process {other_execution_failures} file(s) "
|
|
620
|
+
"due to execution errors",
|
|
621
|
+
)
|
|
622
|
+
output = "\n".join(output_lines) if output_lines else None
|
|
623
|
+
# Include execution failures (timeouts/errors) in issues_count
|
|
624
|
+
# to properly reflect tool failure status
|
|
625
|
+
total_issues_with_failures = total_issues + other_execution_failures
|
|
212
626
|
return ToolResult(
|
|
213
627
|
name=self.name,
|
|
214
628
|
success=all_success,
|
|
215
629
|
output=output,
|
|
216
|
-
issues_count=
|
|
630
|
+
issues_count=total_issues_with_failures,
|
|
217
631
|
issues=all_issues,
|
|
218
632
|
)
|
|
219
633
|
|
lintro/tools/tool_enum.py
CHANGED
|
@@ -6,8 +6,11 @@ from lintro.tools.implementations.tool_actionlint import ActionlintTool
|
|
|
6
6
|
from lintro.tools.implementations.tool_bandit import BanditTool
|
|
7
7
|
from lintro.tools.implementations.tool_black import BlackTool
|
|
8
8
|
from lintro.tools.implementations.tool_darglint import DarglintTool
|
|
9
|
+
from lintro.tools.implementations.tool_eslint import EslintTool
|
|
9
10
|
from lintro.tools.implementations.tool_hadolint import HadolintTool
|
|
11
|
+
from lintro.tools.implementations.tool_markdownlint import MarkdownlintTool
|
|
10
12
|
from lintro.tools.implementations.tool_prettier import PrettierTool
|
|
13
|
+
from lintro.tools.implementations.tool_pytest import PytestTool
|
|
11
14
|
from lintro.tools.implementations.tool_ruff import RuffTool
|
|
12
15
|
from lintro.tools.implementations.tool_yamllint import YamllintTool
|
|
13
16
|
|
|
@@ -17,8 +20,11 @@ class ToolEnum(Enum):
|
|
|
17
20
|
|
|
18
21
|
BLACK = BlackTool
|
|
19
22
|
DARGLINT = DarglintTool
|
|
23
|
+
ESLINT = EslintTool
|
|
20
24
|
HADOLINT = HadolintTool
|
|
25
|
+
MARKDOWNLINT = MarkdownlintTool
|
|
21
26
|
PRETTIER = PrettierTool
|
|
27
|
+
PYTEST = PytestTool
|
|
22
28
|
RUFF = RuffTool
|
|
23
29
|
YAMLLINT = YamllintTool
|
|
24
30
|
ACTIONLINT = ActionlintTool
|
lintro/utils/config.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"""Project configuration helpers for Lintro.
|
|
2
2
|
|
|
3
|
+
This module provides backward-compatible access to configuration functions.
|
|
4
|
+
The canonical implementation is in unified_config.py.
|
|
5
|
+
|
|
3
6
|
Reads configuration from `pyproject.toml` under the `[tool.lintro]` table.
|
|
4
7
|
Allows tool-specific defaults via `[tool.lintro.<tool>]` (e.g., `[tool.lintro.ruff]`).
|
|
5
8
|
"""
|
|
@@ -10,8 +13,44 @@ import tomllib
|
|
|
10
13
|
from pathlib import Path
|
|
11
14
|
from typing import Any
|
|
12
15
|
|
|
16
|
+
# Re-export from unified_config for backward compatibility
|
|
17
|
+
from lintro.utils.unified_config import (
|
|
18
|
+
get_effective_line_length,
|
|
19
|
+
load_lintro_global_config,
|
|
20
|
+
load_lintro_tool_config,
|
|
21
|
+
)
|
|
22
|
+
from lintro.utils.unified_config import (
|
|
23
|
+
validate_config_consistency as validate_line_length_consistency,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_central_line_length() -> int | None:
|
|
28
|
+
"""Get the central line length configuration.
|
|
29
|
+
|
|
30
|
+
Backward-compatible wrapper that returns the effective line length
|
|
31
|
+
for Ruff (which serves as the source of truth).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Line length value if configured, None otherwise.
|
|
35
|
+
"""
|
|
36
|
+
return get_effective_line_length("ruff")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"get_central_line_length",
|
|
41
|
+
"load_lintro_global_config",
|
|
42
|
+
"load_lintro_tool_config",
|
|
43
|
+
"load_post_checks_config",
|
|
44
|
+
"validate_line_length_consistency",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _load_lintro_section() -> dict[str, Any]:
|
|
49
|
+
"""Load Lintro configuration section from pyproject.toml.
|
|
13
50
|
|
|
14
|
-
|
|
51
|
+
Returns:
|
|
52
|
+
Dict containing [tool.lintro] section or empty dict.
|
|
53
|
+
"""
|
|
15
54
|
pyproject_path = Path("pyproject.toml")
|
|
16
55
|
if not pyproject_path.exists():
|
|
17
56
|
return {}
|
|
@@ -23,22 +62,6 @@ def _load_pyproject() -> dict[str, Any]:
|
|
|
23
62
|
return {}
|
|
24
63
|
|
|
25
64
|
|
|
26
|
-
def load_lintro_tool_config(tool_name: str) -> dict[str, Any]:
|
|
27
|
-
"""Load tool-specific config from pyproject.
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
tool_name: Tool name (e.g., "ruff").
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
A dict of options for the given tool, or an empty dict if none.
|
|
34
|
-
"""
|
|
35
|
-
cfg = _load_pyproject()
|
|
36
|
-
section = cfg.get(tool_name, {})
|
|
37
|
-
if isinstance(section, dict):
|
|
38
|
-
return section
|
|
39
|
-
return {}
|
|
40
|
-
|
|
41
|
-
|
|
42
65
|
def load_post_checks_config() -> dict[str, Any]:
|
|
43
66
|
"""Load post-checks configuration from pyproject.
|
|
44
67
|
|
|
@@ -48,7 +71,7 @@ def load_post_checks_config() -> dict[str, Any]:
|
|
|
48
71
|
- tools: list[str]
|
|
49
72
|
- enforce_failure: bool
|
|
50
73
|
"""
|
|
51
|
-
cfg =
|
|
74
|
+
cfg = _load_lintro_section()
|
|
52
75
|
section = cfg.get("post_checks", {})
|
|
53
76
|
if isinstance(section, dict):
|
|
54
77
|
return section
|