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.
Files changed (72) hide show
  1. lintro/__init__.py +1 -1
  2. lintro/cli.py +226 -16
  3. lintro/cli_utils/commands/__init__.py +8 -1
  4. lintro/cli_utils/commands/check.py +1 -0
  5. lintro/cli_utils/commands/config.py +325 -0
  6. lintro/cli_utils/commands/init.py +361 -0
  7. lintro/cli_utils/commands/list_tools.py +180 -42
  8. lintro/cli_utils/commands/test.py +316 -0
  9. lintro/cli_utils/commands/versions.py +81 -0
  10. lintro/config/__init__.py +62 -0
  11. lintro/config/config_loader.py +420 -0
  12. lintro/config/lintro_config.py +189 -0
  13. lintro/config/tool_config_generator.py +403 -0
  14. lintro/enums/tool_name.py +2 -0
  15. lintro/enums/tool_type.py +2 -0
  16. lintro/formatters/tools/__init__.py +12 -0
  17. lintro/formatters/tools/eslint_formatter.py +108 -0
  18. lintro/formatters/tools/markdownlint_formatter.py +88 -0
  19. lintro/formatters/tools/pytest_formatter.py +201 -0
  20. lintro/parsers/__init__.py +69 -9
  21. lintro/parsers/bandit/__init__.py +6 -0
  22. lintro/parsers/bandit/bandit_issue.py +49 -0
  23. lintro/parsers/bandit/bandit_parser.py +99 -0
  24. lintro/parsers/black/black_issue.py +4 -0
  25. lintro/parsers/eslint/__init__.py +6 -0
  26. lintro/parsers/eslint/eslint_issue.py +26 -0
  27. lintro/parsers/eslint/eslint_parser.py +63 -0
  28. lintro/parsers/markdownlint/__init__.py +6 -0
  29. lintro/parsers/markdownlint/markdownlint_issue.py +22 -0
  30. lintro/parsers/markdownlint/markdownlint_parser.py +113 -0
  31. lintro/parsers/pytest/__init__.py +21 -0
  32. lintro/parsers/pytest/pytest_issue.py +28 -0
  33. lintro/parsers/pytest/pytest_parser.py +483 -0
  34. lintro/tools/__init__.py +2 -0
  35. lintro/tools/core/timeout_utils.py +112 -0
  36. lintro/tools/core/tool_base.py +255 -45
  37. lintro/tools/core/tool_manager.py +77 -24
  38. lintro/tools/core/version_requirements.py +482 -0
  39. lintro/tools/implementations/pytest/pytest_command_builder.py +311 -0
  40. lintro/tools/implementations/pytest/pytest_config.py +200 -0
  41. lintro/tools/implementations/pytest/pytest_error_handler.py +128 -0
  42. lintro/tools/implementations/pytest/pytest_executor.py +122 -0
  43. lintro/tools/implementations/pytest/pytest_handlers.py +375 -0
  44. lintro/tools/implementations/pytest/pytest_option_validators.py +212 -0
  45. lintro/tools/implementations/pytest/pytest_output_processor.py +408 -0
  46. lintro/tools/implementations/pytest/pytest_result_processor.py +113 -0
  47. lintro/tools/implementations/pytest/pytest_utils.py +697 -0
  48. lintro/tools/implementations/tool_actionlint.py +106 -16
  49. lintro/tools/implementations/tool_bandit.py +23 -7
  50. lintro/tools/implementations/tool_black.py +236 -29
  51. lintro/tools/implementations/tool_darglint.py +180 -21
  52. lintro/tools/implementations/tool_eslint.py +374 -0
  53. lintro/tools/implementations/tool_hadolint.py +94 -25
  54. lintro/tools/implementations/tool_markdownlint.py +354 -0
  55. lintro/tools/implementations/tool_prettier.py +313 -26
  56. lintro/tools/implementations/tool_pytest.py +327 -0
  57. lintro/tools/implementations/tool_ruff.py +247 -70
  58. lintro/tools/implementations/tool_yamllint.py +448 -34
  59. lintro/tools/tool_enum.py +6 -0
  60. lintro/utils/config.py +41 -18
  61. lintro/utils/console_logger.py +211 -25
  62. lintro/utils/path_utils.py +42 -0
  63. lintro/utils/tool_executor.py +336 -39
  64. lintro/utils/tool_utils.py +38 -2
  65. lintro/utils/unified_config.py +926 -0
  66. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/METADATA +131 -29
  67. lintro-0.17.2.dist-info/RECORD +134 -0
  68. lintro-0.13.2.dist-info/RECORD +0 -96
  69. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
  70. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
  71. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
  72. {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