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,420 @@
|
|
|
1
|
+
"""Configuration loader for Lintro.
|
|
2
|
+
|
|
3
|
+
Loads configuration from .lintro-config.yaml with fallback to
|
|
4
|
+
[tool.lintro] in pyproject.toml for backward compatibility.
|
|
5
|
+
|
|
6
|
+
Supports the new tiered configuration model:
|
|
7
|
+
1. execution: What tools run and how
|
|
8
|
+
2. enforce: Cross-cutting settings (replaces 'global')
|
|
9
|
+
3. defaults: Fallback config when no native config exists
|
|
10
|
+
4. tools: Per-tool enable/disable and config source
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import tomllib
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from loguru import logger
|
|
20
|
+
|
|
21
|
+
from lintro.config.lintro_config import (
|
|
22
|
+
EnforceConfig,
|
|
23
|
+
ExecutionConfig,
|
|
24
|
+
LintroConfig,
|
|
25
|
+
ToolConfig,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import yaml
|
|
30
|
+
except ImportError:
|
|
31
|
+
yaml = None # type: ignore[assignment]
|
|
32
|
+
|
|
33
|
+
# Default config file name
|
|
34
|
+
LINTRO_CONFIG_FILENAME = ".lintro-config.yaml"
|
|
35
|
+
LINTRO_CONFIG_FILENAMES = [
|
|
36
|
+
".lintro-config.yaml",
|
|
37
|
+
".lintro-config.yml",
|
|
38
|
+
"lintro-config.yaml",
|
|
39
|
+
"lintro-config.yml",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _find_config_file(start_dir: Path | None = None) -> Path | None:
|
|
44
|
+
"""Find .lintro-config.yaml by searching upward from start_dir.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
start_dir: Directory to start searching from. Defaults to cwd.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Path | None: Path to config file if found.
|
|
51
|
+
"""
|
|
52
|
+
current = Path(start_dir) if start_dir else Path.cwd()
|
|
53
|
+
current = current.resolve()
|
|
54
|
+
|
|
55
|
+
while True:
|
|
56
|
+
for filename in LINTRO_CONFIG_FILENAMES:
|
|
57
|
+
config_path = current / filename
|
|
58
|
+
if config_path.exists():
|
|
59
|
+
return config_path
|
|
60
|
+
|
|
61
|
+
# Move up one directory
|
|
62
|
+
parent = current.parent
|
|
63
|
+
if parent == current:
|
|
64
|
+
# Reached filesystem root
|
|
65
|
+
break
|
|
66
|
+
current = parent
|
|
67
|
+
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _load_yaml_file(path: Path) -> dict[str, Any]:
|
|
72
|
+
"""Load a YAML file.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
path: Path to YAML file.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
dict[str, Any]: Parsed YAML content.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ImportError: If PyYAML is not installed.
|
|
82
|
+
"""
|
|
83
|
+
if yaml is None:
|
|
84
|
+
raise ImportError(
|
|
85
|
+
"PyYAML is required to load .lintro-config.yaml. "
|
|
86
|
+
"Install it with: pip install pyyaml",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
with path.open(encoding="utf-8") as f:
|
|
90
|
+
content = yaml.safe_load(f)
|
|
91
|
+
|
|
92
|
+
return content if isinstance(content, dict) else {}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _load_pyproject_fallback() -> tuple[dict[str, Any], Path | None]:
|
|
96
|
+
"""Load [tool.lintro] from pyproject.toml as fallback.
|
|
97
|
+
|
|
98
|
+
Searches upward from current directory for pyproject.toml, consistent
|
|
99
|
+
with _find_config_file's search behavior.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
tuple[dict[str, Any], Path | None]: Tuple of (config data, path to
|
|
103
|
+
pyproject.toml). Path is None if no pyproject.toml was found.
|
|
104
|
+
"""
|
|
105
|
+
current = Path.cwd().resolve()
|
|
106
|
+
|
|
107
|
+
while True:
|
|
108
|
+
pyproject_path = current / "pyproject.toml"
|
|
109
|
+
if pyproject_path.exists():
|
|
110
|
+
try:
|
|
111
|
+
with pyproject_path.open("rb") as f:
|
|
112
|
+
data = tomllib.load(f)
|
|
113
|
+
return data.get("tool", {}).get("lintro", {}), pyproject_path
|
|
114
|
+
except (OSError, tomllib.TOMLDecodeError) as e:
|
|
115
|
+
logger.debug(f"Failed to load pyproject.toml: {e}")
|
|
116
|
+
return {}, None
|
|
117
|
+
|
|
118
|
+
# Move up one directory
|
|
119
|
+
parent = current.parent
|
|
120
|
+
if parent == current:
|
|
121
|
+
# Reached filesystem root
|
|
122
|
+
break
|
|
123
|
+
current = parent
|
|
124
|
+
|
|
125
|
+
return {}, None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _parse_enforce_config(data: dict[str, Any]) -> EnforceConfig:
|
|
129
|
+
"""Parse enforce configuration section.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
data: Raw 'enforce' or 'global' section from config.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
EnforceConfig: Parsed enforce configuration.
|
|
136
|
+
"""
|
|
137
|
+
return EnforceConfig(
|
|
138
|
+
line_length=data.get("line_length"),
|
|
139
|
+
target_python=data.get("target_python"),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _parse_execution_config(data: dict[str, Any]) -> ExecutionConfig:
|
|
144
|
+
"""Parse execution configuration section.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
data: Raw 'execution' section from config.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
ExecutionConfig: Parsed execution configuration.
|
|
151
|
+
"""
|
|
152
|
+
enabled_tools = data.get("enabled_tools", [])
|
|
153
|
+
if isinstance(enabled_tools, str):
|
|
154
|
+
enabled_tools = [enabled_tools]
|
|
155
|
+
|
|
156
|
+
tool_order = data.get("tool_order", "priority")
|
|
157
|
+
|
|
158
|
+
return ExecutionConfig(
|
|
159
|
+
enabled_tools=enabled_tools,
|
|
160
|
+
tool_order=tool_order,
|
|
161
|
+
fail_fast=data.get("fail_fast", False),
|
|
162
|
+
parallel=data.get("parallel", False),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _parse_tool_config(data: dict[str, Any]) -> ToolConfig:
|
|
167
|
+
"""Parse a single tool configuration.
|
|
168
|
+
|
|
169
|
+
In the tiered model, tools only have enabled and optional config_source.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
data: Raw tool configuration dict.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
ToolConfig: Parsed tool configuration.
|
|
176
|
+
"""
|
|
177
|
+
enabled = data.get("enabled", True)
|
|
178
|
+
config_source = data.get("config_source")
|
|
179
|
+
|
|
180
|
+
return ToolConfig(
|
|
181
|
+
enabled=enabled,
|
|
182
|
+
config_source=config_source,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _parse_tools_config(data: dict[str, Any]) -> dict[str, ToolConfig]:
|
|
187
|
+
"""Parse all tool configurations.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
data: Raw 'tools' section from config.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
dict[str, ToolConfig]: Tool configurations keyed by tool name.
|
|
194
|
+
"""
|
|
195
|
+
tools: dict[str, ToolConfig] = {}
|
|
196
|
+
|
|
197
|
+
for tool_name, tool_data in data.items():
|
|
198
|
+
if isinstance(tool_data, dict):
|
|
199
|
+
tools[tool_name.lower()] = _parse_tool_config(tool_data)
|
|
200
|
+
elif isinstance(tool_data, bool):
|
|
201
|
+
# Simple enabled/disabled flag
|
|
202
|
+
tools[tool_name.lower()] = ToolConfig(enabled=tool_data)
|
|
203
|
+
|
|
204
|
+
return tools
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _parse_defaults(data: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
208
|
+
"""Parse defaults configuration section.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
data: Raw 'defaults' section from config.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
dict[str, dict[str, Any]]: Defaults configurations keyed by tool name.
|
|
215
|
+
"""
|
|
216
|
+
defaults: dict[str, dict[str, Any]] = {}
|
|
217
|
+
|
|
218
|
+
for tool_name, tool_defaults in data.items():
|
|
219
|
+
if isinstance(tool_defaults, dict):
|
|
220
|
+
defaults[tool_name.lower()] = tool_defaults
|
|
221
|
+
|
|
222
|
+
return defaults
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _convert_pyproject_to_config(data: dict[str, Any]) -> dict[str, Any]:
|
|
226
|
+
"""Convert pyproject.toml [tool.lintro] format to .lintro-config.yaml format.
|
|
227
|
+
|
|
228
|
+
The pyproject format uses flat tool sections like [tool.lintro.ruff],
|
|
229
|
+
while .lintro-config.yaml uses nested tools: section.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
data: Raw [tool.lintro] section from pyproject.toml.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
dict[str, Any]: Converted configuration in .lintro-config.yaml format.
|
|
236
|
+
"""
|
|
237
|
+
result: dict[str, Any] = {
|
|
238
|
+
"enforce": {},
|
|
239
|
+
"execution": {},
|
|
240
|
+
"defaults": {},
|
|
241
|
+
"tools": {},
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# Known tool names to separate from enforce settings
|
|
245
|
+
# Lazy import to avoid circular dependency
|
|
246
|
+
from lintro.tools.tool_enum import ToolEnum
|
|
247
|
+
|
|
248
|
+
# Derived from ToolEnum to stay synchronized with implementations
|
|
249
|
+
known_tools = {t.name.lower() for t in ToolEnum}
|
|
250
|
+
# Add common aliases for tools
|
|
251
|
+
tool_aliases = {"markdownlint-cli2": "markdownlint"}
|
|
252
|
+
known_tools.update(tool_aliases.keys())
|
|
253
|
+
|
|
254
|
+
# Known execution settings
|
|
255
|
+
execution_keys = {"enabled_tools", "tool_order", "fail_fast", "parallel"}
|
|
256
|
+
|
|
257
|
+
# Known enforce settings (formerly global)
|
|
258
|
+
enforce_keys = {"line_length", "target_python"}
|
|
259
|
+
|
|
260
|
+
for key, value in data.items():
|
|
261
|
+
key_lower = key.lower()
|
|
262
|
+
|
|
263
|
+
if key_lower in known_tools:
|
|
264
|
+
# Tool-specific config - normalize aliases to canonical names
|
|
265
|
+
canonical_name = tool_aliases.get(key_lower, key_lower)
|
|
266
|
+
result["tools"][canonical_name] = value
|
|
267
|
+
elif key in execution_keys or key.replace("-", "_") in execution_keys:
|
|
268
|
+
# Execution config
|
|
269
|
+
result["execution"][key.replace("-", "_")] = value
|
|
270
|
+
elif key in enforce_keys or key.replace("-", "_") in enforce_keys:
|
|
271
|
+
# Enforce config
|
|
272
|
+
result["enforce"][key.replace("-", "_")] = value
|
|
273
|
+
elif key == "post_checks":
|
|
274
|
+
# Skip post_checks (handled separately)
|
|
275
|
+
pass
|
|
276
|
+
elif key == "versions":
|
|
277
|
+
# Skip versions (handled separately)
|
|
278
|
+
pass
|
|
279
|
+
elif key == "defaults" and isinstance(value, dict):
|
|
280
|
+
# Defaults section
|
|
281
|
+
result["defaults"] = value
|
|
282
|
+
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def load_config(
|
|
287
|
+
config_path: Path | str | None = None,
|
|
288
|
+
allow_pyproject_fallback: bool = True,
|
|
289
|
+
) -> LintroConfig:
|
|
290
|
+
"""Load Lintro configuration.
|
|
291
|
+
|
|
292
|
+
Priority:
|
|
293
|
+
1. Explicit config_path if provided
|
|
294
|
+
2. .lintro-config.yaml found by searching upward
|
|
295
|
+
3. [tool.lintro] in pyproject.toml (deprecated fallback)
|
|
296
|
+
4. Default empty configuration
|
|
297
|
+
|
|
298
|
+
Supports both new 'enforce' section and deprecated 'global' section.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
config_path: Explicit path to config file. If None, searches for
|
|
302
|
+
.lintro-config.yaml.
|
|
303
|
+
allow_pyproject_fallback: Whether to fall back to pyproject.toml
|
|
304
|
+
if no .lintro-config.yaml is found.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
LintroConfig: Loaded configuration.
|
|
308
|
+
"""
|
|
309
|
+
data: dict[str, Any] = {}
|
|
310
|
+
resolved_path: str | None = None
|
|
311
|
+
|
|
312
|
+
# Try explicit path first
|
|
313
|
+
if config_path:
|
|
314
|
+
path = Path(config_path)
|
|
315
|
+
if path.exists():
|
|
316
|
+
data = _load_yaml_file(path)
|
|
317
|
+
resolved_path = str(path.resolve())
|
|
318
|
+
logger.debug(f"Loaded config from explicit path: {resolved_path}")
|
|
319
|
+
else:
|
|
320
|
+
logger.warning(f"Config file not found: {config_path}")
|
|
321
|
+
|
|
322
|
+
# Try searching for .lintro-config.yaml
|
|
323
|
+
if not data:
|
|
324
|
+
found_path = _find_config_file()
|
|
325
|
+
if found_path:
|
|
326
|
+
data = _load_yaml_file(found_path)
|
|
327
|
+
resolved_path = str(found_path.resolve())
|
|
328
|
+
logger.debug(f"Loaded config from: {resolved_path}")
|
|
329
|
+
|
|
330
|
+
# Fall back to pyproject.toml
|
|
331
|
+
if not data and allow_pyproject_fallback:
|
|
332
|
+
pyproject_data, pyproject_path = _load_pyproject_fallback()
|
|
333
|
+
if pyproject_data:
|
|
334
|
+
data = _convert_pyproject_to_config(pyproject_data)
|
|
335
|
+
resolved_path = str(pyproject_path.resolve()) if pyproject_path else None
|
|
336
|
+
logger.debug(
|
|
337
|
+
"Using [tool.lintro] from pyproject.toml (deprecated). "
|
|
338
|
+
"Consider migrating to .lintro-config.yaml",
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Parse enforce config - support both 'enforce' and deprecated 'global'
|
|
342
|
+
enforce_data = data.get("enforce", {})
|
|
343
|
+
global_data = data.get("global", {})
|
|
344
|
+
|
|
345
|
+
# Always warn if 'global' section exists (it's deprecated)
|
|
346
|
+
if global_data:
|
|
347
|
+
if enforce_data:
|
|
348
|
+
# Both exist - warn that 'global' is ignored
|
|
349
|
+
logger.warning(
|
|
350
|
+
"The 'global' config section is deprecated and ignored "
|
|
351
|
+
"because 'enforce' is also present. Remove 'global' from your "
|
|
352
|
+
".lintro-config.yaml",
|
|
353
|
+
)
|
|
354
|
+
else:
|
|
355
|
+
# Only 'global' exists - warn and use it
|
|
356
|
+
logger.warning(
|
|
357
|
+
"The 'global' config section is deprecated. "
|
|
358
|
+
"Please rename it to 'enforce' in your .lintro-config.yaml",
|
|
359
|
+
)
|
|
360
|
+
enforce_data = global_data
|
|
361
|
+
|
|
362
|
+
enforce_config = _parse_enforce_config(enforce_data)
|
|
363
|
+
execution_config = _parse_execution_config(data.get("execution", {}))
|
|
364
|
+
defaults = _parse_defaults(data.get("defaults", {}))
|
|
365
|
+
tools_config = _parse_tools_config(data.get("tools", {}))
|
|
366
|
+
|
|
367
|
+
return LintroConfig(
|
|
368
|
+
execution=execution_config,
|
|
369
|
+
enforce=enforce_config,
|
|
370
|
+
defaults=defaults,
|
|
371
|
+
tools=tools_config,
|
|
372
|
+
config_path=resolved_path,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def get_default_config() -> LintroConfig:
|
|
377
|
+
"""Get a default configuration with sensible defaults.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
LintroConfig: Default configuration.
|
|
381
|
+
"""
|
|
382
|
+
return LintroConfig(
|
|
383
|
+
enforce=EnforceConfig(
|
|
384
|
+
line_length=88,
|
|
385
|
+
# target_python omitted - let tools infer from requires-python
|
|
386
|
+
),
|
|
387
|
+
execution=ExecutionConfig(
|
|
388
|
+
tool_order="priority",
|
|
389
|
+
),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# Global singleton for loaded config
|
|
394
|
+
_loaded_config: LintroConfig | None = None
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def get_config(reload: bool = False) -> LintroConfig:
|
|
398
|
+
"""Get the loaded configuration singleton.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
reload: Force reload from disk.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
LintroConfig: Loaded configuration.
|
|
405
|
+
"""
|
|
406
|
+
global _loaded_config
|
|
407
|
+
|
|
408
|
+
if _loaded_config is None or reload:
|
|
409
|
+
_loaded_config = load_config()
|
|
410
|
+
|
|
411
|
+
return _loaded_config
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def clear_config_cache() -> None:
|
|
415
|
+
"""Clear the configuration cache.
|
|
416
|
+
|
|
417
|
+
Useful for testing or when config file has changed.
|
|
418
|
+
"""
|
|
419
|
+
global _loaded_config
|
|
420
|
+
_loaded_config = None
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Lintro configuration dataclasses.
|
|
2
|
+
|
|
3
|
+
This module defines the configuration structure for .lintro-config.yaml.
|
|
4
|
+
The configuration follows a 4-tier model:
|
|
5
|
+
|
|
6
|
+
1. EXECUTION: What tools run and how (Lintro's core responsibility)
|
|
7
|
+
2. ENFORCE: Cross-cutting settings injected via CLI flags (overrides native configs)
|
|
8
|
+
3. DEFAULTS: Fallback config when no native config exists for a tool
|
|
9
|
+
4. TOOLS: Per-tool enable/disable and config source
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class EnforceConfig:
|
|
18
|
+
"""Cross-cutting settings enforced across all tools via CLI flags.
|
|
19
|
+
|
|
20
|
+
These settings override native tool configs to ensure consistency
|
|
21
|
+
across different tools for shared concerns.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
line_length: Line length limit injected via CLI flags.
|
|
25
|
+
Injected as: --line-length (ruff, black), --print-width (prettier)
|
|
26
|
+
target_python: Python version target (e.g., "py313").
|
|
27
|
+
Injected as: --target-version (ruff, black)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
line_length: int | None = None
|
|
31
|
+
target_python: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Backward compatibility alias
|
|
35
|
+
GlobalConfig = EnforceConfig
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ExecutionConfig:
|
|
40
|
+
"""Execution control settings.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
enabled_tools: List of tool names to run. If empty/None, all tools run.
|
|
44
|
+
tool_order: Execution order strategy. One of:
|
|
45
|
+
- "priority": Use default priority (formatters before linters)
|
|
46
|
+
- "alphabetical": Alphabetical order
|
|
47
|
+
- list[str]: Custom order as explicit list
|
|
48
|
+
fail_fast: Stop on first tool failure.
|
|
49
|
+
parallel: Run tools in parallel where possible (future).
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
enabled_tools: list[str] = field(default_factory=list)
|
|
53
|
+
tool_order: str | list[str] = "priority"
|
|
54
|
+
fail_fast: bool = False
|
|
55
|
+
parallel: bool = False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ToolConfig:
|
|
60
|
+
"""Configuration for a single tool.
|
|
61
|
+
|
|
62
|
+
In the tiered model, tools use their native configs by default.
|
|
63
|
+
Lintro only controls whether tools run and optionally specifies
|
|
64
|
+
an explicit config source path.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
enabled: Whether the tool is enabled.
|
|
68
|
+
config_source: Optional explicit path to native config file.
|
|
69
|
+
If not set, tool uses its own config discovery.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
enabled: bool = True
|
|
73
|
+
config_source: str | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class LintroConfig:
|
|
78
|
+
"""Main Lintro configuration container.
|
|
79
|
+
|
|
80
|
+
This is the root configuration object loaded from .lintro-config.yaml.
|
|
81
|
+
Follows the 4-tier model:
|
|
82
|
+
|
|
83
|
+
1. execution: What tools run and how
|
|
84
|
+
2. enforce: Cross-cutting settings that override native configs
|
|
85
|
+
3. defaults: Fallback config when no native config exists
|
|
86
|
+
4. tools: Per-tool enable/disable and config source
|
|
87
|
+
|
|
88
|
+
Attributes:
|
|
89
|
+
execution: Execution control settings.
|
|
90
|
+
enforce: Cross-cutting settings enforced via CLI flags.
|
|
91
|
+
defaults: Fallback configs for tools without native configs.
|
|
92
|
+
tools: Per-tool configuration, keyed by tool name.
|
|
93
|
+
config_path: Path to the config file (set by loader).
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
execution: ExecutionConfig = field(default_factory=ExecutionConfig)
|
|
97
|
+
enforce: EnforceConfig = field(default_factory=EnforceConfig)
|
|
98
|
+
defaults: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
99
|
+
tools: dict[str, ToolConfig] = field(default_factory=dict)
|
|
100
|
+
config_path: str | None = None
|
|
101
|
+
|
|
102
|
+
# Backward compatibility property
|
|
103
|
+
@property
|
|
104
|
+
def global_config(self) -> EnforceConfig:
|
|
105
|
+
"""Get enforce config (deprecated alias for backward compatibility).
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
EnforceConfig: The enforce configuration.
|
|
109
|
+
"""
|
|
110
|
+
return self.enforce
|
|
111
|
+
|
|
112
|
+
def get_tool_config(self, tool_name: str) -> ToolConfig:
|
|
113
|
+
"""Get configuration for a specific tool.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
tool_name: Name of the tool (e.g., "ruff", "prettier").
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
ToolConfig: Tool configuration. Returns default config if not
|
|
120
|
+
explicitly configured.
|
|
121
|
+
"""
|
|
122
|
+
return self.tools.get(tool_name.lower(), ToolConfig())
|
|
123
|
+
|
|
124
|
+
def is_tool_enabled(self, tool_name: str) -> bool:
|
|
125
|
+
"""Check if a tool is enabled.
|
|
126
|
+
|
|
127
|
+
A tool is enabled if:
|
|
128
|
+
1. execution.enabled_tools is empty (all tools enabled), OR
|
|
129
|
+
2. tool_name is in execution.enabled_tools, AND
|
|
130
|
+
3. The tool's config has enabled=True (default)
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
tool_name: Name of the tool.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
bool: True if tool should run.
|
|
137
|
+
"""
|
|
138
|
+
tool_lower = tool_name.lower()
|
|
139
|
+
|
|
140
|
+
# Check execution.enabled_tools filter
|
|
141
|
+
if self.execution.enabled_tools:
|
|
142
|
+
enabled_lower = [t.lower() for t in self.execution.enabled_tools]
|
|
143
|
+
if tool_lower not in enabled_lower:
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
# Check tool-specific enabled flag
|
|
147
|
+
tool_config = self.get_tool_config(tool_lower)
|
|
148
|
+
return tool_config.enabled
|
|
149
|
+
|
|
150
|
+
def get_tool_defaults(self, tool_name: str) -> dict[str, Any]:
|
|
151
|
+
"""Get default configuration for a tool.
|
|
152
|
+
|
|
153
|
+
Used when the tool has no native config file.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
tool_name: Name of the tool.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
dict[str, Any]: Default configuration or empty dict.
|
|
160
|
+
"""
|
|
161
|
+
return self.defaults.get(tool_name.lower(), {})
|
|
162
|
+
|
|
163
|
+
def get_effective_line_length(self, tool_name: str) -> int | None:
|
|
164
|
+
"""Get effective line length for a specific tool.
|
|
165
|
+
|
|
166
|
+
In the tiered model, this simply returns the enforce.line_length
|
|
167
|
+
value, which will be injected via CLI flags.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
tool_name: Name of the tool (unused, kept for compatibility).
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
int | None: Enforced line length or None.
|
|
174
|
+
"""
|
|
175
|
+
return self.enforce.line_length
|
|
176
|
+
|
|
177
|
+
def get_effective_target_python(self, tool_name: str) -> str | None:
|
|
178
|
+
"""Get effective Python target version for a specific tool.
|
|
179
|
+
|
|
180
|
+
In the tiered model, this simply returns the enforce.target_python
|
|
181
|
+
value, which will be injected via CLI flags.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
tool_name: Name of the tool (unused, kept for compatibility).
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
str | None: Enforced target version or None.
|
|
188
|
+
"""
|
|
189
|
+
return self.enforce.target_python
|