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.
Files changed (109) hide show
  1. lintro/__init__.py +1 -1
  2. lintro/cli.py +230 -14
  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/format.py +2 -2
  7. lintro/cli_utils/commands/init.py +361 -0
  8. lintro/cli_utils/commands/list_tools.py +180 -42
  9. lintro/cli_utils/commands/test.py +316 -0
  10. lintro/cli_utils/commands/versions.py +81 -0
  11. lintro/config/__init__.py +62 -0
  12. lintro/config/config_loader.py +420 -0
  13. lintro/config/lintro_config.py +189 -0
  14. lintro/config/tool_config_generator.py +403 -0
  15. lintro/enums/__init__.py +1 -0
  16. lintro/enums/darglint_strictness.py +10 -0
  17. lintro/enums/hadolint_enums.py +22 -0
  18. lintro/enums/tool_name.py +2 -0
  19. lintro/enums/tool_type.py +2 -0
  20. lintro/enums/yamllint_format.py +11 -0
  21. lintro/exceptions/__init__.py +1 -0
  22. lintro/formatters/__init__.py +1 -0
  23. lintro/formatters/core/__init__.py +1 -0
  24. lintro/formatters/core/output_style.py +11 -0
  25. lintro/formatters/core/table_descriptor.py +8 -0
  26. lintro/formatters/styles/csv.py +2 -0
  27. lintro/formatters/styles/grid.py +2 -0
  28. lintro/formatters/styles/html.py +2 -0
  29. lintro/formatters/styles/json.py +2 -0
  30. lintro/formatters/styles/markdown.py +2 -0
  31. lintro/formatters/styles/plain.py +2 -0
  32. lintro/formatters/tools/__init__.py +12 -0
  33. lintro/formatters/tools/black_formatter.py +27 -5
  34. lintro/formatters/tools/darglint_formatter.py +16 -1
  35. lintro/formatters/tools/eslint_formatter.py +108 -0
  36. lintro/formatters/tools/hadolint_formatter.py +13 -0
  37. lintro/formatters/tools/markdownlint_formatter.py +88 -0
  38. lintro/formatters/tools/prettier_formatter.py +15 -0
  39. lintro/formatters/tools/pytest_formatter.py +201 -0
  40. lintro/formatters/tools/ruff_formatter.py +26 -5
  41. lintro/formatters/tools/yamllint_formatter.py +14 -1
  42. lintro/models/__init__.py +1 -0
  43. lintro/models/core/__init__.py +1 -0
  44. lintro/models/core/tool_config.py +11 -7
  45. lintro/parsers/__init__.py +69 -9
  46. lintro/parsers/actionlint/actionlint_parser.py +1 -1
  47. lintro/parsers/bandit/__init__.py +6 -0
  48. lintro/parsers/bandit/bandit_issue.py +49 -0
  49. lintro/parsers/bandit/bandit_parser.py +99 -0
  50. lintro/parsers/black/black_issue.py +4 -0
  51. lintro/parsers/darglint/__init__.py +1 -0
  52. lintro/parsers/darglint/darglint_issue.py +11 -0
  53. lintro/parsers/eslint/__init__.py +6 -0
  54. lintro/parsers/eslint/eslint_issue.py +26 -0
  55. lintro/parsers/eslint/eslint_parser.py +63 -0
  56. lintro/parsers/markdownlint/__init__.py +6 -0
  57. lintro/parsers/markdownlint/markdownlint_issue.py +22 -0
  58. lintro/parsers/markdownlint/markdownlint_parser.py +113 -0
  59. lintro/parsers/prettier/__init__.py +1 -0
  60. lintro/parsers/prettier/prettier_issue.py +12 -0
  61. lintro/parsers/prettier/prettier_parser.py +1 -1
  62. lintro/parsers/pytest/__init__.py +21 -0
  63. lintro/parsers/pytest/pytest_issue.py +28 -0
  64. lintro/parsers/pytest/pytest_parser.py +483 -0
  65. lintro/parsers/ruff/ruff_parser.py +6 -2
  66. lintro/parsers/yamllint/__init__.py +1 -0
  67. lintro/tools/__init__.py +3 -1
  68. lintro/tools/core/__init__.py +1 -0
  69. lintro/tools/core/timeout_utils.py +112 -0
  70. lintro/tools/core/tool_base.py +286 -50
  71. lintro/tools/core/tool_manager.py +77 -24
  72. lintro/tools/core/version_requirements.py +482 -0
  73. lintro/tools/implementations/__init__.py +1 -0
  74. lintro/tools/implementations/pytest/pytest_command_builder.py +311 -0
  75. lintro/tools/implementations/pytest/pytest_config.py +200 -0
  76. lintro/tools/implementations/pytest/pytest_error_handler.py +128 -0
  77. lintro/tools/implementations/pytest/pytest_executor.py +122 -0
  78. lintro/tools/implementations/pytest/pytest_handlers.py +375 -0
  79. lintro/tools/implementations/pytest/pytest_option_validators.py +212 -0
  80. lintro/tools/implementations/pytest/pytest_output_processor.py +408 -0
  81. lintro/tools/implementations/pytest/pytest_result_processor.py +113 -0
  82. lintro/tools/implementations/pytest/pytest_utils.py +697 -0
  83. lintro/tools/implementations/tool_actionlint.py +106 -16
  84. lintro/tools/implementations/tool_bandit.py +34 -29
  85. lintro/tools/implementations/tool_black.py +236 -29
  86. lintro/tools/implementations/tool_darglint.py +183 -22
  87. lintro/tools/implementations/tool_eslint.py +374 -0
  88. lintro/tools/implementations/tool_hadolint.py +94 -25
  89. lintro/tools/implementations/tool_markdownlint.py +354 -0
  90. lintro/tools/implementations/tool_prettier.py +317 -24
  91. lintro/tools/implementations/tool_pytest.py +327 -0
  92. lintro/tools/implementations/tool_ruff.py +278 -84
  93. lintro/tools/implementations/tool_yamllint.py +448 -34
  94. lintro/tools/tool_enum.py +8 -0
  95. lintro/utils/__init__.py +1 -0
  96. lintro/utils/ascii_normalize_cli.py +5 -0
  97. lintro/utils/config.py +41 -18
  98. lintro/utils/console_logger.py +211 -25
  99. lintro/utils/path_utils.py +42 -0
  100. lintro/utils/tool_executor.py +339 -45
  101. lintro/utils/tool_utils.py +51 -24
  102. lintro/utils/unified_config.py +926 -0
  103. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/METADATA +172 -30
  104. lintro-0.17.2.dist-info/RECORD +134 -0
  105. lintro-0.6.2.dist-info/RECORD +0 -96
  106. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
  107. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
  108. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
  109. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,482 @@
