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
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,10 +115,11 @@ 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):
|
|
105
|
-
with open(lintro_ignore_path,
|
|
120
|
+
lintro_ignore_path = self._find_lintro_ignore()
|
|
121
|
+
if lintro_ignore_path and os.path.exists(lintro_ignore_path):
|
|
122
|
+
with open(lintro_ignore_path, encoding="utf-8") as f:
|
|
106
123
|
for line in f:
|
|
107
124
|
line_stripped = line.strip()
|
|
108
125
|
if not line_stripped or line_stripped.startswith("#"):
|
|
@@ -147,6 +164,9 @@ class BaseTool(ABC):
|
|
|
147
164
|
TimeoutExpired: If command times out.
|
|
148
165
|
FileNotFoundError: If command executable is not found.
|
|
149
166
|
"""
|
|
167
|
+
# Validate command arguments for safety prior to execution
|
|
168
|
+
self._validate_subprocess_command(cmd=cmd)
|
|
169
|
+
|
|
150
170
|
try:
|
|
151
171
|
result = subprocess.run( # nosec B603 - args list, shell=False
|
|
152
172
|
cmd,
|
|
@@ -157,7 +177,6 @@ class BaseTool(ABC):
|
|
|
157
177
|
"timeout",
|
|
158
178
|
self._default_timeout,
|
|
159
179
|
),
|
|
160
|
-
check=False,
|
|
161
180
|
cwd=cwd,
|
|
162
181
|
)
|
|
163
182
|
return result.returncode == 0, result.stdout + result.stderr
|
|
@@ -184,6 +203,31 @@ class BaseTool(ABC):
|
|
|
184
203
|
f"Please ensure it is installed and in your PATH.",
|
|
185
204
|
) from e
|
|
186
205
|
|
|
206
|
+
def _validate_subprocess_command(self, cmd: list[str]) -> None:
|
|
207
|
+
"""Validate a subprocess command argument list for safety.
|
|
208
|
+
|
|
209
|
+
Ensures that the command is a non-empty list of strings and that no
|
|
210
|
+
argument contains shell metacharacters that could enable command
|
|
211
|
+
injection when passed to subprocess (even with ``shell=False``).
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
cmd: list[str]: Command and arguments to validate.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
ValueError: If the command list is empty, contains non-strings,
|
|
218
|
+
or contains unsafe characters.
|
|
219
|
+
"""
|
|
220
|
+
if not cmd or not isinstance(cmd, list):
|
|
221
|
+
raise ValueError("Command must be a non-empty list of strings")
|
|
222
|
+
|
|
223
|
+
unsafe_chars: set[str] = {";", "&", "|", ">", "<", "`", "$", "\\", "\n", "\r"}
|
|
224
|
+
|
|
225
|
+
for arg in cmd:
|
|
226
|
+
if not isinstance(arg, str):
|
|
227
|
+
raise ValueError("All command arguments must be strings")
|
|
228
|
+
if any(ch in arg for ch in unsafe_chars):
|
|
229
|
+
raise ValueError("Unsafe character detected in command argument")
|
|
230
|
+
|
|
187
231
|
def set_options(self, **kwargs) -> None:
|
|
188
232
|
"""Set core options.
|
|
189
233
|
|
|
@@ -233,11 +277,10 @@ class BaseTool(ABC):
|
|
|
233
277
|
self,
|
|
234
278
|
paths: list[str],
|
|
235
279
|
) -> str | None:
|
|
236
|
-
"""Return the common parent directory for the given paths
|
|
237
|
-
applicable.
|
|
280
|
+
"""Return the common parent directory for the given paths.
|
|
238
281
|
|
|
239
282
|
Args:
|
|
240
|
-
paths: list[str]:
|
|
283
|
+
paths: list[str]: Paths to compute a common parent directory for.
|
|
241
284
|
|
|
242
285
|
Returns:
|
|
243
286
|
str | None: Common parent directory path, or None if not applicable.
|
|
@@ -256,64 +299,257 @@ class BaseTool(ABC):
|
|
|
256
299
|
) -> list[str]:
|
|
257
300
|
"""Get the command prefix to execute a tool.
|
|
258
301
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
263
306
|
|
|
264
307
|
Args:
|
|
265
308
|
tool_name: str: Name of the tool executable to find.
|
|
266
309
|
|
|
267
310
|
Returns:
|
|
268
311
|
list[str]: Command prefix to execute the tool.
|
|
269
|
-
|
|
270
|
-
Examples:
|
|
271
|
-
>>> self._get_executable_command("ruff")
|
|
272
|
-
["uv", "run", "ruff"] # preferred when uv is available
|
|
273
|
-
|
|
274
|
-
>>> self._get_executable_command("ruff")
|
|
275
|
-
["ruff"] # if uv is not available but the tool is on PATH
|
|
276
312
|
"""
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
#
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
286
325
|
return [tool_name]
|
|
287
326
|
|
|
288
|
-
#
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if shutil.which("uvx"):
|
|
296
|
-
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
|
|
297
334
|
return [tool_name]
|
|
298
335
|
|
|
299
|
-
#
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if shutil.which("uvx"):
|
|
306
|
-
return ["uvx", 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
|
|
307
342
|
return [tool_name]
|
|
308
343
|
|
|
309
|
-
#
|
|
310
|
-
# prettier, hadolint, actionlint)
|
|
311
|
-
if shutil.which(tool_name):
|
|
312
|
-
return [tool_name]
|
|
313
|
-
if shutil.which("uv"):
|
|
314
|
-
return ["uv", "run", tool_name]
|
|
344
|
+
# Binary tools: use system executable
|
|
315
345
|
return [tool_name]
|
|
316
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
|
+
|
|
317
553
|
@abstractmethod
|
|
318
554
|
def check(
|
|
319
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
|