lintro 0.3.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.
Potentially problematic release.
This version of lintro might be problematic. Click here for more details.
- lintro/__init__.py +3 -0
- lintro/__main__.py +6 -0
- lintro/ascii-art/fail.txt +404 -0
- lintro/ascii-art/success.txt +484 -0
- lintro/cli.py +70 -0
- lintro/cli_utils/__init__.py +7 -0
- lintro/cli_utils/commands/__init__.py +7 -0
- lintro/cli_utils/commands/check.py +210 -0
- lintro/cli_utils/commands/format.py +167 -0
- lintro/cli_utils/commands/list_tools.py +114 -0
- lintro/enums/__init__.py +0 -0
- lintro/enums/action.py +29 -0
- lintro/enums/darglint_strictness.py +22 -0
- lintro/enums/group_by.py +31 -0
- lintro/enums/hadolint_enums.py +46 -0
- lintro/enums/output_format.py +40 -0
- lintro/enums/tool_name.py +36 -0
- lintro/enums/tool_type.py +27 -0
- lintro/enums/yamllint_format.py +22 -0
- lintro/exceptions/__init__.py +0 -0
- lintro/exceptions/errors.py +15 -0
- lintro/formatters/__init__.py +0 -0
- lintro/formatters/core/__init__.py +0 -0
- lintro/formatters/core/output_style.py +21 -0
- lintro/formatters/core/table_descriptor.py +24 -0
- lintro/formatters/styles/__init__.py +17 -0
- lintro/formatters/styles/csv.py +41 -0
- lintro/formatters/styles/grid.py +91 -0
- lintro/formatters/styles/html.py +48 -0
- lintro/formatters/styles/json.py +61 -0
- lintro/formatters/styles/markdown.py +41 -0
- lintro/formatters/styles/plain.py +39 -0
- lintro/formatters/tools/__init__.py +35 -0
- lintro/formatters/tools/darglint_formatter.py +72 -0
- lintro/formatters/tools/hadolint_formatter.py +84 -0
- lintro/formatters/tools/prettier_formatter.py +76 -0
- lintro/formatters/tools/ruff_formatter.py +116 -0
- lintro/formatters/tools/yamllint_formatter.py +87 -0
- lintro/models/__init__.py +0 -0
- lintro/models/core/__init__.py +0 -0
- lintro/models/core/tool.py +104 -0
- lintro/models/core/tool_config.py +23 -0
- lintro/models/core/tool_result.py +39 -0
- lintro/parsers/__init__.py +0 -0
- lintro/parsers/darglint/__init__.py +0 -0
- lintro/parsers/darglint/darglint_issue.py +9 -0
- lintro/parsers/darglint/darglint_parser.py +62 -0
- lintro/parsers/hadolint/__init__.py +1 -0
- lintro/parsers/hadolint/hadolint_issue.py +24 -0
- lintro/parsers/hadolint/hadolint_parser.py +65 -0
- lintro/parsers/prettier/__init__.py +0 -0
- lintro/parsers/prettier/prettier_issue.py +10 -0
- lintro/parsers/prettier/prettier_parser.py +60 -0
- lintro/parsers/ruff/__init__.py +1 -0
- lintro/parsers/ruff/ruff_issue.py +43 -0
- lintro/parsers/ruff/ruff_parser.py +89 -0
- lintro/parsers/yamllint/__init__.py +0 -0
- lintro/parsers/yamllint/yamllint_issue.py +24 -0
- lintro/parsers/yamllint/yamllint_parser.py +68 -0
- lintro/tools/__init__.py +40 -0
- lintro/tools/core/__init__.py +0 -0
- lintro/tools/core/tool_base.py +320 -0
- lintro/tools/core/tool_manager.py +167 -0
- lintro/tools/implementations/__init__.py +0 -0
- lintro/tools/implementations/tool_darglint.py +245 -0
- lintro/tools/implementations/tool_hadolint.py +302 -0
- lintro/tools/implementations/tool_prettier.py +270 -0
- lintro/tools/implementations/tool_ruff.py +618 -0
- lintro/tools/implementations/tool_yamllint.py +240 -0
- lintro/tools/tool_enum.py +17 -0
- lintro/utils/__init__.py +0 -0
- lintro/utils/ascii_normalize_cli.py +84 -0
- lintro/utils/config.py +39 -0
- lintro/utils/console_logger.py +783 -0
- lintro/utils/formatting.py +173 -0
- lintro/utils/output_manager.py +301 -0
- lintro/utils/path_utils.py +41 -0
- lintro/utils/tool_executor.py +443 -0
- lintro/utils/tool_utils.py +431 -0
- lintro-0.3.2.dist-info/METADATA +338 -0
- lintro-0.3.2.dist-info/RECORD +85 -0
- lintro-0.3.2.dist-info/WHEEL +5 -0
- lintro-0.3.2.dist-info/entry_points.txt +2 -0
- lintro-0.3.2.dist-info/licenses/LICENSE +21 -0
- lintro-0.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Base core implementation for Lintro."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from lintro.enums.tool_type import ToolType
|
|
10
|
+
from lintro.models.core.tool import ToolConfig, ToolResult
|
|
11
|
+
|
|
12
|
+
# Constants for default values
|
|
13
|
+
DEFAULT_TIMEOUT: int = 30
|
|
14
|
+
DEFAULT_EXCLUDE_PATTERNS: list[str] = [
|
|
15
|
+
".git",
|
|
16
|
+
".hg",
|
|
17
|
+
".svn",
|
|
18
|
+
"__pycache__",
|
|
19
|
+
"*.pyc",
|
|
20
|
+
"*.pyo",
|
|
21
|
+
"*.pyd",
|
|
22
|
+
".pytest_cache",
|
|
23
|
+
".coverage",
|
|
24
|
+
"htmlcov",
|
|
25
|
+
"dist",
|
|
26
|
+
"build",
|
|
27
|
+
"*.egg-info",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class BaseTool(ABC):
|
|
33
|
+
"""Base class for all tools.
|
|
34
|
+
|
|
35
|
+
This class provides common functionality for all tools and implements
|
|
36
|
+
the Tool protocol. Tool implementations should inherit from this class
|
|
37
|
+
and implement the abstract methods.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
name: str: Tool name.
|
|
41
|
+
description: str: Tool description.
|
|
42
|
+
can_fix: bool: Whether the core can fix issues.
|
|
43
|
+
config: ToolConfig: Tool configuration.
|
|
44
|
+
exclude_patterns: list[str]: List of patterns to exclude.
|
|
45
|
+
include_venv: bool: Whether to include virtual environment files.
|
|
46
|
+
_default_timeout: int: Default timeout for core execution in seconds.
|
|
47
|
+
_default_exclude_patterns: list[str]: Default patterns to exclude.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If the configuration is invalid.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
description: str
|
|
55
|
+
can_fix: bool
|
|
56
|
+
config: ToolConfig = field(default_factory=ToolConfig)
|
|
57
|
+
exclude_patterns: list[str] = field(default_factory=list)
|
|
58
|
+
include_venv: bool = False
|
|
59
|
+
|
|
60
|
+
_default_timeout: int = DEFAULT_TIMEOUT
|
|
61
|
+
_default_exclude_patterns: list[str] = field(
|
|
62
|
+
default_factory=lambda: DEFAULT_EXCLUDE_PATTERNS
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def __post_init__(self) -> None:
|
|
66
|
+
"""Initialize core options and validate configuration."""
|
|
67
|
+
self.options: dict[str, object] = {}
|
|
68
|
+
self._validate_config()
|
|
69
|
+
self._setup_defaults()
|
|
70
|
+
|
|
71
|
+
def _validate_config(self) -> None:
|
|
72
|
+
"""Validate core configuration.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
ValueError: If the configuration is invalid.
|
|
76
|
+
"""
|
|
77
|
+
if not self.name:
|
|
78
|
+
raise ValueError("Tool name cannot be empty")
|
|
79
|
+
if not self.description:
|
|
80
|
+
raise ValueError("Tool description cannot be empty")
|
|
81
|
+
if not isinstance(self.config, ToolConfig):
|
|
82
|
+
raise ValueError("Tool config must be a ToolConfig instance")
|
|
83
|
+
if not isinstance(self.config.priority, int):
|
|
84
|
+
raise ValueError("Tool priority must be an integer")
|
|
85
|
+
if not isinstance(self.config.conflicts_with, list):
|
|
86
|
+
raise ValueError("Tool conflicts_with must be a list")
|
|
87
|
+
if not isinstance(self.config.file_patterns, list):
|
|
88
|
+
raise ValueError("Tool file_patterns must be a list")
|
|
89
|
+
if not isinstance(self.config.tool_type, ToolType):
|
|
90
|
+
raise ValueError("Tool tool_type must be a ToolType instance")
|
|
91
|
+
|
|
92
|
+
def _setup_defaults(self) -> None:
|
|
93
|
+
"""Set up default core options and patterns."""
|
|
94
|
+
# Add default exclude patterns if not already present
|
|
95
|
+
for pattern in self._default_exclude_patterns:
|
|
96
|
+
if pattern not in self.exclude_patterns:
|
|
97
|
+
self.exclude_patterns.append(pattern)
|
|
98
|
+
|
|
99
|
+
# Add .lintro-ignore patterns (project-wide) if present
|
|
100
|
+
try:
|
|
101
|
+
lintro_ignore_path = os.path.abspath(".lintro-ignore")
|
|
102
|
+
if os.path.exists(lintro_ignore_path):
|
|
103
|
+
with open(lintro_ignore_path, "r", encoding="utf-8") as f:
|
|
104
|
+
for line in f:
|
|
105
|
+
line_stripped = line.strip()
|
|
106
|
+
if not line_stripped or line_stripped.startswith("#"):
|
|
107
|
+
continue
|
|
108
|
+
if line_stripped not in self.exclude_patterns:
|
|
109
|
+
self.exclude_patterns.append(line_stripped)
|
|
110
|
+
except Exception:
|
|
111
|
+
# Non-fatal if ignore file can't be read
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
# Load default options from config
|
|
115
|
+
if hasattr(self.config, "options") and self.config.options:
|
|
116
|
+
for key, value in self.config.options.items():
|
|
117
|
+
if key not in self.options:
|
|
118
|
+
self.options[key] = value
|
|
119
|
+
|
|
120
|
+
# Set default timeout if not specified
|
|
121
|
+
if "timeout" not in self.options:
|
|
122
|
+
self.options["timeout"] = self._default_timeout
|
|
123
|
+
|
|
124
|
+
def _run_subprocess(
|
|
125
|
+
self,
|
|
126
|
+
cmd: list[str],
|
|
127
|
+
timeout: int | None = None,
|
|
128
|
+
cwd: str | None = None,
|
|
129
|
+
) -> tuple[bool, str]:
|
|
130
|
+
"""Run a subprocess command.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
cmd: list[str]: Command to run.
|
|
134
|
+
timeout: int | None: Command timeout in seconds (defaults to core's \
|
|
135
|
+
timeout).
|
|
136
|
+
cwd: str | None: Working directory to run the command in (optional).
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
tuple[bool, str]: Tuple of (success, output)
|
|
140
|
+
- success: True if the command succeeded, False otherwise.
|
|
141
|
+
- output: Command output (stdout + stderr).
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
CalledProcessError: If command fails.
|
|
145
|
+
TimeoutExpired: If command times out.
|
|
146
|
+
FileNotFoundError: If command executable is not found.
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
result = subprocess.run(
|
|
150
|
+
cmd,
|
|
151
|
+
capture_output=True,
|
|
152
|
+
text=True,
|
|
153
|
+
timeout=timeout
|
|
154
|
+
or self.options.get(
|
|
155
|
+
"timeout",
|
|
156
|
+
self._default_timeout,
|
|
157
|
+
),
|
|
158
|
+
check=False,
|
|
159
|
+
cwd=cwd,
|
|
160
|
+
)
|
|
161
|
+
return result.returncode == 0, result.stdout + result.stderr
|
|
162
|
+
except subprocess.TimeoutExpired as e:
|
|
163
|
+
raise subprocess.TimeoutExpired(
|
|
164
|
+
cmd=cmd,
|
|
165
|
+
timeout=timeout
|
|
166
|
+
or self.options.get(
|
|
167
|
+
"timeout",
|
|
168
|
+
self._default_timeout,
|
|
169
|
+
),
|
|
170
|
+
output=str(e),
|
|
171
|
+
) from e
|
|
172
|
+
except subprocess.CalledProcessError as e:
|
|
173
|
+
raise subprocess.CalledProcessError(
|
|
174
|
+
returncode=e.returncode,
|
|
175
|
+
cmd=cmd,
|
|
176
|
+
output=e.output,
|
|
177
|
+
stderr=e.stderr,
|
|
178
|
+
) from e
|
|
179
|
+
except FileNotFoundError as e:
|
|
180
|
+
raise FileNotFoundError(
|
|
181
|
+
f"Command not found: {cmd[0]}. "
|
|
182
|
+
f"Please ensure it is installed and in your PATH.",
|
|
183
|
+
) from e
|
|
184
|
+
|
|
185
|
+
def set_options(self, **kwargs) -> None:
|
|
186
|
+
"""Set core options.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
**kwargs: Tool-specific options.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
ValueError: If an option value is invalid.
|
|
193
|
+
"""
|
|
194
|
+
for key, value in kwargs.items():
|
|
195
|
+
if key == "timeout" and not isinstance(value, (int, type(None))):
|
|
196
|
+
raise ValueError("Timeout must be an integer or None")
|
|
197
|
+
if key == "exclude_patterns" and not isinstance(value, list):
|
|
198
|
+
raise ValueError("Exclude patterns must be a list")
|
|
199
|
+
if key == "include_venv" and not isinstance(value, bool):
|
|
200
|
+
raise ValueError("Include venv must be a boolean")
|
|
201
|
+
|
|
202
|
+
# Update options dict
|
|
203
|
+
self.options.update(kwargs)
|
|
204
|
+
|
|
205
|
+
# Update specific attributes for exclude_patterns and include_venv
|
|
206
|
+
if "exclude_patterns" in kwargs:
|
|
207
|
+
self.exclude_patterns = kwargs["exclude_patterns"]
|
|
208
|
+
if "include_venv" in kwargs:
|
|
209
|
+
self.include_venv = kwargs["include_venv"]
|
|
210
|
+
|
|
211
|
+
def _validate_paths(
|
|
212
|
+
self,
|
|
213
|
+
paths: list[str],
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Validate that paths exist and are accessible.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
paths: list[str]: List of paths to validate.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
FileNotFoundError: If any path does not exist.
|
|
222
|
+
PermissionError: If any path is not accessible.
|
|
223
|
+
"""
|
|
224
|
+
for path in paths:
|
|
225
|
+
if not os.path.exists(path):
|
|
226
|
+
raise FileNotFoundError(f"Path does not exist: {path}")
|
|
227
|
+
if not os.access(path, os.R_OK):
|
|
228
|
+
raise PermissionError(f"Path is not accessible: {path}")
|
|
229
|
+
|
|
230
|
+
def get_cwd(
|
|
231
|
+
self,
|
|
232
|
+
paths: list[str],
|
|
233
|
+
) -> str | None:
|
|
234
|
+
"""Return the common parent directory for the given paths, or None if not
|
|
235
|
+
applicable.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
paths: list[str]: List of file paths to find common parent directory for.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
str | None: Common parent directory path, or None if not applicable.
|
|
242
|
+
"""
|
|
243
|
+
if paths:
|
|
244
|
+
parent_dirs: set[str] = {os.path.dirname(os.path.abspath(p)) for p in paths}
|
|
245
|
+
if len(parent_dirs) == 1:
|
|
246
|
+
return parent_dirs.pop()
|
|
247
|
+
else:
|
|
248
|
+
return os.path.commonpath(list(parent_dirs))
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
def _get_executable_command(
|
|
252
|
+
self,
|
|
253
|
+
tool_name: str,
|
|
254
|
+
) -> list[str]:
|
|
255
|
+
"""Get the command prefix to execute a tool.
|
|
256
|
+
|
|
257
|
+
This method provides common logic for tool executable detection.
|
|
258
|
+
It first tries to find the tool directly in PATH, and if not found,
|
|
259
|
+
falls back to running via 'uv run' if uv is available.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
tool_name: str: Name of the tool executable to find.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
list[str]: Command prefix to execute the tool.
|
|
266
|
+
|
|
267
|
+
Examples:
|
|
268
|
+
>>> self._get_executable_command("ruff")
|
|
269
|
+
["ruff"] # if ruff is directly available
|
|
270
|
+
|
|
271
|
+
>>> self._get_executable_command("ruff")
|
|
272
|
+
["uv", "run", "ruff"] # if ruff not available but uv is
|
|
273
|
+
"""
|
|
274
|
+
# First try direct tool execution
|
|
275
|
+
if shutil.which(tool_name):
|
|
276
|
+
return [tool_name]
|
|
277
|
+
|
|
278
|
+
# If tool not directly available, try via uv
|
|
279
|
+
if shutil.which("uv"):
|
|
280
|
+
return ["uv", "run", tool_name]
|
|
281
|
+
|
|
282
|
+
# Fallback to direct tool (will likely fail but gives clear error)
|
|
283
|
+
return [tool_name]
|
|
284
|
+
|
|
285
|
+
@abstractmethod
|
|
286
|
+
def check(
|
|
287
|
+
self,
|
|
288
|
+
paths: list[str],
|
|
289
|
+
) -> ToolResult:
|
|
290
|
+
"""Check files for issues.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
paths: list[str]: List of file paths to check.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
ToolResult: ToolResult instance.
|
|
297
|
+
|
|
298
|
+
Raises:
|
|
299
|
+
FileNotFoundError: If any path does not exist or is not accessible.
|
|
300
|
+
subprocess.TimeoutExpired: If the core execution times out.
|
|
301
|
+
subprocess.CalledProcessError: If the core execution fails.
|
|
302
|
+
"""
|
|
303
|
+
...
|
|
304
|
+
|
|
305
|
+
@abstractmethod
|
|
306
|
+
def fix(
|
|
307
|
+
self,
|
|
308
|
+
paths: list[str],
|
|
309
|
+
) -> ToolResult:
|
|
310
|
+
"""Fix issues in files.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
paths: list[str]: List of file paths to fix.
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
NotImplementedError: If the core does not support fixing issues.
|
|
317
|
+
"""
|
|
318
|
+
if not self.can_fix:
|
|
319
|
+
raise NotImplementedError(f"{self.name} does not support fixing issues")
|
|
320
|
+
...
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Tool manager for Lintro."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from lintro.models.core.tool import Tool
|
|
7
|
+
from lintro.tools.tool_enum import ToolEnum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ToolManager:
|
|
12
|
+
"""Manager for core registration and execution.
|
|
13
|
+
|
|
14
|
+
This class is responsible for:
|
|
15
|
+
- Tool registration
|
|
16
|
+
- Tool conflict resolution
|
|
17
|
+
- Tool execution order
|
|
18
|
+
- Tool configuration management
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
_tools: Dictionary mapping core names to core classes
|
|
22
|
+
_check_tools: Dictionary mapping core names to core classes that can check
|
|
23
|
+
_fix_tools: Dictionary mapping core names to core classes that can fix
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
_tools: dict[ToolEnum, type[Tool]] = field(default_factory=dict)
|
|
27
|
+
_check_tools: dict[ToolEnum, type[Tool]] = field(default_factory=dict)
|
|
28
|
+
_fix_tools: dict[ToolEnum, type[Tool]] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
def register_tool(
|
|
31
|
+
self,
|
|
32
|
+
tool_class: type[Tool],
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Register a core class.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
tool_class: The core class to register.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
ValueError: If the tool class is not found in ToolEnum.
|
|
41
|
+
"""
|
|
42
|
+
tool = tool_class()
|
|
43
|
+
# Find the ToolEnum member for this class
|
|
44
|
+
tool_enum = next((e for e in ToolEnum if e.value is tool_class), None)
|
|
45
|
+
if tool_enum is None:
|
|
46
|
+
raise ValueError(f"Tool class {tool_class} not found in ToolEnum")
|
|
47
|
+
self._tools[tool_enum] = tool_class
|
|
48
|
+
# All tools can check (they all inherit from BaseTool with check method)
|
|
49
|
+
self._check_tools[tool_enum] = tool_class
|
|
50
|
+
# Only tools with can_fix=True can actually fix issues
|
|
51
|
+
if tool.can_fix:
|
|
52
|
+
self._fix_tools[tool_enum] = tool_class
|
|
53
|
+
|
|
54
|
+
def get_tool(
|
|
55
|
+
self,
|
|
56
|
+
name: ToolEnum,
|
|
57
|
+
) -> Tool:
|
|
58
|
+
"""Get a core instance by name.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
name: The name of the core to get
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The core instance
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: If the core is not found
|
|
68
|
+
"""
|
|
69
|
+
if name not in self._tools:
|
|
70
|
+
raise ValueError(f"Tool {name} not found")
|
|
71
|
+
return self._tools[name]()
|
|
72
|
+
|
|
73
|
+
def get_tool_execution_order(
|
|
74
|
+
self,
|
|
75
|
+
tool_list: list[ToolEnum],
|
|
76
|
+
ignore_conflicts: bool = False,
|
|
77
|
+
) -> list[ToolEnum]:
|
|
78
|
+
"""Get the order in which tools should be executed.
|
|
79
|
+
|
|
80
|
+
This method takes into account:
|
|
81
|
+
- Tool conflicts
|
|
82
|
+
- Alphabetical ordering
|
|
83
|
+
- Tool dependencies
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
tool_list: List of core names to execute
|
|
87
|
+
ignore_conflicts: Whether to ignore core conflicts
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
List of core names in alphabetical execution order
|
|
91
|
+
"""
|
|
92
|
+
if not tool_list:
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
# Get core instances
|
|
96
|
+
tools = {name: self.get_tool(name) for name in tool_list}
|
|
97
|
+
|
|
98
|
+
# Sort tools alphabetically by name
|
|
99
|
+
if ignore_conflicts:
|
|
100
|
+
return sorted(
|
|
101
|
+
tool_list,
|
|
102
|
+
key=lambda name: name.name,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Build conflict graph
|
|
106
|
+
conflict_graph: dict[ToolEnum, set[ToolEnum]] = {
|
|
107
|
+
name: set() for name in tool_list
|
|
108
|
+
}
|
|
109
|
+
for name, tool in tools.items():
|
|
110
|
+
for conflict in tool.config.conflicts_with:
|
|
111
|
+
if conflict in tool_list:
|
|
112
|
+
conflict_graph[name].add(conflict)
|
|
113
|
+
conflict_graph[conflict].add(name)
|
|
114
|
+
|
|
115
|
+
# Sort tools alphabetically by name
|
|
116
|
+
sorted_tools = sorted(
|
|
117
|
+
tool_list,
|
|
118
|
+
key=lambda name: name.name,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Resolve conflicts by keeping the first alphabetical tool
|
|
122
|
+
result = []
|
|
123
|
+
for tool_name in sorted_tools:
|
|
124
|
+
# Check if this core conflicts with any already selected tools
|
|
125
|
+
conflicts = conflict_graph[tool_name] & set(result)
|
|
126
|
+
if not conflicts:
|
|
127
|
+
result.append(tool_name)
|
|
128
|
+
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
def set_tool_options(
|
|
132
|
+
self,
|
|
133
|
+
name: ToolEnum,
|
|
134
|
+
**options: Any,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Set options for a core.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
name: The name of the core
|
|
140
|
+
**options: The options to set
|
|
141
|
+
"""
|
|
142
|
+
tool = self.get_tool(name)
|
|
143
|
+
tool.set_options(**options)
|
|
144
|
+
|
|
145
|
+
def get_available_tools(self) -> dict[ToolEnum, Tool]:
|
|
146
|
+
"""Get all available tools.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Dictionary mapping core names to core classes
|
|
150
|
+
"""
|
|
151
|
+
return {name: tool_class() for name, tool_class in self._tools.items()}
|
|
152
|
+
|
|
153
|
+
def get_check_tools(self) -> dict[ToolEnum, Tool]:
|
|
154
|
+
"""Get all tools that can check files.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Dictionary mapping core names to core instances
|
|
158
|
+
"""
|
|
159
|
+
return {name: tool_class() for name, tool_class in self._check_tools.items()}
|
|
160
|
+
|
|
161
|
+
def get_fix_tools(self) -> dict[ToolEnum, Tool]:
|
|
162
|
+
"""Get all tools that can fix files.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Dictionary mapping core names to core instances
|
|
166
|
+
"""
|
|
167
|
+
return {name: tool_class() for name, tool_class in self._fix_tools.items()}
|
|
File without changes
|