1
+ """Tool version requirements and checking utilities.
2
+
3
+ This module centralizes version management for all lintro tools. Version requirements
4
+ are read from pyproject.toml to ensure consistency across the entire codebase.
5
+
6
+ ## Adding a New Tool
7
+
8
+ When adding a new tool to lintro, follow these steps:
9
+
10
+ ### For Bundled Python Tools (installed with lintro):
11
+ 1. Add the tool as a dependency in pyproject.toml:
12
+ ```toml
13
+ dependencies = [
14
+ # ... existing deps ...
15
+ "newtool>=1.0.0",
16
+ ]
17
+ ```
18
+
19
+ 2. Update get_all_tool_versions() to include the new tool's command:
20
+ ```python
21
+ tool_commands = {
22
+ # ... existing tools ...
23
+ "newtool": ["newtool"], # Or ["python", "-m", "newtool"] if module-based
24
+ }
25
+ ```
26
+
27
+ 3. Add version extraction logic in _extract_version_from_output() if needed.
28
+
29
+ ### For External Tools (user must install separately):
30
+ 1. Add minimum version to [tool.lintro.versions] in pyproject.toml:
31
+ ```toml
32
+ [tool.lintro.versions]
33
+ newtool = "1.0.0"
34
+ ```
35
+
36
+ 2. Update get_all_tool_versions() with the tool's command.
37
+
38
+ 3. Add version extraction logic in _extract_version_from_output() if needed.
39
+
40
+ ### Implementation Steps:
41
+ 1. Create tool implementation class in lintro/tools/implementations/
42
+ 2. Add version checking in the tool's check() and fix() methods
43
+ 3. Update ToolEnum in lintro/tools/tool_enum.py
44
+ 4. Add tool to tool_commands dict in this file
45
+ 5. Test with `lintro versions` command
46
+
47
+ The version system automatically reads from pyproject.toml, so Renovate and other
48
+ dependency management tools will keep versions up to date.
49
+ """
50
+
51
+ import re
52
+ import subprocess # nosec B404 - used safely with shell disabled
53
+ import tomllib
54
+ from dataclasses import dataclass
55
+ from pathlib import Path
56
+
57
+ from loguru import logger
58
+
59
+
60
+ def _load_pyproject_config() -> dict:
61
+ """Load pyproject.toml configuration.
62
+
63
+ Returns:
64
+ dict: Configuration dictionary from pyproject.toml, or empty dict if not found.
65
+ """
66
+ pyproject_path = Path("pyproject.toml")
67
+ if not pyproject_path.exists():
68
+ logger.warning("pyproject.toml not found, using default version requirements")
69
+ return {}
70
+
71
+ try:
72
+ with open(pyproject_path, "rb") as f:
73
+ return tomllib.load(f)
74
+ except Exception as e:
75
+ logger.warning(f"Failed to load pyproject.toml: {e}")
76
+ return {}
77
+
78
+
79
+ def _parse_version_specifier(specifier: str) -> str:
80
+ """Extract minimum version from a PEP 508 version specifier.
81
+
82
+ Args:
83
+ specifier: PEP 508 version specifier string.
84
+
85
+ Returns:
86
+ str: Minimum version string extracted from specifier.
87
+
88
+ Examples:
89
+ ">=0.14.0" -> "0.14.0"
90
+ "==1.8.1" -> "1.8.1"
91
+ ">=25.0.0,<26.0.0" -> "25.0.0"
92
+ """
93
+ # Split on comma and take the first constraint
94
+ constraints = [c.strip() for c in specifier.split(",")]
95
+ for constraint in constraints:
96
+ if constraint.startswith(">=") or constraint.startswith("=="):
97
+ return constraint[2:]
98
+ # If no recognized constraint, return the specifier as-is
99
+ return specifier.strip()
100
+
101
+
102
+ def _get_minimum_versions() -> dict[str, str]:
103
+ """Get minimum version requirements for all tools from pyproject.toml.
104
+
105
+ Returns:
106
+ dict[str, str]: Dictionary mapping tool names to minimum version strings.
107
+ """
108
+ config = _load_pyproject_config()
109
+
110
+ versions = {}
111
+
112
+ # Python tools bundled with lintro - extract from dependencies
113
+ python_bundled_tools = {"ruff", "black", "bandit", "yamllint", "darglint"}
114
+ dependencies = config.get("project", {}).get("dependencies", [])
115
+
116
+ for dep in dependencies:
117
+ dep = dep.strip()
118
+ for tool in python_bundled_tools:
119
+ if dep.startswith(f"{tool}>=") or dep.startswith(f"{tool}=="):
120
+ versions[tool] = _parse_version_specifier(dep[len(tool) :])
121
+ break
122
+
123
+ # Other tools - read from [tool.lintro.versions] section
124
+ lintro_versions = config.get("tool", {}).get("lintro", {}).get("versions", {})
125
+ versions.update(lintro_versions)
126
+
127
+ # Fill in any missing tools with defaults (for backward compatibility)
128
+ defaults = {
129
+ "pytest": "8.0.0",
130
+ "prettier": "3.7.0",
131
+ "eslint": "9.0.0",
132
+ "hadolint": "2.12.0",
133
+ "actionlint": "1.7.0",
134
+ "markdownlint": "0.16.0",
135
+ }
136
+
137
+ for tool, default_version in defaults.items():
138
+ if tool not in versions:
139
+ versions[tool] = default_version
140
+
141
+ return versions
142
+
143
+
144
+ def _get_install_hints() -> dict[str, str]:
145
+ """Generate installation hints based on tool type and version requirements.
146
+
147
+ Returns:
148
+ dict[str, str]: Dictionary mapping tool names to installation hint strings.
149
+ """
150
+ versions = _get_minimum_versions()
151
+ hints = {}
152
+
153
+ # Python bundled tools
154
+ python_bundled = {"ruff", "black", "bandit", "yamllint", "darglint"}
155
+ for tool in python_bundled:
156
+ version = versions.get(tool, "latest")
157
+ hints[tool] = (
158
+ f"Install via: pip install {tool}>={version} or uv add {tool}>={version}"
159
+ )
160
+
161
+ # Other tools
162
+ pytest_version = versions.get("pytest", "8.0.0")
163
+ hints.update(
164
+ {
165
+ "pytest": (
166
+ f"Install via: pip install pytest>={pytest_version} "
167
+ f"or uv add pytest>={pytest_version}"
168
+ ),
169
+ "prettier": (
170
+ f"Install via: npm install --save-dev "
171
+ f"prettier>={versions.get('prettier', '3.7.0')}"
172
+ ),
173
+ "eslint": (
174
+ f"Install via: npm install --save-dev "
175
+ f"eslint>={versions.get('eslint', '9.0.0')}"
176
+ ),
177
+ "markdownlint": (
178
+ f"Install via: npm install --save-dev "
179
+ f"markdownlint-cli2>={versions.get('markdownlint', '0.16.0')}"
180
+ ),
181
+ "hadolint": (
182
+ f"Install via: https://github.com/hadolint/hadolint/releases "
183
+ f"(v{versions.get('hadolint', '2.12.0')}+)"
184
+ ),
185
+ "actionlint": (
186
+ f"Install via: https://github.com/rhysd/actionlint/releases "
187
+ f"(v{versions.get('actionlint', '1.7.0')}+)"
188
+ ),
189
+ },
190
+ )
191
+
192
+ return hints
193
+
194
+
195
+ # Cache the loaded versions to avoid re-reading pyproject.toml repeatedly
196
+ _MINIMUM_VERSIONS_CACHE: dict[str, str] | None = None
197
+ _INSTALL_HINTS_CACHE: dict[str, str] | None = None
198
+
199
+
200
+ def get_minimum_versions() -> dict[str, str]:
201
+ """Get minimum version requirements for all tools.
202
+
203
+ Returns:
204
+ dict[str, str]: Dictionary mapping tool names to minimum version strings.
205
+ """
206
+ global _MINIMUM_VERSIONS_CACHE
207
+ if _MINIMUM_VERSIONS_CACHE is None:
208
+ _MINIMUM_VERSIONS_CACHE = _get_minimum_versions()
209
+ return _MINIMUM_VERSIONS_CACHE
210
+
211
+
212
+ def get_install_hints() -> dict[str, str]:
213
+ """Get installation hints for tools that don't meet requirements.
214
+
215
+ Returns:
216
+ dict[str, str]: Dictionary mapping tool names to installation hint strings.
217
+ """
218
+ global _INSTALL_HINTS_CACHE
219
+ if _INSTALL_HINTS_CACHE is None:
220
+ _INSTALL_HINTS_CACHE = _get_install_hints()
221
+ return _INSTALL_HINTS_CACHE
222
+
223
+
224
+ @dataclass
225
+ class ToolVersionInfo:
226
+ """Information about a tool's version requirements."""
227
+
228
+ name: str
229
+ min_version: str
230
+ install_hint: str
231
+ current_version: str | None = None
232
+ version_check_passed: bool = False
233
+ error_message: str | None = None
234
+
235
+
236
+ def _parse_version(version_str: str) -> tuple[int, ...]:
237
+ """Parse a version string into a comparable tuple.
238
+
239
+ Args:
240
+ version_str: Version string like "1.2.3" or "0.14.0"
241
+
242
+ Returns:
243
+ tuple[int, ...]: Comparable version tuple like (1, 2, 3)
244
+ """
245
+ # Extract version numbers, handling pre-release suffixes
246
+ match = re.match(r"^(\d+(?:\.\d+)*)", version_str.strip())
247
+ if not match:
248
+ return (0,)
249
+
250
+ version_part = match.group(1)
251
+ return tuple(int(part) for part in version_part.split("."))
252
+
253
+
254
+ def _compare_versions(version1: str, version2: str) -> int:
255
+ """Compare two version strings.
256
+
257
+ Args:
258
+ version1: First version string
259
+ version2: Second version string
260
+
261
+ Returns:
262
+ int: -1 if version1 < version2, 0 if equal, 1 if version1 > version2
263
+ """
264
+ v1_parts = _parse_version(version1)
265
+ v2_parts = _parse_version(version2)
266
+
267
+ # Pad shorter version to same length
268
+ max_len = max(len(v1_parts), len(v2_parts))
269
+ v1_padded = v1_parts + (0,) * (max_len - len(v1_parts))
270
+ v2_padded = v2_parts + (0,) * (max_len - len(v2_parts))
271
+
272
+ if v1_padded < v2_padded:
273
+ return -1
274
+ elif v1_padded > v2_padded:
275
+ return 1
276
+ else:
277
+ return 0
278
+
279
+
280
+ def check_tool_version(tool_name: str, command: list[str]) -> ToolVersionInfo:
281
+ """Check if a tool meets minimum version requirements.
282
+
283
+ Args:
284
+ tool_name: Name of the tool to check
285
+ command: Command list to run the tool (e.g., ["python", "-m", "ruff"])
286
+
287
+ Returns:
288
+ ToolVersionInfo: Version check results
289
+ """
290
+ minimum_versions = get_minimum_versions()
291
+ install_hints = get_install_hints()
292
+
293
+ min_version = minimum_versions.get(tool_name, "unknown")
294
+ install_hint = install_hints.get(
295
+ tool_name,
296
+ f"Install {tool_name} and ensure it's in PATH",
297
+ )
298
+ has_requirements = tool_name in minimum_versions
299
+
300
+ info = ToolVersionInfo(
301
+ name=tool_name,
302
+ min_version=min_version,
303
+ install_hint=install_hint,
304
+ # If no requirements, assume check passes
305
+ version_check_passed=not has_requirements,
306
+ )
307
+
308
+ try:
309
+ # Run the tool with --version flag
310
+ version_cmd = command + ["--version"]
311
+ result = subprocess.run( # nosec B603 - args list, shell=False
312
+ version_cmd,
313
+ capture_output=True,
314
+ text=True,
315
+ timeout=10, # 10 second timeout
316
+ )
317
+
318
+ if result.returncode != 0:
319
+ info.error_message = f"Command failed: {' '.join(version_cmd)}"
320
+ logger.debug(
321
+ f"[VersionCheck] Failed to get version for {tool_name}: "
322
+ f"{info.error_message}",
323
+ )
324
+ return info
325
+
326
+ # Extract version from output
327
+ output = result.stdout + result.stderr
328
+ info.current_version = _extract_version_from_output(output, tool_name)
329
+
330
+ if not info.current_version:
331
+ info.error_message = (
332
+ f"Could not parse version from output: {output.strip()}"
333
+ )
334
+ logger.debug(
335
+ f"[VersionCheck] Failed to parse version for {tool_name}: "
336
+ f"{info.error_message}",
337
+ )
338
+ return info
339
+
340
+ # Compare versions
341
+ comparison = _compare_versions(info.current_version, min_version)
342
+ info.version_check_passed = comparison >= 0
343
+
344
+ if not info.version_check_passed:
345
+ info.error_message = (
346
+ f"Version {info.current_version} is below minimum requirement "
347
+ f"{min_version}"
348
+ )
349
+ logger.debug(
350
+ f"[VersionCheck] Version check failed for {tool_name}: "
351
+ f"{info.error_message}",
352
+ )
353
+
354
+ except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError) as e:
355
+ info.error_message = f"Failed to run version check: {e}"
356
+ logger.debug(f"[VersionCheck] Exception checking version for {tool_name}: {e}")
357
+
358
+ return info
359
+
360
+
361
+ def _extract_version_from_output(output: str, tool_name: str) -> str | None:
362
+ """Extract version string from tool --version output.
363
+
364
+ Args:
365
+ output: Raw output from tool --version
366
+ tool_name: Name of the tool (to handle tool-specific parsing)
367
+
368
+ Returns:
369
+ Optional[str]: Extracted version string, or None if not found
370
+ """
371
+ output = output.strip()
372
+
373
+ # Tool-specific patterns first (most reliable)
374
+ if tool_name == "black":
375
+ # black: "black, 25.9.0 (compiled: yes)"
376
+ match = re.search(r"black,\s+(\d+(?:\.\d+)*)", output, re.IGNORECASE)
377
+ if match:
378
+ return match.group(1)
379
+
380
+ elif tool_name == "bandit":
381
+ # bandit: "__main__.py 1.8.6"
382
+ match = re.search(r"(\d+(?:\.\d+)*)", output)
383
+ if match:
384
+ return match.group(1)
385
+
386
+ elif tool_name == "hadolint":
387
+ # hadolint: "Haskell Dockerfile Linter 2.14.0"
388
+ match = re.search(r"(\d+(?:\.\d+)*)", output)
389
+ if match:
390
+ return match.group(1)
391
+
392
+ elif tool_name == "prettier":
393
+ # prettier: "Prettier x.y.z" or just version
394
+ match = re.search(r"(\d+(?:\.\d+)*)", output)
395
+ if match:
396
+ return match.group(1)
397
+
398
+ elif tool_name == "eslint":
399
+ # eslint: "v9.0.0" or "9.0.0"
400
+ match = re.search(r"v?(\d+(?:\.\d+)*)", output)
401
+ if match:
402
+ return match.group(1)
403
+
404
+ elif tool_name == "actionlint":
405
+ # actionlint: "actionlint x.y.z" or just version
406
+ match = re.search(r"(\d+(?:\.\d+)*)", output)
407
+ if match:
408
+ return match.group(1)
409
+
410
+ elif tool_name == "darglint":
411
+ # darglint outputs just the version number
412
+ match = re.search(r"(\d+(?:\.\d+)*)", output)
413
+ if match:
414
+ return match.group(1)
415
+
416
+ elif tool_name == "markdownlint":
417
+ # markdownlint-cli2: "markdownlint-cli2 v0.19.1 (markdownlint v0.39.0)"
418
+ # Extract the cli2 version (first version number after "v")
419
+ match = re.search(
420
+ r"markdownlint-cli2\s+v(\d+(?:\.\d+)*)",
421
+ output,
422
+ re.IGNORECASE,
423
+ )
424
+ if match:
425
+ return match.group(1)
426
+ # Fallback: look for any version pattern
427
+ match = re.search(r"v(\d+(?:\.\d+)+)", output)
428
+ if match:
429
+ return match.group(1)
430
+
431
+ # Fallback: look for any version-like pattern
432
+ match = re.search(r"(\d+(?:\.\d+)+)", output)
433
+ if match:
434
+ return match.group(1)
435
+
436
+ return None
437
+
438
+
439
+ def get_all_tool_versions() -> dict[str, ToolVersionInfo]:
440
+ """Get version information for all supported tools.
441
+
442
+ Returns:
443
+ dict[str, ToolVersionInfo]: Tool name to version info mapping
444
+ """
445
+ # Define tool commands - this avoids circular imports
446
+ tool_commands = {
447
+ # Python bundled tools (available as scripts when installed)
448
+ "ruff": ["ruff"],
449
+ "black": ["black"],
450
+ "bandit": ["bandit"],
451
+ "yamllint": ["yamllint"],
452
+ "darglint": ["darglint"],
453
+ # Python user tools
454
+ "pytest": ["python", "-m", "pytest"],
455
+ # Node.js tools
456
+ "prettier": ["npx", "--yes", "prettier"],
457
+ "eslint": ["npx", "--yes", "eslint"],
458
+ "markdownlint": ["npx", "--yes", "markdownlint-cli2"],
459
+ # Binary tools
460
+ "hadolint": ["hadolint"],
461
+ "actionlint": ["actionlint"],
462
+ }
463
+
464
+ results = {}
465
+ minimum_versions = get_minimum_versions()
466
+ install_hints = get_install_hints()
467
+
468
+ for tool_name, command in tool_commands.items():
469
+ try:
470
+ results[tool_name] = check_tool_version(tool_name, command)
471
+ except Exception as e:
472
+ logger.debug(f"Failed to check version for {tool_name}: {e}")
473
+ min_version = minimum_versions.get(tool_name, "unknown")
474
+ install_hint = install_hints.get(tool_name, f"Install {tool_name}")
475
+ results[tool_name] = ToolVersionInfo(
476
+ name=tool_name,
477
+ min_version=min_version,
478
+ install_hint=install_hint,
479
+ error_message=f"Failed to check version: {e}",
480
+ )
481
+
482
+ return results
@@ -0,0 +1 @@
1
+ """Concrete tool integrations (Ruff, Black, Prettier, etc.)."""