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
@@ -1,6 +1,7 @@
1
1
  """Ruff Python linter and formatter integration."""
2
2
 
3
3
  import os
4
+ import subprocess # nosec B404 - used safely with shell disabled
4
5
  import tomllib
5
6
  from dataclasses import dataclass, field
6
7
  from pathlib import Path
@@ -14,6 +15,11 @@ from lintro.parsers.ruff.ruff_parser import (
14
15
  parse_ruff_format_check_output,
15
16
  parse_ruff_output,
16
17
  )
18
+ from lintro.tools.core.timeout_utils import (
19
+ create_timeout_result,
20
+ get_timeout_value,
21
+ run_subprocess_with_timeout,
22
+ )
17
23
  from lintro.tools.core.tool_base import BaseTool
18
24
  from lintro.utils.tool_utils import walk_files_with_excludes
19
25
 
@@ -31,7 +37,7 @@ def _load_ruff_config() -> dict:
31
37
  """Load ruff configuration from pyproject.toml.
32
38
 
33
39
  Returns:
34
- dict: Ruff configuration dictionary.
40
+ dict: Ruff configuration dictionary with flattened lint settings.
35
41
  """
36
42
  config: dict = {}
37
43
  pyproject_path = Path("pyproject.toml")
@@ -41,7 +47,20 @@ def _load_ruff_config() -> dict:
41
47
  with open(pyproject_path, "rb") as f:
42
48
  pyproject_data = tomllib.load(f)
43
49
  if "tool" in pyproject_data and "ruff" in pyproject_data["tool"]:
44
- config = pyproject_data["tool"]["ruff"]
50
+ ruff_config = pyproject_data["tool"]["ruff"]
51
+ # Copy top-level settings
52
+ config = dict(ruff_config)
53
+ # Flatten nested lint section to top level for easy access
54
+ if "lint" in ruff_config:
55
+ lint_config = ruff_config["lint"]
56
+ if "select" in lint_config:
57
+ config["select"] = lint_config["select"]
58
+ if "ignore" in lint_config:
59
+ config["ignore"] = lint_config["ignore"]
60
+ if "extend-select" in lint_config:
61
+ config["extend_select"] = lint_config["extend-select"]
62
+ if "extend-ignore" in lint_config:
63
+ config["extend_ignore"] = lint_config["extend-ignore"]
45
64
  except Exception as e:
46
65
  logger.warning(f"Failed to load ruff configuration: {e}")
47
66
 
@@ -54,12 +73,14 @@ def _load_lintro_ignore() -> list[str]:
54
73
  Returns:
55
74
  list[str]: List of ignore patterns.
