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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Shared timeout handling utilities for tool implementations.
|
|
2
|
+
|
|
3
|
+
This module provides standardized timeout handling across different tools,
|
|
4
|
+
ensuring consistent behavior and error messages for subprocess timeouts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess # nosec B404 - used safely with shell disabled
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_subprocess_with_timeout(
|
|
14
|
+
tool,
|
|
15
|
+
cmd: list[str],
|
|
16
|
+
timeout: int | None = None,
|
|
17
|
+
cwd: str | None = None,
|
|
18
|
+
tool_name: str | None = None,
|
|
19
|
+
) -> tuple[bool, str]:
|
|
20
|
+
"""Run a subprocess command with timeout handling.
|
|
21
|
+
|
|
22
|
+
This is a wrapper around tool._run_subprocess that provides consistent
|
|
23
|
+
timeout error handling and messaging across different tools.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
tool: Tool instance with _run_subprocess method.
|
|
27
|
+
cmd: Command to run.
|
|
28
|
+
timeout: Timeout in seconds. If None, uses tool's default timeout.
|
|
29
|
+
cwd: Working directory for command execution.
|
|
30
|
+
tool_name: Name of the tool for error messages. If None, uses tool.name.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
tuple[bool, str]: (success, output) where success is True if command
|
|
34
|
+
succeeded without timeout, and output contains command output or
|
|
35
|
+
timeout error message.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
TimeoutExpired: If command times out (re-raised from subprocess).
|
|
39
|
+
"""
|
|
40
|
+
tool_name = tool_name or tool.name
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
return tool._run_subprocess(cmd=cmd, timeout=timeout, cwd=cwd)
|
|
44
|
+
except subprocess.TimeoutExpired as e:
|
|
45
|
+
# Re-raise with more context for the calling tool
|
|
46
|
+
actual_timeout = timeout or tool.options.get("timeout", tool._default_timeout)
|
|
47
|
+
timeout_msg = (
|
|
48
|
+
f"{tool_name} execution timed out ({actual_timeout}s limit exceeded).\n\n"
|
|
49
|
+
"This may indicate:\n"
|
|
50
|
+
" - Large codebase taking too long to process\n"
|
|
51
|
+
" - Need to increase timeout via --tool-options timeout=N\n"
|
|
52
|
+
" - Command hanging due to external dependencies\n"
|
|
53
|
+
)
|
|
54
|
+
logger.warning(timeout_msg)
|
|
55
|
+
|
|
56
|
+
# Create a new TimeoutExpired with enhanced message
|
|
57
|
+
raise subprocess.TimeoutExpired(
|
|
58
|
+
cmd=cmd,
|
|
59
|
+
timeout=actual_timeout,
|
|
60
|
+
output=timeout_msg,
|
|
61
|
+
) from e
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_timeout_value(tool, default_timeout: int | None = None) -> int:
|
|
65
|
+
"""Get timeout value from tool options with fallback to default.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
tool: Tool instance with options.
|
|
69
|
+
default_timeout: Default timeout if not specified in options.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
int: Timeout value in seconds.
|
|
73
|
+
"""
|
|
74
|
+
if default_timeout is None:
|
|
75
|
+
default_timeout = getattr(tool, "_default_timeout", 300)
|
|
76
|
+
|
|
77
|
+
return tool.options.get("timeout", default_timeout)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def create_timeout_result(
|
|
81
|
+
tool,
|
|
82
|
+
timeout: int,
|
|
83
|
+
cmd: list[str] | None = None,
|
|
84
|
+
tool_name: str | None = None,
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
"""Create a standardized timeout result dictionary.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
tool: Tool instance.
|
|
90
|
+
timeout: Timeout value that was exceeded.
|
|
91
|
+
cmd: Optional command that timed out.
|
|
92
|
+
tool_name: Optional tool name override.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
dict: Result dictionary with timeout information.
|
|
96
|
+
"""
|
|
97
|
+
tool_name = tool_name or tool.name
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"success": False,
|
|
101
|
+
"output": (
|
|
102
|
+
f"{tool_name} execution timed out ({timeout}s limit exceeded).\n\n"
|
|
103
|
+
"This may indicate:\n"
|
|
104
|
+
" - Large codebase taking too long to process\n"
|
|
105
|
+
" - Need to increase timeout via --tool-options timeout=N\n"
|
|
106
|
+
" - Command hanging due to external dependencies\n"
|
|
107
|
+
),
|
|
108
|
+
"issues_count": 1, # Count timeout as execution failure
|
|
109
|
+
"issues": [],
|
|
110
|
+
"timed_out": True,
|
|
111
|
+
"timeout_seconds": timeout,
|
|
112
|
+
}
|
lintro/tools/core/tool_base.py
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
"""Base core implementation for Lintro."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import os
|
|
4
6
|
import shutil
|
|
5
7
|
import subprocess # nosec B404 - subprocess used safely with shell=False
|
|
6
8
|
from abc import ABC, abstractmethod
|
|
7
9
|
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
8
11
|
|
|
9
12
|
from loguru import logger
|
|
10
13
|
|
|
14
|
+
from lintro.config import LintroConfig
|
|
11
15
|
from lintro.enums.tool_type import ToolType
|
|
12
16
|
from lintro.models.core.tool import ToolConfig, ToolResult
|
|
17
|
+
from lintro.utils.path_utils import find_lintro_ignore
|
|
13
18
|
|
|
14
19
|
# Constants for default values
|
|
15
20
|
DEFAULT_TIMEOUT: int = 30
|
|
@@ -91,6 +96,17 @@ class BaseTool(ABC):
|
|
|
91
96
|
if not isinstance(self.config.tool_type, ToolType):
|
|
92
97
|
raise ValueError("Tool tool_type must be a ToolType instance")
|
|
93
98
|
|
|
99
|
+
def _find_lintro_ignore(self) -> str | None:
|
|
100
|
+
"""Find .lintro-ignore file by searching upward from current directory.
|
|
101
|
+
|
|
102
|
+
Uses the shared utility function to ensure consistent behavior.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
str | None: Path to .lintro-ignore file if found, None otherwise.
|
|
106
|
+
"""
|
|
107
|
+
lintro_ignore_path = find_lintro_ignore()
|
|
108
|
+
return str(lintro_ignore_path) if lintro_ignore_path else None
|
|
109
|
+
|
|
94
110
|
def _setup_defaults(self) -> None:
|
|
95
111
|
"""Set up default core options and patterns."""
|
|
96
112
|
# Add default exclude patterns if not already present
|
|
@@ -99,9 +115,10 @@ class BaseTool(ABC):
|
|
|
99
115
|
self.exclude_patterns.append(pattern)
|
|
100
116
|
|
|
101
117
|
# Add .lintro-ignore patterns (project-wide) if present
|
|
118
|
+
# Search upward from current directory to find project root
|
|
102
119
|
try:
|
|
103
|
-
lintro_ignore_path =
|
|
104
|
-
if os.path.exists(lintro_ignore_path):
|
|
120
|
+
lintro_ignore_path = self._find_lintro_ignore()
|
|
121
|
+
if lintro_ignore_path and os.path.exists(lintro_ignore_path):
|
|
105
122
|
with open(lintro_ignore_path, encoding="utf-8") as f:
|
|
106
123
|
for line in f:
|
|
107
124
|
line_stripped = line.strip()
|
|
@@ -282,64 +299,257 @@ class BaseTool(ABC):
|
|
|
282
299
|
) -> list[str]:
|
|
283
300
|
"""Get the command prefix to execute a tool.
|
|
284
301
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
302
|
+
Uses a unified approach based on tool category:
|
|
303
|
+
- Python bundled tools: Use python -m (guaranteed to use lintro's environment)
|
|
304
|
+
- Node.js tools: Use npx (respects project's package.json)
|
|
305
|
+
- Binary tools: Use system executable
|
|
289
306
|
|
|
290
307
|
Args:
|
|
291
308
|
tool_name: str: Name of the tool executable to find.
|
|
292
309
|
|
|
293
310
|
Returns:
|
|
294
311
|
list[str]: Command prefix to execute the tool.
|
|
295
|
-
|
|
296
|
-
Examples:
|
|
297
|
-
>>> self._get_executable_command("ruff")
|
|
298
|
-
["uv", "run", "ruff"] # preferred when uv is available
|
|
299
|
-
|
|
300
|
-
>>> self._get_executable_command("ruff")
|
|
301
|
-
["ruff"] # if uv is not available but the tool is on PATH
|
|
302
312
|
"""
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
#
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
313
|
+
import sys
|
|
314
|
+
|
|
315
|
+
# Python tools bundled with lintro (guaranteed in our environment)
|
|
316
|
+
# Note: darglint cannot be run as a module (python -m darglint),
|
|
317
|
+
# so it's excluded
|
|
318
|
+
python_bundled_tools = {"ruff", "black", "bandit", "yamllint"}
|
|
319
|
+
if tool_name in python_bundled_tools:
|
|
320
|
+
# Use python -m to ensure we use the tool from lintro's environment
|
|
321
|
+
python_exe = sys.executable
|
|
322
|
+
if python_exe:
|
|
323
|
+
return [python_exe, "-m", tool_name]
|
|
324
|
+
# Fallback to direct executable if python path not found
|
|
312
325
|
return [tool_name]
|
|
313
326
|
|
|
314
|
-
#
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if shutil.which("uvx"):
|
|
322
|
-
return ["uvx", tool_name]
|
|
327
|
+
# Pytest: user environment tool (not bundled)
|
|
328
|
+
if tool_name == "pytest":
|
|
329
|
+
# Use python -m pytest for cross-platform compatibility
|
|
330
|
+
python_exe = sys.executable
|
|
331
|
+
if python_exe:
|
|
332
|
+
return [python_exe, "-m", "pytest"]
|
|
333
|
+
# Fall back to direct executable
|
|
323
334
|
return [tool_name]
|
|
324
335
|
|
|
325
|
-
#
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if shutil.which("uv"):
|
|
332
|
-
return ["uv", "run", tool_name]
|
|
336
|
+
# Node.js tools: use npx to respect project's package.json
|
|
337
|
+
nodejs_tools = {"eslint", "prettier"}
|
|
338
|
+
if tool_name in nodejs_tools:
|
|
339
|
+
if shutil.which("npx"):
|
|
340
|
+
return ["npx", "--yes", tool_name]
|
|
341
|
+
# Fall back to direct executable
|
|
333
342
|
return [tool_name]
|
|
334
343
|
|
|
335
|
-
#
|
|
336
|
-
# prettier, hadolint, actionlint)
|
|
337
|
-
if shutil.which(tool_name):
|
|
338
|
-
return [tool_name]
|
|
339
|
-
if shutil.which("uv"):
|
|
340
|
-
return ["uv", "run", tool_name]
|
|
344
|
+
# Binary tools: use system executable
|
|
341
345
|
return [tool_name]
|
|
342
346
|
|
|
347
|
+
def _verify_tool_version(self) -> ToolResult | None:
|
|
348
|
+
"""Verify that the tool meets minimum version requirements.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Optional[ToolResult]: None if version check passes, or a skip result
|
|
352
|
+
if it fails
|
|
353
|
+
"""
|
|
354
|
+
from lintro.tools.core.version_requirements import check_tool_version
|
|
355
|
+
|
|
356
|
+
command = self._get_executable_command(self.name)
|
|
357
|
+
version_info = check_tool_version(self.name, command)
|
|
358
|
+
|
|
359
|
+
if version_info.version_check_passed:
|
|
360
|
+
return None # Version check passed
|
|
361
|
+
|
|
362
|
+
# Version check failed - return skip result with warning
|
|
363
|
+
skip_message = (
|
|
364
|
+
f"Skipping {self.name}: {version_info.error_message}. "
|
|
365
|
+
f"Minimum required: {version_info.min_version}. "
|
|
366
|
+
f"{version_info.install_hint}"
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return ToolResult(
|
|
370
|
+
name=self.name,
|
|
371
|
+
success=True, # Not an error, just skipping
|
|
372
|
+
output=skip_message,
|
|
373
|
+
issues_count=0,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# -------------------------------------------------------------------------
|
|
377
|
+
# Lintro Config Support - Tiered Model
|
|
378
|
+
# -------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
def _get_lintro_config(self) -> LintroConfig:
|
|
381
|
+
"""Get the current Lintro configuration.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
LintroConfig: The loaded Lintro configuration.
|
|
385
|
+
"""
|
|
386
|
+
from lintro.config import get_config
|
|
387
|
+
|
|
388
|
+
return get_config()
|
|
389
|
+
|
|
390
|
+
def _should_use_lintro_config(self) -> bool:
|
|
391
|
+
"""Check if Lintro config should be used.
|
|
392
|
+
|
|
393
|
+
Returns True if:
|
|
394
|
+
1. LINTRO_SKIP_CONFIG_INJECTION env var is NOT set, AND
|
|
395
|
+
2. A .lintro-config.yaml exists, OR [tool.lintro] is in pyproject.toml
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
bool: True if Lintro config should be used.
|
|
399
|
+
"""
|
|
400
|
+
# Allow tests to disable config injection
|
|
401
|
+
if os.environ.get("LINTRO_SKIP_CONFIG_INJECTION"):
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
lintro_config = self._get_lintro_config()
|
|
405
|
+
return lintro_config.config_path is not None
|
|
406
|
+
|
|
407
|
+
def _get_enforced_settings(self) -> set[str]:
|
|
408
|
+
"""Get the set of settings that are enforced by Lintro config.
|
|
409
|
+
|
|
410
|
+
This allows tools to check whether a setting is already being
|
|
411
|
+
injected via CLI args from the enforce tier, so they can avoid
|
|
412
|
+
adding duplicate arguments from their own options.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
set[str]: Set of setting names like 'line_length', 'target_python'.
|
|
416
|
+
"""
|
|
417
|
+
if not self._should_use_lintro_config():
|
|
418
|
+
return set()
|
|
419
|
+
|
|
420
|
+
lintro_config = self._get_lintro_config()
|
|
421
|
+
enforced: set[str] = set()
|
|
422
|
+
|
|
423
|
+
if lintro_config.enforce.line_length is not None:
|
|
424
|
+
enforced.add("line_length")
|
|
425
|
+
if lintro_config.enforce.target_python is not None:
|
|
426
|
+
enforced.add("target_python")
|
|
427
|
+
|
|
428
|
+
return enforced
|
|
429
|
+
|
|
430
|
+
def _get_enforce_cli_args(self) -> list[str]:
|
|
431
|
+
"""Get CLI arguments for enforced settings.
|
|
432
|
+
|
|
433
|
+
Returns CLI args that inject enforce settings (like line_length)
|
|
434
|
+
directly into the tool command line.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
list[str]: CLI arguments for enforced settings.
|
|
438
|
+
"""
|
|
439
|
+
from lintro.config import get_enforce_cli_args
|
|
440
|
+
|
|
441
|
+
if not self._should_use_lintro_config():
|
|
442
|
+
return []
|
|
443
|
+
|
|
444
|
+
lintro_config = self._get_lintro_config()
|
|
445
|
+
return get_enforce_cli_args(
|
|
446
|
+
tool_name=self.name,
|
|
447
|
+
lintro_config=lintro_config,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def _get_defaults_config_args(self) -> list[str]:
|
|
451
|
+
"""Get CLI arguments for defaults config injection.
|
|
452
|
+
|
|
453
|
+
If the tool has no native config and defaults are defined in
|
|
454
|
+
the Lintro config, generates a temp config file and returns
|
|
455
|
+
the CLI args to pass it to the tool.
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
list[str]: CLI arguments for defaults config injection.
|
|
459
|
+
"""
|
|
460
|
+
from lintro.config import (
|
|
461
|
+
generate_defaults_config,
|
|
462
|
+
get_defaults_injection_args,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
if not self._should_use_lintro_config():
|
|
466
|
+
return []
|
|
467
|
+
|
|
468
|
+
lintro_config = self._get_lintro_config()
|
|
469
|
+
config_path = generate_defaults_config(
|
|
470
|
+
tool_name=self.name,
|
|
471
|
+
lintro_config=lintro_config,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
if config_path is None:
|
|
475
|
+
return []
|
|
476
|
+
|
|
477
|
+
return get_defaults_injection_args(
|
|
478
|
+
tool_name=self.name,
|
|
479
|
+
config_path=config_path,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
def _build_config_args(self) -> list[str]:
|
|
483
|
+
"""Build complete config-related CLI arguments for this tool.
|
|
484
|
+
|
|
485
|
+
Uses the tiered model:
|
|
486
|
+
1. Enforced settings are injected via CLI flags
|
|
487
|
+
2. Defaults config is used only if no native config exists
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
list[str]: Combined CLI arguments for configuration.
|
|
491
|
+
"""
|
|
492
|
+
args: list[str] = []
|
|
493
|
+
|
|
494
|
+
# Add enforce CLI args (e.g., --line-length 88)
|
|
495
|
+
args.extend(self._get_enforce_cli_args())
|
|
496
|
+
|
|
497
|
+
# Add defaults config args if applicable
|
|
498
|
+
args.extend(self._get_defaults_config_args())
|
|
499
|
+
|
|
500
|
+
return args
|
|
501
|
+
|
|
502
|
+
# -------------------------------------------------------------------------
|
|
503
|
+
# Deprecated methods for backward compatibility
|
|
504
|
+
# -------------------------------------------------------------------------
|
|
505
|
+
|
|
506
|
+
def _generate_tool_config(self) -> Path | None:
|
|
507
|
+
"""Generate a temporary config file for this tool.
|
|
508
|
+
|
|
509
|
+
DEPRECATED: Use _get_enforce_cli_args() and _get_defaults_config_args()
|
|
510
|
+
instead.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
Path | None: Path to generated config file, or None if not needed.
|
|
514
|
+
"""
|
|
515
|
+
from lintro.config import generate_defaults_config
|
|
516
|
+
|
|
517
|
+
logger.debug(
|
|
518
|
+
f"_generate_tool_config() is deprecated for {self.name}. "
|
|
519
|
+
"Use _build_config_args() or call _get_enforce_cli_args() and "
|
|
520
|
+
"_get_defaults_config_args() directly.",
|
|
521
|
+
)
|
|
522
|
+
lintro_config = self._get_lintro_config()
|
|
523
|
+
return generate_defaults_config(
|
|
524
|
+
tool_name=self.name,
|
|
525
|
+
lintro_config=lintro_config,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
def _get_config_injection_args(self) -> list[str]:
|
|
529
|
+
"""Get CLI arguments to inject Lintro config into this tool.
|
|
530
|
+
|
|
531
|
+
DEPRECATED: Use _get_enforce_cli_args() and _get_defaults_config_args()
|
|
532
|
+
instead, or use _build_config_args() which combines both.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
list[str]: CLI arguments for config injection (enforce + defaults).
|
|
536
|
+
"""
|
|
537
|
+
args: list[str] = []
|
|
538
|
+
args.extend(self._get_enforce_cli_args())
|
|
539
|
+
args.extend(self._get_defaults_config_args())
|
|
540
|
+
return args
|
|
541
|
+
|
|
542
|
+
def _get_no_auto_config_args(self) -> list[str]:
|
|
543
|
+
"""Get CLI arguments to disable native config auto-discovery.
|
|
544
|
+
|
|
545
|
+
DEPRECATED: No longer needed with the tiered model.
|
|
546
|
+
Tools use their native configs by default.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
list[str]: Empty list (no longer used).
|
|
550
|
+
"""
|
|
551
|
+
return []
|
|
552
|
+
|
|
343
553
|
@abstractmethod
|
|
344
554
|
def check(
|
|
345
555
|
self,
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
6
8
|
from lintro.models.core.tool import Tool
|
|
7
9
|
from lintro.tools.tool_enum import ToolEnum
|
|
10
|
+
from lintro.utils.unified_config import get_ordered_tools
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
@dataclass
|
|
@@ -14,9 +17,14 @@ class ToolManager:
|
|
|
14
17
|
This class is responsible for:
|
|
15
18
|
- Tool registration
|
|
16
19
|
- Tool conflict resolution
|
|
17
|
-
- Tool execution order
|
|
20
|
+
- Tool execution order (priority-based, alphabetical, or custom)
|
|
18
21
|
- Tool configuration management
|
|
19
22
|
|
|
23
|
+
Tool ordering is controlled by [tool.lintro].tool_order in pyproject.toml:
|
|
24
|
+
- "priority" (default): Formatters run before linters based on priority values
|
|
25
|
+
- "alphabetical": Tools run in alphabetical order by name
|
|
26
|
+
- "custom": Tools run in order specified by [tool.lintro].tool_order_custom
|
|
27
|
+
|
|
20
28
|
Attributes:
|
|
21
29
|
_tools: Dictionary mapping core names to core classes
|
|
22
30
|
_check_tools: Dictionary mapping core names to core classes that can check
|
|
@@ -77,17 +85,23 @@ class ToolManager:
|
|
|
77
85
|
) -> list[ToolEnum]:
|
|
78
86
|
"""Get the order in which tools should be executed.
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
88
|
+
Tool ordering is controlled by [tool.lintro].tool_order in pyproject.toml:
|
|
89
|
+
- "priority" (default): Formatters run before linters based on priority
|
|
90
|
+
- "alphabetical": Tools run in alphabetical order by name
|
|
91
|
+
- "custom": Tools run in order specified by [tool.lintro].tool_order_custom
|
|
92
|
+
|
|
93
|
+
This method also handles:
|
|
94
|
+
- Tool conflicts (unless ignore_conflicts is True)
|
|
84
95
|
|
|
85
96
|
Args:
|
|
86
|
-
tool_list: List of
|
|
87
|
-
ignore_conflicts:
|
|
97
|
+
tool_list: List of tools to order.
|
|
98
|
+
ignore_conflicts: If True, skip conflict checking.
|
|
88
99
|
|
|
89
100
|
Returns:
|
|
90
|
-
List of
|
|
101
|
+
List of ToolEnum members in execution order based on configured strategy.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If duplicate tools are found in tool_list.
|
|
91
105
|
"""
|
|
92
106
|
if not tool_list:
|
|
93
107
|
return []
|
|
@@ -95,30 +109,69 @@ class ToolManager:
|
|
|
95
109
|
# Get core instances
|
|
96
110
|
tools = {name: self.get_tool(name) for name in tool_list}
|
|
97
111
|
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
# Validate for duplicate tools
|
|
113
|
+
seen_names: set[str] = set()
|
|
114
|
+
duplicates: list[str] = []
|
|
115
|
+
for tool in tool_list:
|
|
116
|
+
tool_name_lower = tool.name.lower()
|
|
117
|
+
if tool_name_lower in seen_names:
|
|
118
|
+
duplicates.append(tool.name)
|
|
119
|
+
else:
|
|
120
|
+
seen_names.add(tool_name_lower)
|
|
121
|
+
if duplicates:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"Duplicate tools found in tool_list: {', '.join(duplicates)}",
|
|
103
124
|
)
|
|
104
125
|
|
|
126
|
+
# Convert ToolEnum to tool names for unified config ordering
|
|
127
|
+
tool_names = [t.name.lower() for t in tool_list]
|
|
128
|
+
ordered_names = get_ordered_tools(tool_names)
|
|
129
|
+
|
|
130
|
+
# Map back to ToolEnum in the ordered sequence
|
|
131
|
+
name_to_enum = {t.name.lower(): t for t in tool_list}
|
|
132
|
+
sorted_tools = [
|
|
133
|
+
name_to_enum[name] for name in ordered_names if name in name_to_enum
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
# Validate that all requested tools are preserved
|
|
137
|
+
original_names = {t.name.lower() for t in tool_list}
|
|
138
|
+
sorted_names = {t.name.lower() for t in sorted_tools}
|
|
139
|
+
missing_tools = original_names - sorted_names
|
|
140
|
+
if missing_tools:
|
|
141
|
+
# Append missing tools in their original order
|
|
142
|
+
missing_enums = [t for t in tool_list if t.name.lower() in missing_tools]
|
|
143
|
+
sorted_tools.extend(missing_enums)
|
|
144
|
+
logger.warning(
|
|
145
|
+
f"Some tools were not found in ordered list and appended: "
|
|
146
|
+
f"{[t.name for t in missing_enums]}",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if ignore_conflicts:
|
|
150
|
+
return sorted_tools
|
|
151
|
+
|
|
105
152
|
# Build conflict graph
|
|
106
153
|
conflict_graph: dict[ToolEnum, set[ToolEnum]] = {
|
|
107
154
|
name: set() for name in tool_list
|
|
108
155
|
}
|
|
156
|
+
# Create mapping from tool name strings to ToolEnum members
|
|
157
|
+
# Handle both lowercase strings and ToolEnum members in conflicts_with
|
|
158
|
+
name_to_enum_map = {t.name.lower(): t for t in ToolEnum}
|
|
109
159
|
for name, tool in tools.items():
|
|
110
160
|
for conflict in tool.config.conflicts_with:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
161
|
+
# Convert conflict string to ToolEnum if needed
|
|
162
|
+
conflict_enum: ToolEnum | None = None
|
|
163
|
+
if isinstance(conflict, str):
|
|
164
|
+
# Look up by lowercase name
|
|
165
|
+
conflict_enum = name_to_enum_map.get(conflict.lower())
|
|
166
|
+
elif isinstance(conflict, ToolEnum):
|
|
167
|
+
# Already a ToolEnum member
|
|
168
|
+
conflict_enum = conflict
|
|
169
|
+
# Only add to conflict graph if conflict_enum is in tool_list
|
|
170
|
+
if conflict_enum is not None and conflict_enum in tool_list:
|
|
171
|
+
conflict_graph[name].add(conflict_enum)
|
|
172
|
+
conflict_graph[conflict_enum].add(name)
|
|
173
|
+
|
|
174
|
+
# Resolve conflicts by keeping the first tool in ordered sequence
|
|
122
175
|
result = []
|
|
123
176
|
for tool_name in sorted_tools:
|
|
124
177
|
# Check if this core conflicts with any already selected tools
|