lintro 0.6.2__py3-none-any.whl → 0.17.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lintro/__init__.py +1 -1
- lintro/cli.py +230 -14
- lintro/cli_utils/commands/__init__.py +8 -1
- lintro/cli_utils/commands/check.py +1 -0
- lintro/cli_utils/commands/config.py +325 -0
- lintro/cli_utils/commands/format.py +2 -2
- lintro/cli_utils/commands/init.py +361 -0
- lintro/cli_utils/commands/list_tools.py +180 -42
- lintro/cli_utils/commands/test.py +316 -0
- lintro/cli_utils/commands/versions.py +81 -0
- lintro/config/__init__.py +62 -0
- lintro/config/config_loader.py +420 -0
- lintro/config/lintro_config.py +189 -0
- lintro/config/tool_config_generator.py +403 -0
- lintro/enums/__init__.py +1 -0
- lintro/enums/darglint_strictness.py +10 -0
- lintro/enums/hadolint_enums.py +22 -0
- lintro/enums/tool_name.py +2 -0
- lintro/enums/tool_type.py +2 -0
- lintro/enums/yamllint_format.py +11 -0
- lintro/exceptions/__init__.py +1 -0
- lintro/formatters/__init__.py +1 -0
- lintro/formatters/core/__init__.py +1 -0
- lintro/formatters/core/output_style.py +11 -0
- lintro/formatters/core/table_descriptor.py +8 -0
- lintro/formatters/styles/csv.py +2 -0
- lintro/formatters/styles/grid.py +2 -0
- lintro/formatters/styles/html.py +2 -0
- lintro/formatters/styles/json.py +2 -0
- lintro/formatters/styles/markdown.py +2 -0
- lintro/formatters/styles/plain.py +2 -0
- lintro/formatters/tools/__init__.py +12 -0
- lintro/formatters/tools/black_formatter.py +27 -5
- lintro/formatters/tools/darglint_formatter.py +16 -1
- lintro/formatters/tools/eslint_formatter.py +108 -0
- lintro/formatters/tools/hadolint_formatter.py +13 -0
- lintro/formatters/tools/markdownlint_formatter.py +88 -0
- lintro/formatters/tools/prettier_formatter.py +15 -0
- lintro/formatters/tools/pytest_formatter.py +201 -0
- lintro/formatters/tools/ruff_formatter.py +26 -5
- lintro/formatters/tools/yamllint_formatter.py +14 -1
- lintro/models/__init__.py +1 -0
- lintro/models/core/__init__.py +1 -0
- lintro/models/core/tool_config.py +11 -7
- lintro/parsers/__init__.py +69 -9
- lintro/parsers/actionlint/actionlint_parser.py +1 -1
- lintro/parsers/bandit/__init__.py +6 -0
- lintro/parsers/bandit/bandit_issue.py +49 -0
- lintro/parsers/bandit/bandit_parser.py +99 -0
- lintro/parsers/black/black_issue.py +4 -0
- lintro/parsers/darglint/__init__.py +1 -0
- lintro/parsers/darglint/darglint_issue.py +11 -0
- lintro/parsers/eslint/__init__.py +6 -0
- lintro/parsers/eslint/eslint_issue.py +26 -0
- lintro/parsers/eslint/eslint_parser.py +63 -0
- lintro/parsers/markdownlint/__init__.py +6 -0
- lintro/parsers/markdownlint/markdownlint_issue.py +22 -0
- lintro/parsers/markdownlint/markdownlint_parser.py +113 -0
- lintro/parsers/prettier/__init__.py +1 -0
- lintro/parsers/prettier/prettier_issue.py +12 -0
- lintro/parsers/prettier/prettier_parser.py +1 -1
- lintro/parsers/pytest/__init__.py +21 -0
- lintro/parsers/pytest/pytest_issue.py +28 -0
- lintro/parsers/pytest/pytest_parser.py +483 -0
- lintro/parsers/ruff/ruff_parser.py +6 -2
- lintro/parsers/yamllint/__init__.py +1 -0
- lintro/tools/__init__.py +3 -1
- lintro/tools/core/__init__.py +1 -0
- lintro/tools/core/timeout_utils.py +112 -0
- lintro/tools/core/tool_base.py +286 -50
- lintro/tools/core/tool_manager.py +77 -24
- lintro/tools/core/version_requirements.py +482 -0
- lintro/tools/implementations/__init__.py +1 -0
- lintro/tools/implementations/pytest/pytest_command_builder.py +311 -0
- lintro/tools/implementations/pytest/pytest_config.py +200 -0
- lintro/tools/implementations/pytest/pytest_error_handler.py +128 -0
- lintro/tools/implementations/pytest/pytest_executor.py +122 -0
- lintro/tools/implementations/pytest/pytest_handlers.py +375 -0
- lintro/tools/implementations/pytest/pytest_option_validators.py +212 -0
- lintro/tools/implementations/pytest/pytest_output_processor.py +408 -0
- lintro/tools/implementations/pytest/pytest_result_processor.py +113 -0
- lintro/tools/implementations/pytest/pytest_utils.py +697 -0
- lintro/tools/implementations/tool_actionlint.py +106 -16
- lintro/tools/implementations/tool_bandit.py +34 -29
- lintro/tools/implementations/tool_black.py +236 -29
- lintro/tools/implementations/tool_darglint.py +183 -22
- lintro/tools/implementations/tool_eslint.py +374 -0
- lintro/tools/implementations/tool_hadolint.py +94 -25
- lintro/tools/implementations/tool_markdownlint.py +354 -0
- lintro/tools/implementations/tool_prettier.py +317 -24
- lintro/tools/implementations/tool_pytest.py +327 -0
- lintro/tools/implementations/tool_ruff.py +278 -84
- lintro/tools/implementations/tool_yamllint.py +448 -34
- lintro/tools/tool_enum.py +8 -0
- lintro/utils/__init__.py +1 -0
- lintro/utils/ascii_normalize_cli.py +5 -0
- lintro/utils/config.py +41 -18
- lintro/utils/console_logger.py +211 -25
- lintro/utils/path_utils.py +42 -0
- lintro/utils/tool_executor.py +339 -45
- lintro/utils/tool_utils.py +51 -24
- lintro/utils/unified_config.py +926 -0
- {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/METADATA +172 -30
- lintro-0.17.2.dist-info/RECORD +134 -0
- lintro-0.6.2.dist-info/RECORD +0 -96
- {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
- {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
- {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
- {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
"""Unified configuration manager for Lintro.
|
|
2
|
+
|
|
3
|
+
This module provides a centralized configuration system that:
|
|
4
|
+
1. Reads global settings from [tool.lintro]
|
|
5
|
+
2. Reads native tool configs (for comparison/validation)
|
|
6
|
+
3. Reads tool-specific overrides from [tool.lintro.<tool>]
|
|
7
|
+
4. Computes effective config per tool with clear priority rules
|
|
8
|
+
5. Warns about inconsistencies between configs
|
|
9
|
+
6. Manages tool execution order (priority-based or alphabetical)
|
|
10
|
+
|
|
11
|
+
Priority order (highest to lowest):
|
|
12
|
+
1. CLI --tool-options (always wins)
|
|
13
|
+
2. [tool.lintro.<tool>] in pyproject.toml
|
|
14
|
+
3. [tool.lintro] global settings in pyproject.toml
|
|
15
|
+
4. Native tool config (e.g., .prettierrc, [tool.ruff])
|
|
16
|
+
5. Tool defaults
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import tomllib
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from enum import StrEnum, auto
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from loguru import logger
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
import yaml
|
|
32
|
+
except ImportError:
|
|
33
|
+
yaml = None # type: ignore[assignment]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _strip_jsonc_comments(content: str) -> str:
|
|
37
|
+
"""Strip JSONC comments from content, preserving strings.
|
|
38
|
+
|
|
39
|
+
This function safely removes // and /* */ comments from JSONC content
|
|
40
|
+
while preserving comment-like sequences inside string values.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
content: JSONC content as string
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Content with comments stripped
|
|
47
|
+
|
|
48
|
+
Note:
|
|
49
|
+
This is a simple implementation that handles most common cases.
|
|
50
|
+
For complex JSONC with nested comments or edge cases, consider
|
|
51
|
+
using a proper JSONC parser library (e.g., json5 or commentjson).
|
|
52
|
+
"""
|
|
53
|
+
result: list[str] = []
|
|
54
|
+
i = 0
|
|
55
|
+
content_len = len(content)
|
|
56
|
+
in_string = False
|
|
57
|
+
escape_next = False
|
|
58
|
+
in_block_comment = False
|
|
59
|
+
|
|
60
|
+
while i < content_len:
|
|
61
|
+
char = content[i]
|
|
62
|
+
|
|
63
|
+
if escape_next:
|
|
64
|
+
escape_next = False
|
|
65
|
+
if not in_block_comment:
|
|
66
|
+
result.append(char)
|
|
67
|
+
i += 1
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if char == "\\" and in_string:
|
|
71
|
+
escape_next = True
|
|
72
|
+
if not in_block_comment:
|
|
73
|
+
result.append(char)
|
|
74
|
+
i += 1
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
if char == '"' and not in_block_comment:
|
|
78
|
+
in_string = not in_string
|
|
79
|
+
result.append(char)
|
|
80
|
+
i += 1
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
if in_string:
|
|
84
|
+
result.append(char)
|
|
85
|
+
i += 1
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
# Check for block comment start /* ... */
|
|
89
|
+
if i < content_len - 1 and char == "/" and content[i + 1] == "*":
|
|
90
|
+
in_block_comment = True
|
|
91
|
+
i += 2
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Check for block comment end */
|
|
95
|
+
if (
|
|
96
|
+
i > 0
|
|
97
|
+
and i < content_len
|
|
98
|
+
and char == "/"
|
|
99
|
+
and content[i - 1] == "*"
|
|
100
|
+
and in_block_comment
|
|
101
|
+
):
|
|
102
|
+
in_block_comment = False
|
|
103
|
+
i += 1
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
# Check for line comment //
|
|
107
|
+
if (
|
|
108
|
+
i < content_len - 1
|
|
109
|
+
and char == "/"
|
|
110
|
+
and content[i + 1] == "/"
|
|
111
|
+
and not in_block_comment
|
|
112
|
+
):
|
|
113
|
+
# Skip to end of line
|
|
114
|
+
while i < content_len and content[i] != "\n":
|
|
115
|
+
i += 1
|
|
116
|
+
# Include the newline if present
|
|
117
|
+
if i < content_len:
|
|
118
|
+
result.append("\n")
|
|
119
|
+
i += 1
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
if not in_block_comment:
|
|
123
|
+
result.append(char)
|
|
124
|
+
|
|
125
|
+
i += 1
|
|
126
|
+
|
|
127
|
+
if in_block_comment:
|
|
128
|
+
logger.warning("Unclosed block comment in JSONC content")
|
|
129
|
+
|
|
130
|
+
return "".join(result)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ToolOrderStrategy(StrEnum):
|
|
134
|
+
"""Strategy for ordering tool execution."""
|
|
135
|
+
|
|
136
|
+
PRIORITY = auto() # Use tool priority values (formatters before linters)
|
|
137
|
+
ALPHABETICAL = auto() # Alphabetical by tool name
|
|
138
|
+
CUSTOM = auto() # Custom order defined in config
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class ToolConfigInfo:
|
|
143
|
+
"""Information about a tool's configuration sources.
|
|
144
|
+
|
|
145
|
+
Attributes:
|
|
146
|
+
tool_name: Name of the tool
|
|
147
|
+
native_config: Configuration from native tool config files
|
|
148
|
+
lintro_tool_config: Configuration from [tool.lintro.<tool>]
|
|
149
|
+
effective_config: Computed effective configuration
|
|
150
|
+
warnings: List of warnings about configuration issues
|
|
151
|
+
is_injectable: Whether Lintro can inject config to this tool
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
tool_name: str
|
|
155
|
+
native_config: dict[str, Any] = field(default_factory=dict)
|
|
156
|
+
lintro_tool_config: dict[str, Any] = field(default_factory=dict)
|
|
157
|
+
effective_config: dict[str, Any] = field(default_factory=dict)
|
|
158
|
+
warnings: list[str] = field(default_factory=list)
|
|
159
|
+
is_injectable: bool = True
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# Global settings that Lintro can manage across tools
|
|
163
|
+
# Each setting maps to tool-specific config keys and indicates which tools
|
|
164
|
+
# support injection via Lintro config (vs requiring native config files)
|
|
165
|
+
GLOBAL_SETTINGS: dict[str, dict[str, Any]] = {
|
|
166
|
+
"line_length": {
|
|
167
|
+
"tools": {
|
|
168
|
+
"ruff": "line-length",
|
|
169
|
+
"black": "line-length",
|
|
170
|
+
"markdownlint": "config.MD013.line_length", # Nested in config object
|
|
171
|
+
"prettier": "printWidth",
|
|
172
|
+
"yamllint": "rules.line-length.max", # Nested under rules.line-length.max
|
|
173
|
+
},
|
|
174
|
+
"injectable": {
|
|
175
|
+
"ruff",
|
|
176
|
+
"black",
|
|
177
|
+
"markdownlint",
|
|
178
|
+
"prettier",
|
|
179
|
+
"yamllint",
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
"target_python": {
|
|
183
|
+
"tools": {
|
|
184
|
+
"ruff": "target-version",
|
|
185
|
+
"black": "target-version",
|
|
186
|
+
},
|
|
187
|
+
"injectable": {"ruff", "black"},
|
|
188
|
+
},
|
|
189
|
+
"indent_size": {
|
|
190
|
+
"tools": {
|
|
191
|
+
"prettier": "tabWidth",
|
|
192
|
+
"ruff": "indent-width",
|
|
193
|
+
},
|
|
194
|
+
"injectable": {"prettier", "ruff"},
|
|
195
|
+
},
|
|
196
|
+
"quote_style": {
|
|
197
|
+
"tools": {
|
|
198
|
+
"ruff": "quote-style", # Under [tool.ruff.format]
|
|
199
|
+
"prettier": "singleQuote", # Boolean: true for single quotes
|
|
200
|
+
},
|
|
201
|
+
"injectable": {"ruff", "prettier"},
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Default tool priorities (lower = runs first)
|
|
206
|
+
# Formatters run before linters to avoid false positives
|
|
207
|
+
DEFAULT_TOOL_PRIORITIES: dict[str, int] = {
|
|
208
|
+
"prettier": 10, # Formatter - runs first
|
|
209
|
+
"black": 15, # Formatter
|
|
210
|
+
"ruff": 20, # Linter/Formatter hybrid
|
|
211
|
+
"markdownlint": 30, # Linter
|
|
212
|
+
"yamllint": 35, # Linter
|
|
213
|
+
"darglint": 40, # Linter
|
|
214
|
+
"bandit": 45, # Security linter
|
|
215
|
+
"eslint": 50, # JavaScript/TypeScript linter
|
|
216
|
+
"hadolint": 50, # Docker linter
|
|
217
|
+
"actionlint": 55, # GitHub Actions linter
|
|
218
|
+
"pytest": 100, # Test runner - runs last
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _load_pyproject() -> dict[str, Any]:
|
|
223
|
+
"""Load the full pyproject.toml.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Full pyproject.toml contents as dict
|
|
227
|
+
"""
|
|
228
|
+
pyproject_path = Path("pyproject.toml")
|
|
229
|
+
if not pyproject_path.exists():
|
|
230
|
+
return {}
|
|
231
|
+
try:
|
|
232
|
+
with pyproject_path.open("rb") as f:
|
|
233
|
+
return tomllib.load(f)
|
|
234
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
235
|
+
return {}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _load_native_tool_config(tool_name: str) -> dict[str, Any]:
|
|
239
|
+
"""Load native configuration for a specific tool.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
tool_name: Name of the tool
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Native configuration dictionary
|
|
246
|
+
"""
|
|
247
|
+
pyproject = _load_pyproject()
|
|
248
|
+
tool_section = pyproject.get("tool", {})
|
|
249
|
+
|
|
250
|
+
# Tools with pyproject.toml config
|
|
251
|
+
if tool_name in ("ruff", "black", "bandit"):
|
|
252
|
+
return tool_section.get(tool_name, {})
|
|
253
|
+
|
|
254
|
+
# Yamllint: check native config files (not pyproject.toml)
|
|
255
|
+
if tool_name == "yamllint":
|
|
256
|
+
yamllint_config_files = [".yamllint", ".yamllint.yaml", ".yamllint.yml"]
|
|
257
|
+
for config_file in yamllint_config_files:
|
|
258
|
+
config_path = Path(config_file)
|
|
259
|
+
if config_path.exists():
|
|
260
|
+
if yaml is None:
|
|
261
|
+
logger.debug(
|
|
262
|
+
f"[UnifiedConfig] Found {config_file} but yaml not installed",
|
|
263
|
+
)
|
|
264
|
+
return {}
|
|
265
|
+
try:
|
|
266
|
+
with config_path.open(encoding="utf-8") as f:
|
|
267
|
+
content = yaml.safe_load(f)
|
|
268
|
+
return content if isinstance(content, dict) else {}
|
|
269
|
+
except (yaml.YAMLError, OSError) as e:
|
|
270
|
+
logger.debug(f"[UnifiedConfig] Failed to parse {config_file}: {e}")
|
|
271
|
+
return {}
|
|
272
|
+
|
|
273
|
+
# Prettier: check multiple config file formats
|
|
274
|
+
if tool_name == "prettier":
|
|
275
|
+
for config_file in [".prettierrc", ".prettierrc.json", "prettier.config.js"]:
|
|
276
|
+
config_path = Path(config_file)
|
|
277
|
+
if config_path.exists() and config_file.endswith(".json"):
|
|
278
|
+
try:
|
|
279
|
+
with config_path.open(encoding="utf-8") as f:
|
|
280
|
+
return json.load(f)
|
|
281
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
282
|
+
pass
|
|
283
|
+
elif config_path.exists() and config_file == ".prettierrc":
|
|
284
|
+
# Try parsing as JSON (common format)
|
|
285
|
+
try:
|
|
286
|
+
with config_path.open(encoding="utf-8") as f:
|
|
287
|
+
return json.load(f)
|
|
288
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
289
|
+
pass
|
|
290
|
+
# Check package.json prettier field
|
|
291
|
+
pkg_path = Path("package.json")
|
|
292
|
+
if pkg_path.exists():
|
|
293
|
+
try:
|
|
294
|
+
with pkg_path.open(encoding="utf-8") as f:
|
|
295
|
+
pkg = json.load(f)
|
|
296
|
+
if "prettier" in pkg:
|
|
297
|
+
return pkg["prettier"]
|
|
298
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
299
|
+
pass
|
|
300
|
+
return {}
|
|
301
|
+
|
|
302
|
+
# ESLint: check multiple config file formats
|
|
303
|
+
if tool_name == "eslint":
|
|
304
|
+
# Check flat config (ESLint 9+)
|
|
305
|
+
flat_config = Path("eslint.config.js")
|
|
306
|
+
if flat_config.exists():
|
|
307
|
+
# Note: We can't easily parse JS files, so return empty dict
|
|
308
|
+
# The tool will use the config natively
|
|
309
|
+
return {}
|
|
310
|
+
# Check legacy config files
|
|
311
|
+
for config_file in [
|
|
312
|
+
".eslintrc.js",
|
|
313
|
+
".eslintrc.json",
|
|
314
|
+
".eslintrc.yaml",
|
|
315
|
+
".eslintrc.yml",
|
|
316
|
+
]:
|
|
317
|
+
config_path = Path(config_file)
|
|
318
|
+
if not config_path.exists():
|
|
319
|
+
continue
|
|
320
|
+
# Handle JSON files
|
|
321
|
+
if config_file.endswith(".json"):
|
|
322
|
+
try:
|
|
323
|
+
with config_path.open(encoding="utf-8") as f:
|
|
324
|
+
return json.load(f)
|
|
325
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
326
|
+
pass
|
|
327
|
+
# Handle YAML files
|
|
328
|
+
elif config_file.endswith((".yaml", ".yml")):
|
|
329
|
+
if yaml is None:
|
|
330
|
+
logger.debug(
|
|
331
|
+
f"[UnifiedConfig] Found {config_file} but yaml not installed",
|
|
332
|
+
)
|
|
333
|
+
continue
|
|
334
|
+
try:
|
|
335
|
+
with config_path.open(encoding="utf-8") as f:
|
|
336
|
+
content = yaml.safe_load(f)
|
|
337
|
+
return content if isinstance(content, dict) else {}
|
|
338
|
+
except (yaml.YAMLError, OSError) as e:
|
|
339
|
+
logger.debug(f"[UnifiedConfig] Failed to parse {config_file}: {e}")
|
|
340
|
+
# Check package.json eslintConfig field
|
|
341
|
+
pkg_path = Path("package.json")
|
|
342
|
+
if pkg_path.exists():
|
|
343
|
+
try:
|
|
344
|
+
with pkg_path.open(encoding="utf-8") as f:
|
|
345
|
+
pkg = json.load(f)
|
|
346
|
+
if "eslintConfig" in pkg:
|
|
347
|
+
return pkg["eslintConfig"]
|
|
348
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
349
|
+
pass
|
|
350
|
+
return {}
|
|
351
|
+
|
|
352
|
+
# Markdownlint: check config files
|
|
353
|
+
if tool_name == "markdownlint":
|
|
354
|
+
for config_file in [
|
|
355
|
+
".markdownlint.json",
|
|
356
|
+
".markdownlint.yaml",
|
|
357
|
+
".markdownlint.yml",
|
|
358
|
+
".markdownlint.jsonc",
|
|
359
|
+
]:
|
|
360
|
+
config_path = Path(config_file)
|
|
361
|
+
if not config_path.exists():
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
# Handle JSON/JSONC files
|
|
365
|
+
if config_file.endswith((".json", ".jsonc")):
|
|
366
|
+
try:
|
|
367
|
+
with config_path.open(encoding="utf-8") as f:
|
|
368
|
+
content = f.read()
|
|
369
|
+
# Strip JSONC comments safely (preserves strings)
|
|
370
|
+
content = _strip_jsonc_comments(content)
|
|
371
|
+
return json.loads(content)
|
|
372
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
373
|
+
pass
|
|
374
|
+
|
|
375
|
+
# Handle YAML files
|
|
376
|
+
elif config_file.endswith((".yaml", ".yml")):
|
|
377
|
+
if yaml is None:
|
|
378
|
+
logger.warning(
|
|
379
|
+
"PyYAML not available; cannot parse .markdownlint.yaml",
|
|
380
|
+
)
|
|
381
|
+
continue
|
|
382
|
+
try:
|
|
383
|
+
with config_path.open(encoding="utf-8") as f:
|
|
384
|
+
content = yaml.safe_load(f)
|
|
385
|
+
# Handle multi-document YAML (coerce to dict)
|
|
386
|
+
if isinstance(content, list) and len(content) > 0:
|
|
387
|
+
content = content[0]
|
|
388
|
+
if isinstance(content, dict):
|
|
389
|
+
return content
|
|
390
|
+
except Exception as e: # noqa: BLE001
|
|
391
|
+
# Catch yaml.YAMLError and other exceptions
|
|
392
|
+
# (file I/O, parsing errors)
|
|
393
|
+
# Continue to next config file if this one fails to parse
|
|
394
|
+
logger.debug(
|
|
395
|
+
f"Failed to parse {config_path}: {type(e).__name__}",
|
|
396
|
+
)
|
|
397
|
+
pass
|
|
398
|
+
return {}
|
|
399
|
+
|
|
400
|
+
return {}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _get_nested_value(config: dict[str, Any], key_path: str) -> Any:
|
|
404
|
+
"""Get a nested value from a config dict using dot notation.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
config: Configuration dictionary
|
|
408
|
+
key_path: Dot-separated key path (e.g., "line-length.max")
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Value at path, or None if not found
|
|
412
|
+
"""
|
|
413
|
+
keys = key_path.split(".")
|
|
414
|
+
current = config
|
|
415
|
+
for key in keys:
|
|
416
|
+
if isinstance(current, dict) and key in current:
|
|
417
|
+
current = current[key]
|
|
418
|
+
else:
|
|
419
|
+
return None
|
|
420
|
+
return current
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def load_lintro_global_config() -> dict[str, Any]:
|
|
424
|
+
"""Load global Lintro configuration from [tool.lintro].
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Global configuration dictionary (excludes tool-specific sections)
|
|
428
|
+
"""
|
|
429
|
+
pyproject = _load_pyproject()
|
|
430
|
+
lintro_config = pyproject.get("tool", {}).get("lintro", {})
|
|
431
|
+
|
|
432
|
+
# Filter out known tool-specific sections
|
|
433
|
+
tool_sections = {
|
|
434
|
+
"ruff",
|
|
435
|
+
"black",
|
|
436
|
+
"prettier",
|
|
437
|
+
"yamllint",
|
|
438
|
+
"markdownlint",
|
|
439
|
+
"markdownlint-cli2",
|
|
440
|
+
"bandit",
|
|
441
|
+
"darglint",
|
|
442
|
+
"hadolint",
|
|
443
|
+
"actionlint",
|
|
444
|
+
"pytest",
|
|
445
|
+
"post_checks",
|
|
446
|
+
"versions",
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {k: v for k, v in lintro_config.items() if k not in tool_sections}
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def load_lintro_tool_config(tool_name: str) -> dict[str, Any]:
|
|
453
|
+
"""Load tool-specific Lintro config from [tool.lintro.<tool>].
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
tool_name: Name of the tool
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Tool-specific Lintro configuration
|
|
460
|
+
"""
|
|
461
|
+
pyproject = _load_pyproject()
|
|
462
|
+
lintro_config = pyproject.get("tool", {}).get("lintro", {})
|
|
463
|
+
return lintro_config.get(tool_name, {})
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def get_tool_order_config() -> dict[str, Any]:
|
|
467
|
+
"""Get tool ordering configuration from [tool.lintro].
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Tool ordering configuration with keys:
|
|
471
|
+
- strategy: "priority", "alphabetical", or "custom"
|
|
472
|
+
- custom_order: list of tool names (for custom strategy)
|
|
473
|
+
- priority_overrides: dict of tool -> priority (for priority strategy)
|
|
474
|
+
"""
|
|
475
|
+
global_config = load_lintro_global_config()
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
"strategy": global_config.get("tool_order", "priority"),
|
|
479
|
+
"custom_order": global_config.get("tool_order_custom", []),
|
|
480
|
+
"priority_overrides": global_config.get("tool_priorities", {}),
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def get_tool_priority(tool_name: str) -> int:
|
|
485
|
+
"""Get the execution priority for a tool.
|
|
486
|
+
|
|
487
|
+
Lower values run first. Formatters have lower priorities than linters.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
tool_name: Name of the tool
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Priority value (lower = runs first)
|
|
494
|
+
"""
|
|
495
|
+
order_config = get_tool_order_config()
|
|
496
|
+
priority_overrides = order_config.get("priority_overrides", {})
|
|
497
|
+
# Normalize priority_overrides keys to lowercase for consistent lookup
|
|
498
|
+
priority_overrides_normalized = {
|
|
499
|
+
k.lower(): v for k, v in priority_overrides.items()
|
|
500
|
+
}
|
|
501
|
+
tool_name_lower = tool_name.lower()
|
|
502
|
+
|
|
503
|
+
# Check for override first
|
|
504
|
+
if tool_name_lower in priority_overrides_normalized:
|
|
505
|
+
return priority_overrides_normalized[tool_name_lower]
|
|
506
|
+
|
|
507
|
+
# Use default priority
|
|
508
|
+
return DEFAULT_TOOL_PRIORITIES.get(tool_name_lower, 50)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def get_ordered_tools(
|
|
512
|
+
tool_names: list[str],
|
|
513
|
+
tool_order: str | list[str] | None = None,
|
|
514
|
+
) -> list[str]:
|
|
515
|
+
"""Get tool names in execution order based on configured strategy.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
tool_names: List of tool names to order
|
|
519
|
+
tool_order: Optional override for tool order strategy. Can be:
|
|
520
|
+
- "priority": Sort by tool priority (default)
|
|
521
|
+
- "alphabetical": Sort alphabetically
|
|
522
|
+
- list[str]: Custom order (tools in list come first)
|
|
523
|
+
- None: Read strategy from config
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
List of tool names in execution order
|
|
527
|
+
"""
|
|
528
|
+
# Determine strategy and custom order
|
|
529
|
+
if tool_order is None:
|
|
530
|
+
order_config = get_tool_order_config()
|
|
531
|
+
strategy = order_config.get("strategy", "priority")
|
|
532
|
+
custom_order = order_config.get("custom_order", [])
|
|
533
|
+
elif isinstance(tool_order, list):
|
|
534
|
+
strategy = "custom"
|
|
535
|
+
custom_order = tool_order
|
|
536
|
+
else:
|
|
537
|
+
strategy = tool_order
|
|
538
|
+
custom_order = []
|
|
539
|
+
|
|
540
|
+
if strategy == "alphabetical":
|
|
541
|
+
return sorted(tool_names, key=str.lower)
|
|
542
|
+
|
|
543
|
+
if strategy == "custom":
|
|
544
|
+
# Tools in custom_order come first (in that order), then remaining
|
|
545
|
+
# by priority
|
|
546
|
+
ordered: list[str] = []
|
|
547
|
+
remaining = list(tool_names)
|
|
548
|
+
|
|
549
|
+
for tool in custom_order:
|
|
550
|
+
# Case-insensitive matching for custom order
|
|
551
|
+
tool_lower = tool.lower()
|
|
552
|
+
for t in remaining:
|
|
553
|
+
if t.lower() == tool_lower:
|
|
554
|
+
ordered.append(t)
|
|
555
|
+
remaining.remove(t)
|
|
556
|
+
break
|
|
557
|
+
|
|
558
|
+
# Add remaining tools by priority (consistent with default strategy)
|
|
559
|
+
ordered.extend(
|
|
560
|
+
sorted(remaining, key=lambda t: (get_tool_priority(t), t.lower())),
|
|
561
|
+
)
|
|
562
|
+
return ordered
|
|
563
|
+
|
|
564
|
+
# Default: priority-based ordering
|
|
565
|
+
return sorted(tool_names, key=lambda t: (get_tool_priority(t), t.lower()))
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def get_effective_line_length(tool_name: str) -> int | None:
|
|
569
|
+
"""Get the effective line length for a specific tool.
|
|
570
|
+
|
|
571
|
+
Priority:
|
|
572
|
+
1. [tool.lintro.<tool>] line_length
|
|
573
|
+
2. [tool.lintro] line_length
|
|
574
|
+
3. [tool.ruff] line-length (as fallback source of truth)
|
|
575
|
+
4. Native tool config
|
|
576
|
+
5. None (use tool default)
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
tool_name: Name of the tool
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
Effective line length, or None to use tool default
|
|
583
|
+
"""
|
|
584
|
+
# 1. Check tool-specific lintro config
|
|
585
|
+
lintro_tool = load_lintro_tool_config(tool_name)
|
|
586
|
+
if "line_length" in lintro_tool:
|
|
587
|
+
return lintro_tool["line_length"]
|
|
588
|
+
if "line-length" in lintro_tool:
|
|
589
|
+
return lintro_tool["line-length"]
|
|
590
|
+
|
|
591
|
+
# 2. Check global lintro config
|
|
592
|
+
lintro_global = load_lintro_global_config()
|
|
593
|
+
if "line_length" in lintro_global:
|
|
594
|
+
return lintro_global["line_length"]
|
|
595
|
+
if "line-length" in lintro_global:
|
|
596
|
+
return lintro_global["line-length"]
|
|
597
|
+
|
|
598
|
+
# 3. Fall back to Ruff's line-length as source of truth
|
|
599
|
+
pyproject = _load_pyproject()
|
|
600
|
+
ruff_config = pyproject.get("tool", {}).get("ruff", {})
|
|
601
|
+
if "line-length" in ruff_config:
|
|
602
|
+
return ruff_config["line-length"]
|
|
603
|
+
if "line_length" in ruff_config:
|
|
604
|
+
return ruff_config["line_length"]
|
|
605
|
+
|
|
606
|
+
# 4. Check native tool config (for non-Ruff tools)
|
|
607
|
+
native = _load_native_tool_config(tool_name)
|
|
608
|
+
setting_key = GLOBAL_SETTINGS.get("line_length", {}).get("tools", {}).get(tool_name)
|
|
609
|
+
if setting_key:
|
|
610
|
+
native_value = _get_nested_value(native, setting_key)
|
|
611
|
+
if native_value is not None:
|
|
612
|
+
return native_value
|
|
613
|
+
|
|
614
|
+
return None
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def is_tool_injectable(tool_name: str) -> bool:
|
|
618
|
+
"""Check if Lintro can inject config to a tool.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
tool_name: Name of the tool
|
|
622
|
+
|
|
623
|
+
Returns:
|
|
624
|
+
True if Lintro can inject config via CLI or generated config file
|
|
625
|
+
"""
|
|
626
|
+
return tool_name.lower() in GLOBAL_SETTINGS["line_length"]["injectable"]
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def validate_config_consistency() -> list[str]:
|
|
630
|
+
"""Check for inconsistencies in line length settings across tools.
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
List of warning messages about inconsistent configurations
|
|
634
|
+
"""
|
|
635
|
+
warnings: list[str] = []
|
|
636
|
+
effective_line_length = get_effective_line_length("ruff")
|
|
637
|
+
|
|
638
|
+
if effective_line_length is None:
|
|
639
|
+
return warnings
|
|
640
|
+
|
|
641
|
+
# Check each tool's native config for mismatches
|
|
642
|
+
for tool_name, setting_key in GLOBAL_SETTINGS["line_length"]["tools"].items():
|
|
643
|
+
if tool_name == "ruff":
|
|
644
|
+
continue # Skip Ruff (it's the source of truth)
|
|
645
|
+
|
|
646
|
+
native = _load_native_tool_config(tool_name)
|
|
647
|
+
native_value = _get_nested_value(native, setting_key)
|
|
648
|
+
|
|
649
|
+
if native_value is not None and native_value != effective_line_length:
|
|
650
|
+
injectable = is_tool_injectable(tool_name)
|
|
651
|
+
if injectable:
|
|
652
|
+
warnings.append(
|
|
653
|
+
f"{tool_name}: Native config has {setting_key}={native_value}, "
|
|
654
|
+
f"but Lintro will override with {effective_line_length}",
|
|
655
|
+
)
|
|
656
|
+
else:
|
|
657
|
+
warnings.append(
|
|
658
|
+
f"⚠️ {tool_name}: Native config has {setting_key}={native_value}, "
|
|
659
|
+
f"differs from central line_length={effective_line_length}. "
|
|
660
|
+
f"Lintro cannot override this tool's native config - "
|
|
661
|
+
f"update manually for consistency.",
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
return warnings
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def get_tool_config_summary() -> dict[str, ToolConfigInfo]:
|
|
668
|
+
"""Get a summary of configuration for all tools.
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
Dictionary mapping tool names to their config info
|
|
672
|
+
"""
|
|
673
|
+
tools = [
|
|
674
|
+
"ruff",
|
|
675
|
+
"black",
|
|
676
|
+
"prettier",
|
|
677
|
+
"yamllint",
|
|
678
|
+
"markdownlint",
|
|
679
|
+
"darglint",
|
|
680
|
+
"bandit",
|
|
681
|
+
"hadolint",
|
|
682
|
+
"actionlint",
|
|
683
|
+
]
|
|
684
|
+
summary: dict[str, ToolConfigInfo] = {}
|
|
685
|
+
|
|
686
|
+
for tool_name in tools:
|
|
687
|
+
info = ToolConfigInfo(
|
|
688
|
+
tool_name=tool_name,
|
|
689
|
+
native_config=_load_native_tool_config(tool_name),
|
|
690
|
+
lintro_tool_config=load_lintro_tool_config(tool_name),
|
|
691
|
+
is_injectable=is_tool_injectable(tool_name),
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
# Compute effective line_length
|
|
695
|
+
effective_ll = get_effective_line_length(tool_name)
|
|
696
|
+
if effective_ll is not None:
|
|
697
|
+
info.effective_config["line_length"] = effective_ll
|
|
698
|
+
|
|
699
|
+
summary[tool_name] = info
|
|
700
|
+
|
|
701
|
+
# Add warnings
|
|
702
|
+
warnings = validate_config_consistency()
|
|
703
|
+
for warning in warnings:
|
|
704
|
+
for tool_name in tools:
|
|
705
|
+
if tool_name in warning.lower():
|
|
706
|
+
summary[tool_name].warnings.append(warning)
|
|
707
|
+
break
|
|
708
|
+
|
|
709
|
+
return summary
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def get_config_report() -> str:
|
|
713
|
+
"""Generate a configuration report as a string.
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
Formatted configuration report
|
|
717
|
+
"""
|
|
718
|
+
summary = get_tool_config_summary()
|
|
719
|
+
central_ll = get_effective_line_length("ruff")
|
|
720
|
+
order_config = get_tool_order_config()
|
|
721
|
+
|
|
722
|
+
lines: list[str] = []
|
|
723
|
+
lines.append("=" * 60)
|
|
724
|
+
lines.append("LINTRO CONFIGURATION REPORT")
|
|
725
|
+
lines.append("=" * 60)
|
|
726
|
+
lines.append("")
|
|
727
|
+
|
|
728
|
+
# Global settings section
|
|
729
|
+
lines.append("── Global Settings ──")
|
|
730
|
+
lines.append(f" Central line_length: {central_ll or 'Not configured'}")
|
|
731
|
+
lines.append(f" Tool order strategy: {order_config.get('strategy', 'priority')}")
|
|
732
|
+
if order_config.get("custom_order"):
|
|
733
|
+
lines.append(f" Custom order: {', '.join(order_config['custom_order'])}")
|
|
734
|
+
lines.append("")
|
|
735
|
+
|
|
736
|
+
# Tool execution order section
|
|
737
|
+
lines.append("── Tool Execution Order ──")
|
|
738
|
+
tool_names = list(summary.keys())
|
|
739
|
+
ordered_tools = get_ordered_tools(tool_names)
|
|
740
|
+
for idx, tool_name in enumerate(ordered_tools, 1):
|
|
741
|
+
priority = get_tool_priority(tool_name)
|
|
742
|
+
lines.append(f" {idx}. {tool_name} (priority: {priority})")
|
|
743
|
+
lines.append("")
|
|
744
|
+
|
|
745
|
+
# Per-tool configuration section
|
|
746
|
+
lines.append("── Per-Tool Configuration ──")
|
|
747
|
+
for tool_name, info in summary.items():
|
|
748
|
+
injectable = "✅ Syncable" if info.is_injectable else "⚠️ Native only"
|
|
749
|
+
effective = info.effective_config.get("line_length", "default")
|
|
750
|
+
lines.append(f" {tool_name}:")
|
|
751
|
+
lines.append(f" Status: {injectable}")
|
|
752
|
+
lines.append(f" Effective line_length: {effective}")
|
|
753
|
+
if info.lintro_tool_config:
|
|
754
|
+
lines.append(f" Lintro config: {info.lintro_tool_config}")
|
|
755
|
+
if info.native_config and tool_name not in ("ruff", "black", "bandit"):
|
|
756
|
+
# Only show native config for tools with external config files
|
|
757
|
+
lines.append(f" Native config: {info.native_config}")
|
|
758
|
+
lines.append("")
|
|
759
|
+
|
|
760
|
+
# Warnings section
|
|
761
|
+
all_warnings = validate_config_consistency()
|
|
762
|
+
if all_warnings:
|
|
763
|
+
lines.append("── Configuration Warnings ──")
|
|
764
|
+
for warning in all_warnings:
|
|
765
|
+
lines.append(f" {warning}")
|
|
766
|
+
lines.append("")
|
|
767
|
+
else:
|
|
768
|
+
lines.append("── Configuration Warnings ──")
|
|
769
|
+
lines.append(" None - all configs consistent!")
|
|
770
|
+
lines.append("")
|
|
771
|
+
|
|
772
|
+
lines.append("=" * 60)
|
|
773
|
+
return "\n".join(lines)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def print_config_report() -> None:
|
|
777
|
+
"""Print a report of configuration status for all tools."""
|
|
778
|
+
report = get_config_report()
|
|
779
|
+
for line in report.split("\n"):
|
|
780
|
+
if line.startswith("⚠️") or "Warning" in line:
|
|
781
|
+
logger.warning(line)
|
|
782
|
+
else:
|
|
783
|
+
logger.info(line)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
@dataclass
|
|
787
|
+
class UnifiedConfigManager:
|
|
788
|
+
"""Central configuration manager for Lintro.
|
|
789
|
+
|
|
790
|
+
This class provides a unified interface for:
|
|
791
|
+
- Loading and merging configurations from multiple sources
|
|
792
|
+
- Computing effective configurations for each tool
|
|
793
|
+
- Validating configuration consistency
|
|
794
|
+
- Managing tool execution order
|
|
795
|
+
|
|
796
|
+
Attributes:
|
|
797
|
+
global_config: Global Lintro configuration from [tool.lintro]
|
|
798
|
+
tool_configs: Per-tool configuration info
|
|
799
|
+
warnings: List of configuration warnings
|
|
800
|
+
"""
|
|
801
|
+
|
|
802
|
+
global_config: dict[str, Any] = field(default_factory=dict)
|
|
803
|
+
tool_configs: dict[str, ToolConfigInfo] = field(default_factory=dict)
|
|
804
|
+
warnings: list[str] = field(default_factory=list)
|
|
805
|
+
|
|
806
|
+
def __post_init__(self) -> None:
|
|
807
|
+
"""Initialize configuration manager."""
|
|
808
|
+
self.refresh()
|
|
809
|
+
|
|
810
|
+
def refresh(self) -> None:
|
|
811
|
+
"""Reload all configuration from files."""
|
|
812
|
+
self.global_config = load_lintro_global_config()
|
|
813
|
+
self.tool_configs = get_tool_config_summary()
|
|
814
|
+
self.warnings = validate_config_consistency()
|
|
815
|
+
|
|
816
|
+
def get_effective_line_length(self, tool_name: str) -> int | None:
|
|
817
|
+
"""Get effective line length for a tool.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
tool_name: Name of the tool
|
|
821
|
+
|
|
822
|
+
Returns:
|
|
823
|
+
Effective line length or None
|
|
824
|
+
"""
|
|
825
|
+
return get_effective_line_length(tool_name)
|
|
826
|
+
|
|
827
|
+
def get_tool_config(self, tool_name: str) -> ToolConfigInfo:
|
|
828
|
+
"""Get configuration info for a specific tool.
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
tool_name: Name of the tool
|
|
832
|
+
|
|
833
|
+
Returns:
|
|
834
|
+
Tool configuration info
|
|
835
|
+
"""
|
|
836
|
+
if tool_name not in self.tool_configs:
|
|
837
|
+
self.tool_configs[tool_name] = ToolConfigInfo(
|
|
838
|
+
tool_name=tool_name,
|
|
839
|
+
native_config=_load_native_tool_config(tool_name),
|
|
840
|
+
lintro_tool_config=load_lintro_tool_config(tool_name),
|
|
841
|
+
is_injectable=is_tool_injectable(tool_name),
|
|
842
|
+
)
|
|
843
|
+
return self.tool_configs[tool_name]
|
|
844
|
+
|
|
845
|
+
def get_ordered_tools(self, tool_names: list[str]) -> list[str]:
|
|
846
|
+
"""Get tools in execution order.
|
|
847
|
+
|
|
848
|
+
Args:
|
|
849
|
+
tool_names: List of tool names
|
|
850
|
+
|
|
851
|
+
Returns:
|
|
852
|
+
List of tool names in execution order
|
|
853
|
+
"""
|
|
854
|
+
return get_ordered_tools(tool_names)
|
|
855
|
+
|
|
856
|
+
def apply_config_to_tool(
|
|
857
|
+
self,
|
|
858
|
+
tool: Any,
|
|
859
|
+
cli_overrides: dict[str, Any] | None = None,
|
|
860
|
+
) -> None:
|
|
861
|
+
"""Apply effective configuration to a tool instance.
|
|
862
|
+
|
|
863
|
+
Priority order:
|
|
864
|
+
1. CLI overrides (if provided)
|
|
865
|
+
2. [tool.lintro.<tool>] config
|
|
866
|
+
3. Global [tool.lintro] settings
|
|
867
|
+
|
|
868
|
+
Args:
|
|
869
|
+
tool: Tool instance with set_options method
|
|
870
|
+
cli_overrides: Optional CLI override options
|
|
871
|
+
|
|
872
|
+
Raises:
|
|
873
|
+
TypeError: If tool configuration has type mismatches.
|
|
874
|
+
ValueError: If tool configuration has invalid values.
|
|
875
|
+
"""
|
|
876
|
+
tool_name = getattr(tool, "name", "").lower()
|
|
877
|
+
if not tool_name:
|
|
878
|
+
return
|
|
879
|
+
|
|
880
|
+
# Start with global settings
|
|
881
|
+
effective_opts: dict[str, Any] = {}
|
|
882
|
+
|
|
883
|
+
# Apply global line_length if tool supports it
|
|
884
|
+
if is_tool_injectable(tool_name):
|
|
885
|
+
line_length = self.get_effective_line_length(tool_name)
|
|
886
|
+
if line_length is not None:
|
|
887
|
+
effective_opts["line_length"] = line_length
|
|
888
|
+
|
|
889
|
+
# Apply tool-specific lintro config
|
|
890
|
+
lintro_tool_config = load_lintro_tool_config(tool_name)
|
|
891
|
+
effective_opts.update(lintro_tool_config)
|
|
892
|
+
|
|
893
|
+
# Apply CLI overrides last (highest priority)
|
|
894
|
+
if cli_overrides:
|
|
895
|
+
effective_opts.update(cli_overrides)
|
|
896
|
+
|
|
897
|
+
# Apply to tool
|
|
898
|
+
if effective_opts:
|
|
899
|
+
try:
|
|
900
|
+
tool.set_options(**effective_opts)
|
|
901
|
+
logger.debug(f"Applied config to {tool_name}: {effective_opts}")
|
|
902
|
+
except (ValueError, TypeError) as e:
|
|
903
|
+
# Configuration errors should be visible and re-raised
|
|
904
|
+
logger.warning(
|
|
905
|
+
f"Configuration error for {tool_name}: {e}",
|
|
906
|
+
exc_info=True,
|
|
907
|
+
)
|
|
908
|
+
raise
|
|
909
|
+
except Exception as e:
|
|
910
|
+
# Other unexpected errors - log at warning but allow execution
|
|
911
|
+
logger.warning(
|
|
912
|
+
f"Failed to apply config to {tool_name}: {e}",
|
|
913
|
+
exc_info=True,
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
def get_report(self) -> str:
|
|
917
|
+
"""Get configuration report.
|
|
918
|
+
|
|
919
|
+
Returns:
|
|
920
|
+
Formatted configuration report string
|
|
921
|
+
"""
|
|
922
|
+
return get_config_report()
|
|
923
|
+
|
|
924
|
+
def print_report(self) -> None:
|
|
925
|
+
"""Print configuration report."""
|
|
926
|
+
print_config_report()
|