56
75
  """
76
+ from lintro.utils.path_utils import find_lintro_ignore
77
+
57
78
  ignore_patterns: list[str] = []
58
- lintro_ignore_path = Path(".lintro-ignore")
79
+ lintro_ignore_path = find_lintro_ignore()
59
80
 
60
- if lintro_ignore_path.exists():
81
+ if lintro_ignore_path and lintro_ignore_path.exists():
61
82
  try:
62
- with open(lintro_ignore_path, "r", encoding="utf-8") as f:
83
+ with open(lintro_ignore_path, encoding="utf-8") as f:
63
84
  for line in f:
64
85
  line = line.strip()
65
86
  # Skip empty lines and comments
@@ -127,29 +148,40 @@ class RuffTool(BaseTool):
127
148
  def __post_init__(self) -> None:
128
149
  """Initialize the tool with default configuration."""
129
150
  super().__post_init__()
130
- # Load ruff configuration from pyproject.toml
131
- ruff_config = _load_ruff_config()
132
-
133
- # Load .lintro-ignore patterns
134
- lintro_ignore_patterns = _load_lintro_ignore()
135
-
136
- # Update exclude patterns from configuration and .lintro-ignore
137
- if "exclude" in ruff_config:
138
- self.exclude_patterns.extend(ruff_config["exclude"])
139
- if lintro_ignore_patterns:
140
- self.exclude_patterns.extend(lintro_ignore_patterns)
141
-
142
- # Update other options from configuration
143
- if "line_length" in ruff_config:
144
- self.options["line_length"] = ruff_config["line_length"]
145
- if "target_version" in ruff_config:
146
- self.options["target_version"] = ruff_config["target_version"]
147
- if "select" in ruff_config:
148
- self.options["select"] = ruff_config["select"]
149
- if "ignore" in ruff_config:
150
- self.options["ignore"] = ruff_config["ignore"]
151
- if "unsafe_fixes" in ruff_config:
152
- self.options["unsafe_fixes"] = ruff_config["unsafe_fixes"]
151
+
152
+ # Skip config loading in test mode to allow tests to set specific options
153
+ # without interference from pyproject.toml settings
154
+ if os.environ.get(RUFF_TEST_MODE_ENV) != RUFF_TEST_MODE_VALUE:
155
+ # Load ruff configuration from pyproject.toml
156
+ ruff_config = _load_ruff_config()
157
+
158
+ # Load .lintro-ignore patterns
159
+ lintro_ignore_patterns = _load_lintro_ignore()
160
+
161
+ # Update exclude patterns from configuration and .lintro-ignore
162
+ if "exclude" in ruff_config:
163
+ self.exclude_patterns.extend(ruff_config["exclude"])
164
+ if lintro_ignore_patterns:
165
+ self.exclude_patterns.extend(lintro_ignore_patterns)
166
+
167
+ # Update other options from configuration
168
+ if "line_length" in ruff_config:
169
+ self.options["line_length"] = ruff_config["line_length"]
170
+ if "target_version" in ruff_config:
171
+ self.options["target_version"] = ruff_config["target_version"]
172
+ if "select" in ruff_config:
173
+ self.options["select"] = ruff_config["select"]
174
+ if "ignore" in ruff_config:
175
+ self.options["ignore"] = ruff_config["ignore"]
176
+ if "unsafe_fixes" in ruff_config:
177
+ self.options["unsafe_fixes"] = ruff_config["unsafe_fixes"]
178
+
179
+ # Allow environment variable override for unsafe fixes
180
+ # Useful for development and CI environments
181
+ # This must come after config loading to override config values
182
+ env_unsafe_fixes = os.environ.get("RUFF_UNSAFE_FIXES", "").lower()
183
+ if env_unsafe_fixes in ("true", "1", "yes", "on"):
184
+ self.options["unsafe_fixes"] = True
153
185
 
154
186
  def set_options(
155
187
  self,
@@ -187,14 +219,26 @@ class RuffTool(BaseTool):
187
219
  Raises:
188
220
  ValueError: If an option value is invalid.
189
221
  """
190
- if select is not None and not isinstance(select, list):
191
- raise ValueError("select must be a list of rule codes")
192
- if ignore is not None and not isinstance(ignore, list):
193
- raise ValueError("ignore must be a list of rule codes")
194
- if extend_select is not None and not isinstance(extend_select, list):
195
- raise ValueError("extend_select must be a list of rule codes")
196
- if extend_ignore is not None and not isinstance(extend_ignore, list):
197
- raise ValueError("extend_ignore must be a list of rule codes")
222
+ if select is not None:
223
+ if isinstance(select, str):
224
+ select = [select]
225
+ elif not isinstance(select, list):
226
+ raise ValueError("select must be a string or list of rule codes")
227
+ if ignore is not None:
228
+ if isinstance(ignore, str):
229
+ ignore = [ignore]
230
+ elif not isinstance(ignore, list):
231
+ raise ValueError("ignore must be a string or list of rule codes")
232
+ if extend_select is not None:
233
+ if isinstance(extend_select, str):
234
+ extend_select = [extend_select]
235
+ elif not isinstance(extend_select, list):
236
+ raise ValueError("extend_select must be a string or list of rule codes")
237
+ if extend_ignore is not None:
238
+ if isinstance(extend_ignore, str):
239
+ extend_ignore = [extend_ignore]
240
+ elif not isinstance(extend_ignore, list):
241
+ raise ValueError("extend_ignore must be a string or list of rule codes")
198
242
  if line_length is not None:
199
243
  if not isinstance(line_length, int):
200
244
  raise ValueError("line_length must be an integer")
@@ -247,8 +291,16 @@ class RuffTool(BaseTool):
247
291
  """
248
292
  cmd: list[str] = self._get_executable_command(tool_name="ruff") + ["check"]
249
293
 
250
- # Add --isolated if in test mode
251
- if os.environ.get(RUFF_TEST_MODE_ENV) == RUFF_TEST_MODE_VALUE:
294
+ # Get enforced settings to avoid duplicate CLI args
295
+ enforced = self._get_enforced_settings()
296
+
297
+ # Add Lintro config injection args (--line-length, --target-version)
298
+ # from enforce tier. This takes precedence over native config and options
299
+ config_args = self._build_config_args()
300
+ if config_args:
301
+ cmd.extend(config_args)
302
+ # Add --isolated if in test mode (fallback when no Lintro config)
303
+ elif os.environ.get(RUFF_TEST_MODE_ENV) == RUFF_TEST_MODE_VALUE:
252
304
  cmd.append("--isolated")
253
305
 
254
306
  # Add configuration options
@@ -271,11 +323,15 @@ class RuffTool(BaseTool):
271
323
  cmd.extend(["--ignore", ",".join(sorted(ignored_rules))])
272
324
  if extend_selected_rules:
273
325
  cmd.extend(["--extend-select", ",".join(extend_selected_rules)])
274
- if self.options.get("extend_ignore"):
275
- cmd.extend(["--extend-ignore", ",".join(self.options["extend_ignore"])])
276
- if self.options.get("line_length"):
326
+ extend_ignored_rules = list(self.options.get("extend_ignore") or [])
327
+ if extend_ignored_rules:
328
+ cmd.extend(["--extend-ignore", ",".join(extend_ignored_rules)])
329
+ # Only add line_length/target_version from options if not enforced.
330
+ # Note: enforced uses Lintro's generic names (line_length, target_python)
331
+ # while options use tool-specific names (line_length, target_version).
332
+ if self.options.get("line_length") and "line_length" not in enforced:
277
333
  cmd.extend(["--line-length", str(self.options["line_length"])])
278
- if self.options.get("target_version"):
334
+ if self.options.get("target_version") and "target_python" not in enforced:
279
335
  cmd.extend(["--target-version", self.options["target_version"]])
280
336
 
281
337
  # Fix options
@@ -315,11 +371,16 @@ class RuffTool(BaseTool):
315
371
  if check_only:
316
372
  cmd.append("--check")
317
373
 
318
- # Add configuration options
319
- if self.options.get("line_length"):
320
- cmd.extend(["--line-length", str(self.options["line_length"])])
321
- if self.options.get("target_version"):
322
- cmd.extend(["--target-version", self.options["target_version"]])
374
+ # Add Lintro config injection args (--isolated, --config)
375
+ config_args = self._build_config_args()
376
+ if config_args:
377
+ cmd.extend(config_args)
378
+ else:
379
+ # Fallback to options-based configuration
380
+ if self.options.get("line_length"):
381
+ cmd.extend(["--line-length", str(self.options["line_length"])])
382
+ if self.options.get("target_version"):
383
+ cmd.extend(["--target-version", self.options["target_version"]])
323
384
 
324
385
  # Add files
325
386
  cmd.extend(files)
@@ -338,6 +399,11 @@ class RuffTool(BaseTool):
338
399
  Returns:
339
400
  ToolResult: ToolResult instance.
340
401
  """
402
+ # Check version requirements
403
+ version_result = self._verify_tool_version()
404
+ if version_result is not None:
405
+ return version_result
406
+
341
407
  self._validate_paths(paths=paths)
342
408
  if not paths:
343
409
  return ToolResult(
@@ -373,16 +439,31 @@ class RuffTool(BaseTool):
373
439
  os.path.relpath(f, cwd) if cwd else f for f in python_files
374
440
  ]
375
441
 
376
- timeout: int = self.options.get("timeout", RUFF_DEFAULT_TIMEOUT)
442
+ timeout: int = get_timeout_value(self, RUFF_DEFAULT_TIMEOUT)
377
443
  # Lint check
378
444
  cmd: list[str] = self._build_check_command(files=rel_files, fix=False)
379
445
  success_lint: bool
380
446
  output_lint: str
381
- success_lint, output_lint = self._run_subprocess(
382
- cmd=cmd,
383
- timeout=timeout,
384
- cwd=cwd,
385
- )
447
+ try:
448
+ success_lint, output_lint = run_subprocess_with_timeout(
449
+ tool=self,
450
+ cmd=cmd,
451
+ timeout=timeout,
452
+ cwd=cwd,
453
+ )
454
+ except subprocess.TimeoutExpired:
455
+ timeout_result = create_timeout_result(
456
+ tool=self,
457
+ timeout=timeout,
458
+ cmd=cmd,
459
+ )
460
+ return ToolResult(
461
+ name=self.name,
462
+ success=timeout_result["success"],
463
+ output=timeout_result["output"],
464
+ issues_count=timeout_result["issues_count"],
465
+ issues=timeout_result["issues"],
466
+ )
386
467
  lint_issues = parse_ruff_output(output=output_lint)
387
468
  lint_issues_count: int = len(lint_issues)
388
469
 
@@ -397,11 +478,26 @@ class RuffTool(BaseTool):
397
478
  )
398
479
  success_format: bool
399
480
  output_format: str
400
- success_format, output_format = self._run_subprocess(
401
- cmd=format_cmd,
402
- timeout=timeout,
403
- cwd=cwd,
404
- )
481
+ try:
482
+ success_format, output_format = self._run_subprocess(
483
+ cmd=format_cmd,
484
+ timeout=timeout,
485
+ cwd=cwd,
486
+ )
487
+ except subprocess.TimeoutExpired:
488
+ timeout_msg = (
489
+ f"Ruff execution timed out ({timeout}s limit exceeded).\n\n"
490
+ "This may indicate:\n"
491
+ " - Large codebase taking too long to process\n"
492
+ " - Need to increase timeout via --tool-options ruff:timeout=N"
493
+ )
494
+ return ToolResult(
495
+ name=self.name,
496
+ success=False,
497
+ output=timeout_msg,
498
+ issues_count=1, # Count timeout as execution failure
499
+ issues=lint_issues, # Include any lint issues found before timeout
500
+ )
405
501
  format_files = parse_ruff_format_check_output(output=output_format)
406
502
  # Normalize files to absolute paths to keep behavior consistent with
407
503
  # direct CLI calls and stabilize tests that compare exact paths.
@@ -445,6 +541,11 @@ class RuffTool(BaseTool):
445
541
  Returns:
446
542
  ToolResult: ToolResult instance.
447
543
  """
544
+ # Check version requirements
545
+ version_result = self._verify_tool_version()
546
+ if version_result is not None:
547
+ return version_result
548
+
448
549
  self._validate_paths(paths=paths)
449
550
  if not paths:
450
551
  return ToolResult(
@@ -471,7 +572,7 @@ class RuffTool(BaseTool):
471
572
  )
472
573
 
473
574
  logger.debug(f"Files to fix: {python_files}")
474
- timeout: int = self.options.get("timeout", RUFF_DEFAULT_TIMEOUT)
575
+ timeout: int = get_timeout_value(self, RUFF_DEFAULT_TIMEOUT)
475
576
  all_outputs: list[str] = []
476
577
  overall_success: bool = True
477
578
 
@@ -482,10 +583,28 @@ class RuffTool(BaseTool):
482
583
  cmd_check: list[str] = self._build_check_command(files=python_files, fix=False)
483
584
  success_check: bool
484
585
  output_check: str
485
- success_check, output_check = self._run_subprocess(
486
- cmd=cmd_check,
487
- timeout=timeout,
488
- )
586
+ try:
587
+ success_check, output_check = run_subprocess_with_timeout(
588
+ tool=self,
589
+ cmd=cmd_check,
590
+ timeout=timeout,
591
+ )
592
+ except subprocess.TimeoutExpired:
593
+ timeout_result = create_timeout_result(
594
+ tool=self,
595
+ timeout=timeout,
596
+ cmd=cmd_check,
597
+ )
598
+ return ToolResult(
599
+ name=self.name,
600
+ success=timeout_result["success"],
601
+ output=timeout_result["output"],
602
+ issues_count=timeout_result["issues_count"],
603
+ issues=timeout_result["issues"],
604
+ initial_issues_count=0,
605
+ fixed_issues_count=0,
606
+ remaining_issues_count=1,
607
+ )
489
608
  initial_issues = parse_ruff_output(output=output_check)
490
609
  initial_count: int = len(initial_issues)
491
610
 
@@ -499,10 +618,29 @@ class RuffTool(BaseTool):
499
618
  )
500
619
  success_format_check: bool
501
620
  output_format_check: str
502
- success_format_check, output_format_check = self._run_subprocess(
503
- cmd=format_cmd_check,
504
- timeout=timeout,
505
- )
621
+ try:
622
+ success_format_check, output_format_check = self._run_subprocess(
623
+ cmd=format_cmd_check,
624
+ timeout=timeout,
625
+ )
626
+ except subprocess.TimeoutExpired:
627
+ timeout_msg = (
628
+ f"Ruff execution timed out ({timeout}s limit exceeded).\n\n"
629
+ "This may indicate:\n"
630
+ " - Large codebase taking too long to process\n"
631
+ " - Need to increase timeout via --tool-options ruff:timeout=N"
632
+ )
633
+ return ToolResult(
634
+ name=self.name,
635
+ success=False,
636
+ output=timeout_msg,
637
+ issues_count=1, # Count timeout as execution failure
638
+ # Include any lint issues found before timeout
639
+ issues=initial_issues,
640
+ initial_issues_count=initial_count,
641
+ fixed_issues_count=0,
642
+ remaining_issues_count=1,
643
+ )
506
644
  format_files = parse_ruff_format_check_output(output=output_format_check)
507
645
  initial_format_count = len(format_files)
508
646
 
@@ -512,11 +650,29 @@ class RuffTool(BaseTool):
512
650
  # Optionally run ruff check --fix (lint fixes)
513
651
  remaining_issues = []
514
652
  remaining_count = 0
653
+ success: bool = True # Default to True when lint_fix is disabled
515
654
  if self.options.get("lint_fix", True):
516
655
  cmd: list[str] = self._build_check_command(files=python_files, fix=True)
517
- success: bool
518
656
  output: str
519
- success, output = self._run_subprocess(cmd=cmd, timeout=timeout)
657
+ try:
658
+ success, output = self._run_subprocess(cmd=cmd, timeout=timeout)
659
+ except subprocess.TimeoutExpired:
660
+ timeout_msg = (
661
+ f"Ruff execution timed out ({timeout}s limit exceeded).\n\n"
662
+ "This may indicate:\n"
663
+ " - Large codebase taking too long to process\n"
664
+ " - Need to increase timeout via --tool-options ruff:timeout=N"
665
+ )
666
+ return ToolResult(
667
+ name=self.name,
668
+ success=False,
669
+ output=timeout_msg,
670
+ issues_count=1, # Count timeout as execution failure
671
+ issues=initial_issues, # Include initial issues found
672
+ initial_issues_count=total_initial_count,
673
+ fixed_issues_count=0,
674
+ remaining_issues_count=1,
675
+ )
520
676
  remaining_issues = parse_ruff_output(output=output)
521
677
  remaining_count = len(remaining_issues)
522
678
 
@@ -547,11 +703,18 @@ class RuffTool(BaseTool):
547
703
  # Only run if not already run with unsafe fixes
548
704
  success_unsafe: bool
549
705
  output_unsafe: str
550
- success_unsafe, output_unsafe = self._run_subprocess(
551
- cmd=cmd_unsafe,
552
- timeout=timeout,
553
- )
554
- remaining_unsafe = parse_ruff_output(output=output_unsafe)
706
+ try:
707
+ success_unsafe, output_unsafe = self._run_subprocess(
708
+ cmd=cmd_unsafe,
709
+ timeout=timeout,
710
+ )
711
+ except subprocess.TimeoutExpired:
712
+ # If unsafe check times out, just continue with current results
713
+ # Don't fail the entire operation for this optional check
714
+ logger.debug("Unsafe fixes check timed out, skipping")
715
+ remaining_unsafe = remaining_issues
716
+ else:
717
+ remaining_unsafe = parse_ruff_output(output=output_unsafe)
555
718
  if len(remaining_unsafe) < remaining_count:
556
719
  all_outputs.append(
557
720
  "Some remaining issues could be fixed by enabling unsafe "
@@ -593,10 +756,28 @@ class RuffTool(BaseTool):
593
756
  )
594
757
  format_success: bool
595
758
  format_output: str
596
- format_success, format_output = self._run_subprocess(
597
- cmd=format_cmd,
598
- timeout=timeout,
599
- )
759
+ try:
760
+ format_success, format_output = self._run_subprocess(
761
+ cmd=format_cmd,
762
+ timeout=timeout,
763
+ )
764
+ except subprocess.TimeoutExpired:
765
+ timeout_msg = (
766
+ f"Ruff execution timed out ({timeout}s limit exceeded).\n\n"
767
+ "This may indicate:\n"
768
+ " - Large codebase taking too long to process\n"
769
+ " - Need to increase timeout via --tool-options ruff:timeout=N"
770
+ )
771
+ return ToolResult(
772
+ name=self.name,
773
+ success=False,
774
+ output=timeout_msg,
775
+ issues_count=1, # Count timeout as execution failure
776
+ issues=remaining_issues, # Include any issues found before timeout
777
+ initial_issues_count=total_initial_count,
778
+ fixed_issues_count=fixed_lint_count,
779
+ remaining_issues_count=1,
780
+ )
600
781
  # Formatting fixes are counted separately from lint fixes
601
782
  if initial_format_count > 0:
602
783
  fixed_count = fixed_lint_count + initial_format_count
@@ -621,10 +802,23 @@ class RuffTool(BaseTool):
621
802
 
622
803
  # Success should be based on whether there are remaining issues after fixing
623
804
  # If there are no initial issues, success should be True
624
- if total_initial_count == 0:
625
- overall_success = True
626
- else:
627
- overall_success = remaining_count == 0
805
+ overall_success = True if total_initial_count == 0 else remaining_count == 0
806
+
807
+ # Convert initial format files to RuffFormatIssue objects (these were fixed)
808
+ # and combine with remaining issues so formatter can split them by fixability
809
+ fixed_format_issues: list[RuffFormatIssue] = []
810
+ if format_files:
811
+ # Normalize files to absolute paths to keep behavior consistent
812
+ cwd: str | None = self.get_cwd(paths=python_files)
813
+ for file_path in format_files:
814
+ if cwd and not os.path.isabs(file_path):
815
+ absolute_path = os.path.abspath(os.path.join(cwd, file_path))
816
+ fixed_format_issues.append(RuffFormatIssue(file=absolute_path))
817
+ else:
818
+ fixed_format_issues.append(RuffFormatIssue(file=file_path))
819
+
820
+ # Combine fixed format issues with remaining lint issues
821
+ all_issues = fixed_format_issues + remaining_issues
628
822
 
629
823
  return ToolResult(
630
824
  name=self.name,
@@ -632,8 +826,8 @@ class RuffTool(BaseTool):
632
826
  output=final_output,
633
827
  # For fix operations, issues_count represents remaining for summaries
634
828
  issues_count=remaining_count,
635
- # Display remaining issues only to align tables with summary counts
636
- issues=remaining_issues,
829
+ # Display both fixed format issues and remaining lint issues
830
+ issues=all_issues,
637
831
  initial_issues_count=total_initial_count,
638
832
  fixed_issues_count=fixed_count,
639
833
  remaining_issues_count=remaining_count,