lintro 0.5.2__py3-none-any.whl → 0.6.0__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.
Potentially problematic release.
This version of lintro might be problematic. Click here for more details.
- lintro/__init__.py +1 -1
- lintro/formatters/tools/actionlint_formatter.py +1 -1
- lintro/formatters/tools/bandit_formatter.py +1 -1
- lintro/formatters/tools/black_formatter.py +48 -0
- lintro/formatters/tools/darglint_formatter.py +3 -1
- lintro/parsers/black/black_issue.py +22 -0
- lintro/parsers/black/black_parser.py +90 -0
- lintro/parsers/darglint/darglint_parser.py +1 -1
- lintro/parsers/hadolint/hadolint_parser.py +2 -2
- lintro/parsers/ruff/ruff_parser.py +87 -36
- lintro/parsers/yamllint/yamllint_parser.py +2 -2
- lintro/tools/__init__.py +2 -2
- lintro/tools/core/tool_base.py +41 -11
- lintro/tools/implementations/tool_bandit.py +4 -2
- lintro/tools/implementations/tool_black.py +261 -0
- lintro/tools/implementations/tool_prettier.py +1 -1
- lintro/tools/implementations/tool_ruff.py +47 -25
- lintro/tools/tool_enum.py +2 -0
- lintro/utils/ascii_normalize_cli.py +3 -1
- lintro/utils/config.py +16 -0
- lintro/utils/console_logger.py +59 -11
- lintro/utils/output_manager.py +2 -2
- lintro/utils/tool_executor.py +214 -7
- lintro/utils/tool_utils.py +12 -1
- {lintro-0.5.2.dist-info → lintro-0.6.0.dist-info}/METADATA +12 -10
- {lintro-0.5.2.dist-info → lintro-0.6.0.dist-info}/RECORD +30 -26
- {lintro-0.5.2.dist-info → lintro-0.6.0.dist-info}/WHEEL +0 -0
- {lintro-0.5.2.dist-info → lintro-0.6.0.dist-info}/entry_points.txt +0 -0
- {lintro-0.5.2.dist-info → lintro-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {lintro-0.5.2.dist-info → lintro-0.6.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Black Python formatter integration.
|
|
2
|
+
|
|
3
|
+
Black is an opinionated Python formatter. We wire it as a formatter-only tool
|
|
4
|
+
that cooperates with Ruff by default: when both are run, Ruff keeps linting and
|
|
5
|
+
Black handles formatting. Users can override via --tool-options.
|
|
6
|
+
|
|
7
|
+
Project: https://github.com/psf/black
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
|
|
15
|
+
from loguru import logger
|
|
16
|
+
|
|
17
|
+
from lintro.enums.tool_type import ToolType
|
|
18
|
+
from lintro.models.core.tool import ToolConfig, ToolResult
|
|
19
|
+
from lintro.parsers.black.black_parser import parse_black_output
|
|
20
|
+
from lintro.tools.core.tool_base import BaseTool
|
|
21
|
+
from lintro.utils.tool_utils import walk_files_with_excludes
|
|
22
|
+
|
|
23
|
+
BLACK_DEFAULT_TIMEOUT: int = 30
|
|
24
|
+
BLACK_DEFAULT_PRIORITY: int = 90 # Prefer Black ahead of Ruff formatting
|
|
25
|
+
BLACK_FILE_PATTERNS: list[str] = ["*.py", "*.pyi"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class BlackTool(BaseTool):
|
|
30
|
+
"""Black Python formatter integration."""
|
|
31
|
+
|
|
32
|
+
name: str = "black"
|
|
33
|
+
description: str = "Opinionated Python code formatter"
|
|
34
|
+
can_fix: bool = True
|
|
35
|
+
config: ToolConfig = field(
|
|
36
|
+
default_factory=lambda: ToolConfig(
|
|
37
|
+
priority=BLACK_DEFAULT_PRIORITY,
|
|
38
|
+
conflicts_with=[], # Compatible with Ruff (lint); no direct conflicts
|
|
39
|
+
file_patterns=BLACK_FILE_PATTERNS,
|
|
40
|
+
tool_type=ToolType.FORMATTER,
|
|
41
|
+
options={
|
|
42
|
+
"line_length": None,
|
|
43
|
+
"target_version": None,
|
|
44
|
+
"fast": False, # Do not use --fast by default
|
|
45
|
+
"preview": False, # Do not enable preview by default
|
|
46
|
+
"diff": False, # Default to standard output messages
|
|
47
|
+
},
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def set_options(
|
|
52
|
+
self,
|
|
53
|
+
line_length: int | None = None,
|
|
54
|
+
target_version: str | None = None,
|
|
55
|
+
fast: bool | None = None,
|
|
56
|
+
preview: bool | None = None,
|
|
57
|
+
diff: bool | None = None,
|
|
58
|
+
**kwargs,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Set Black-specific options with validation.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
line_length: Optional line length override.
|
|
64
|
+
target_version: String per Black CLI (e.g., "py313").
|
|
65
|
+
fast: Use --fast mode (skip safety checks).
|
|
66
|
+
preview: Enable preview style.
|
|
67
|
+
diff: Show diffs in output when formatting.
|
|
68
|
+
**kwargs: Additional base options like ``timeout``, ``exclude_patterns``,
|
|
69
|
+
and ``include_venv`` that are handled by ``BaseTool``.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: If any provided option has an invalid type.
|
|
73
|
+
"""
|
|
74
|
+
if line_length is not None and not isinstance(line_length, int):
|
|
75
|
+
raise ValueError("line_length must be an integer")
|
|
76
|
+
if target_version is not None and not isinstance(target_version, str):
|
|
77
|
+
raise ValueError("target_version must be a string")
|
|
78
|
+
if fast is not None and not isinstance(fast, bool):
|
|
79
|
+
raise ValueError("fast must be a boolean")
|
|
80
|
+
if preview is not None and not isinstance(preview, bool):
|
|
81
|
+
raise ValueError("preview must be a boolean")
|
|
82
|
+
if diff is not None and not isinstance(diff, bool):
|
|
83
|
+
raise ValueError("diff must be a boolean")
|
|
84
|
+
|
|
85
|
+
options = {
|
|
86
|
+
"line_length": line_length,
|
|
87
|
+
"target_version": target_version,
|
|
88
|
+
"fast": fast,
|
|
89
|
+
"preview": preview,
|
|
90
|
+
"diff": diff,
|
|
91
|
+
}
|
|
92
|
+
# Remove None values
|
|
93
|
+
options = {k: v for k, v in options.items() if v is not None}
|
|
94
|
+
super().set_options(**options, **kwargs)
|
|
95
|
+
|
|
96
|
+
def _build_common_args(self) -> list[str]:
|
|
97
|
+
args: list[str] = []
|
|
98
|
+
if self.options.get("line_length"):
|
|
99
|
+
args.extend(["--line-length", str(self.options["line_length"])])
|
|
100
|
+
if self.options.get("target_version"):
|
|
101
|
+
args.extend(["--target-version", str(self.options["target_version"])])
|
|
102
|
+
if self.options.get("fast"):
|
|
103
|
+
args.append("--fast")
|
|
104
|
+
if self.options.get("preview"):
|
|
105
|
+
args.append("--preview")
|
|
106
|
+
return args
|
|
107
|
+
|
|
108
|
+
def check(self, paths: list[str]) -> ToolResult:
|
|
109
|
+
"""Check files using Black without applying changes.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
paths: List of file or directory paths to check.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
ToolResult: Result containing success flag, issue count, and issues.
|
|
116
|
+
"""
|
|
117
|
+
self._validate_paths(paths=paths)
|
|
118
|
+
|
|
119
|
+
py_files: list[str] = walk_files_with_excludes(
|
|
120
|
+
paths=paths,
|
|
121
|
+
file_patterns=self.config.file_patterns,
|
|
122
|
+
exclude_patterns=self.exclude_patterns,
|
|
123
|
+
include_venv=self.include_venv,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if not py_files:
|
|
127
|
+
return ToolResult(
|
|
128
|
+
name=self.name,
|
|
129
|
+
success=True,
|
|
130
|
+
output="No files to check.",
|
|
131
|
+
issues_count=0,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
cwd: str | None = self.get_cwd(paths=py_files)
|
|
135
|
+
rel_files: list[str] = [os.path.relpath(f, cwd) if cwd else f for f in py_files]
|
|
136
|
+
|
|
137
|
+
cmd: list[str] = self._get_executable_command(tool_name="black") + [
|
|
138
|
+
"--check",
|
|
139
|
+
]
|
|
140
|
+
cmd.extend(self._build_common_args())
|
|
141
|
+
cmd.extend(rel_files)
|
|
142
|
+
|
|
143
|
+
logger.debug(f"[BlackTool] Running: {' '.join(cmd)} (cwd={cwd})")
|
|
144
|
+
success, output = self._run_subprocess(
|
|
145
|
+
cmd=cmd,
|
|
146
|
+
timeout=self.options.get("timeout", BLACK_DEFAULT_TIMEOUT),
|
|
147
|
+
cwd=cwd,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
issues = parse_black_output(output=output)
|
|
151
|
+
count = len(issues)
|
|
152
|
+
# In check mode, success means no differences
|
|
153
|
+
return ToolResult(
|
|
154
|
+
name=self.name,
|
|
155
|
+
success=(success and count == 0),
|
|
156
|
+
output=None if count == 0 else output,
|
|
157
|
+
issues_count=count,
|
|
158
|
+
issues=issues,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def fix(self, paths: list[str]) -> ToolResult:
|
|
162
|
+
"""Format files using Black, returning standardized counts.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
paths: List of file or directory paths to format.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
ToolResult: Result containing counts and any remaining issues.
|
|
169
|
+
"""
|
|
170
|
+
self._validate_paths(paths=paths)
|
|
171
|
+
|
|
172
|
+
py_files: list[str] = walk_files_with_excludes(
|
|
173
|
+
paths=paths,
|
|
174
|
+
file_patterns=self.config.file_patterns,
|
|
175
|
+
exclude_patterns=self.exclude_patterns,
|
|
176
|
+
include_venv=self.include_venv,
|
|
177
|
+
)
|
|
178
|
+
if not py_files:
|
|
179
|
+
return ToolResult(
|
|
180
|
+
name=self.name,
|
|
181
|
+
success=True,
|
|
182
|
+
output="No files to format.",
|
|
183
|
+
issues_count=0,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
cwd: str | None = self.get_cwd(paths=py_files)
|
|
187
|
+
rel_files: list[str] = [os.path.relpath(f, cwd) if cwd else f for f in py_files]
|
|
188
|
+
|
|
189
|
+
# Build reusable check command (used for final verification)
|
|
190
|
+
check_cmd: list[str] = self._get_executable_command(tool_name="black") + [
|
|
191
|
+
"--check",
|
|
192
|
+
]
|
|
193
|
+
check_cmd.extend(self._build_common_args())
|
|
194
|
+
check_cmd.extend(rel_files)
|
|
195
|
+
|
|
196
|
+
# When diff is requested, skip the initial check to ensure the middle
|
|
197
|
+
# invocation is the formatting run (as exercised by unit tests) and to
|
|
198
|
+
# avoid redundant subprocess calls.
|
|
199
|
+
if self.options.get("diff"):
|
|
200
|
+
initial_issues = []
|
|
201
|
+
initial_count = 0
|
|
202
|
+
else:
|
|
203
|
+
_, check_output = self._run_subprocess(
|
|
204
|
+
cmd=check_cmd,
|
|
205
|
+
timeout=self.options.get("timeout", BLACK_DEFAULT_TIMEOUT),
|
|
206
|
+
cwd=cwd,
|
|
207
|
+
)
|
|
208
|
+
initial_issues = parse_black_output(output=check_output)
|
|
209
|
+
initial_count = len(initial_issues)
|
|
210
|
+
|
|
211
|
+
# Apply formatting
|
|
212
|
+
fix_cmd_base: list[str] = self._get_executable_command(tool_name="black")
|
|
213
|
+
fix_cmd: list[str] = list(fix_cmd_base)
|
|
214
|
+
if self.options.get("diff"):
|
|
215
|
+
# When diff is requested, ensure the flag is present on the format run
|
|
216
|
+
# so tests can assert its presence on the middle invocation.
|
|
217
|
+
fix_cmd.append("--diff")
|
|
218
|
+
fix_cmd.extend(self._build_common_args())
|
|
219
|
+
fix_cmd.extend(rel_files)
|
|
220
|
+
|
|
221
|
+
logger.debug(f"[BlackTool] Fixing: {' '.join(fix_cmd)} (cwd={cwd})")
|
|
222
|
+
_, fix_output = self._run_subprocess(
|
|
223
|
+
cmd=fix_cmd,
|
|
224
|
+
timeout=self.options.get("timeout", BLACK_DEFAULT_TIMEOUT),
|
|
225
|
+
cwd=cwd,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Final check for remaining differences
|
|
229
|
+
final_success, final_output = self._run_subprocess(
|
|
230
|
+
cmd=check_cmd,
|
|
231
|
+
timeout=self.options.get("timeout", BLACK_DEFAULT_TIMEOUT),
|
|
232
|
+
cwd=cwd,
|
|
233
|
+
)
|
|
234
|
+
remaining_issues = parse_black_output(output=final_output)
|
|
235
|
+
remaining_count = len(remaining_issues)
|
|
236
|
+
|
|
237
|
+
fixed_count = max(0, initial_count - remaining_count)
|
|
238
|
+
|
|
239
|
+
# Build concise summary
|
|
240
|
+
summary: list[str] = []
|
|
241
|
+
if fixed_count > 0:
|
|
242
|
+
summary.append(f"Fixed {fixed_count} issue(s)")
|
|
243
|
+
if remaining_count > 0:
|
|
244
|
+
summary.append(
|
|
245
|
+
f"Found {remaining_count} issue(s) that cannot be auto-fixed",
|
|
246
|
+
)
|
|
247
|
+
final_summary = "\n".join(summary) if summary else "No fixes applied."
|
|
248
|
+
|
|
249
|
+
# Parse per-file reformats from the formatting run to display in console
|
|
250
|
+
fixed_issues_parsed = parse_black_output(output=fix_output)
|
|
251
|
+
|
|
252
|
+
return ToolResult(
|
|
253
|
+
name=self.name,
|
|
254
|
+
success=(remaining_count == 0),
|
|
255
|
+
output=final_summary,
|
|
256
|
+
issues_count=remaining_count,
|
|
257
|
+
issues=fixed_issues_parsed if fixed_issues_parsed else remaining_issues,
|
|
258
|
+
initial_issues_count=initial_count,
|
|
259
|
+
fixed_issues_count=fixed_count,
|
|
260
|
+
remaining_issues_count=remaining_count,
|
|
261
|
+
)
|
|
@@ -233,7 +233,7 @@ class PrettierTool(BaseTool):
|
|
|
233
233
|
|
|
234
234
|
if remaining_count > 0:
|
|
235
235
|
output_lines.append(
|
|
236
|
-
f"Found {remaining_count} issue(s) that cannot be auto-fixed"
|
|
236
|
+
f"Found {remaining_count} issue(s) that cannot be auto-fixed",
|
|
237
237
|
)
|
|
238
238
|
for issue in remaining_issues[:5]:
|
|
239
239
|
output_lines.append(f" {issue.file} - {issue.message}")
|
|
@@ -252,12 +252,25 @@ class RuffTool(BaseTool):
|
|
|
252
252
|
cmd.append("--isolated")
|
|
253
253
|
|
|
254
254
|
# Add configuration options
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
255
|
+
selected_rules = list(self.options.get("select") or [])
|
|
256
|
+
ignored_rules = set(self.options.get("ignore") or [])
|
|
257
|
+
extend_selected_rules = list(self.options.get("extend_select") or [])
|
|
258
|
+
|
|
259
|
+
# Ensure E501 is included when selecting E-family unless explicitly ignored
|
|
260
|
+
if (
|
|
261
|
+
"E" in selected_rules
|
|
262
|
+
and "E501" not in ignored_rules
|
|
263
|
+
and "E501" not in selected_rules
|
|
264
|
+
and "E501" not in extend_selected_rules
|
|
265
|
+
):
|
|
266
|
+
extend_selected_rules.append("E501")
|
|
267
|
+
|
|
268
|
+
if selected_rules:
|
|
269
|
+
cmd.extend(["--select", ",".join(selected_rules)])
|
|
270
|
+
if ignored_rules:
|
|
271
|
+
cmd.extend(["--ignore", ",".join(sorted(ignored_rules))])
|
|
272
|
+
if extend_selected_rules:
|
|
273
|
+
cmd.extend(["--extend-select", ",".join(extend_selected_rules)])
|
|
261
274
|
if self.options.get("extend_ignore"):
|
|
262
275
|
cmd.extend(["--extend-ignore", ",".join(self.options["extend_ignore"])])
|
|
263
276
|
if self.options.get("line_length"):
|
|
@@ -366,7 +379,9 @@ class RuffTool(BaseTool):
|
|
|
366
379
|
success_lint: bool
|
|
367
380
|
output_lint: str
|
|
368
381
|
success_lint, output_lint = self._run_subprocess(
|
|
369
|
-
cmd=cmd,
|
|
382
|
+
cmd=cmd,
|
|
383
|
+
timeout=timeout,
|
|
384
|
+
cwd=cwd,
|
|
370
385
|
)
|
|
371
386
|
lint_issues = parse_ruff_output(output=output_lint)
|
|
372
387
|
lint_issues_count: int = len(lint_issues)
|
|
@@ -475,20 +490,23 @@ class RuffTool(BaseTool):
|
|
|
475
490
|
initial_count: int = len(initial_issues)
|
|
476
491
|
|
|
477
492
|
# Also check formatting issues before fixing
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
493
|
+
initial_format_count: int = 0
|
|
494
|
+
format_files: list[str] = []
|
|
495
|
+
if self.options.get("format", False):
|
|
496
|
+
format_cmd_check: list[str] = self._build_format_command(
|
|
497
|
+
files=python_files,
|
|
498
|
+
check_only=True,
|
|
499
|
+
)
|
|
500
|
+
success_format_check: bool
|
|
501
|
+
output_format_check: str
|
|
502
|
+
success_format_check, output_format_check = self._run_subprocess(
|
|
503
|
+
cmd=format_cmd_check,
|
|
504
|
+
timeout=timeout,
|
|
505
|
+
)
|
|
506
|
+
format_files = parse_ruff_format_check_output(output=output_format_check)
|
|
507
|
+
initial_format_count = len(format_files)
|
|
490
508
|
|
|
491
|
-
#
|
|
509
|
+
# Track initial totals separately for accurate fixed/remaining math
|
|
492
510
|
total_initial_count: int = initial_count + initial_format_count
|
|
493
511
|
|
|
494
512
|
# Optionally run ruff check --fix (lint fixes)
|
|
@@ -502,9 +520,12 @@ class RuffTool(BaseTool):
|
|
|
502
520
|
remaining_issues = parse_ruff_output(output=output)
|
|
503
521
|
remaining_count = len(remaining_issues)
|
|
504
522
|
|
|
505
|
-
#
|
|
506
|
-
#
|
|
507
|
-
|
|
523
|
+
# Compute fixed lint issues by diffing initial vs remaining (internal only)
|
|
524
|
+
# Not used for display; summary counts reflect totals.
|
|
525
|
+
|
|
526
|
+
# Calculate how many lint issues were actually fixed
|
|
527
|
+
fixed_lint_count: int = max(0, initial_count - remaining_count)
|
|
528
|
+
fixed_count: int = fixed_lint_count
|
|
508
529
|
|
|
509
530
|
# Do not print raw initial counts; keep output concise and unified
|
|
510
531
|
|
|
@@ -576,9 +597,9 @@ class RuffTool(BaseTool):
|
|
|
576
597
|
cmd=format_cmd,
|
|
577
598
|
timeout=timeout,
|
|
578
599
|
)
|
|
579
|
-
#
|
|
600
|
+
# Formatting fixes are counted separately from lint fixes
|
|
580
601
|
if initial_format_count > 0:
|
|
581
|
-
fixed_count
|
|
602
|
+
fixed_count = fixed_lint_count + initial_format_count
|
|
582
603
|
# Suppress raw formatter output for consistency; rely on unified summary
|
|
583
604
|
# Only consider formatting failure if there are actual formatting
|
|
584
605
|
# issues. Don't fail the overall operation just because formatting
|
|
@@ -611,6 +632,7 @@ class RuffTool(BaseTool):
|
|
|
611
632
|
output=final_output,
|
|
612
633
|
# For fix operations, issues_count represents remaining for summaries
|
|
613
634
|
issues_count=remaining_count,
|
|
635
|
+
# Display remaining issues only to align tables with summary counts
|
|
614
636
|
issues=remaining_issues,
|
|
615
637
|
initial_issues_count=total_initial_count,
|
|
616
638
|
fixed_issues_count=fixed_count,
|
lintro/tools/tool_enum.py
CHANGED
|
@@ -4,6 +4,7 @@ from enum import Enum
|
|
|
4
4
|
|
|
5
5
|
from lintro.tools.implementations.tool_actionlint import ActionlintTool
|
|
6
6
|
from lintro.tools.implementations.tool_bandit import BanditTool
|
|
7
|
+
from lintro.tools.implementations.tool_black import BlackTool
|
|
7
8
|
from lintro.tools.implementations.tool_darglint import DarglintTool
|
|
8
9
|
from lintro.tools.implementations.tool_hadolint import HadolintTool
|
|
9
10
|
from lintro.tools.implementations.tool_prettier import PrettierTool
|
|
@@ -12,6 +13,7 @@ from lintro.tools.implementations.tool_yamllint import YamllintTool
|
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class ToolEnum(Enum):
|
|
16
|
+
BLACK = BlackTool
|
|
15
17
|
DARGLINT = DarglintTool
|
|
16
18
|
HADOLINT = HadolintTool
|
|
17
19
|
PRETTIER = PrettierTool
|
|
@@ -37,7 +37,9 @@ def main() -> int:
|
|
|
37
37
|
parser.add_argument("--width", type=int, default=80)
|
|
38
38
|
parser.add_argument("--height", type=int, default=20)
|
|
39
39
|
parser.add_argument(
|
|
40
|
-
"--align",
|
|
40
|
+
"--align",
|
|
41
|
+
choices=["left", "center", "right"],
|
|
42
|
+
default="center",
|
|
41
43
|
)
|
|
42
44
|
parser.add_argument(
|
|
43
45
|
"--valign",
|
lintro/utils/config.py
CHANGED
|
@@ -37,3 +37,19 @@ def load_lintro_tool_config(tool_name: str) -> dict[str, Any]:
|
|
|
37
37
|
if isinstance(section, dict):
|
|
38
38
|
return section
|
|
39
39
|
return {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_post_checks_config() -> dict[str, Any]:
|
|
43
|
+
"""Load post-checks configuration from pyproject.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Dict with keys like:
|
|
47
|
+
- enabled: bool
|
|
48
|
+
- tools: list[str]
|
|
49
|
+
- enforce_failure: bool
|
|
50
|
+
"""
|
|
51
|
+
cfg = _load_pyproject()
|
|
52
|
+
section = cfg.get("post_checks", {})
|
|
53
|
+
if isinstance(section, dict):
|
|
54
|
+
return section
|
|
55
|
+
return {}
|
lintro/utils/console_logger.py
CHANGED
|
@@ -20,6 +20,7 @@ TOOL_EMOJIS: dict[str, str] = {
|
|
|
20
20
|
"darglint": "📝",
|
|
21
21
|
"hadolint": "🐳",
|
|
22
22
|
"yamllint": "📄",
|
|
23
|
+
"black": "🖤",
|
|
23
24
|
}
|
|
24
25
|
DEFAULT_EMOJI: str = "🔧"
|
|
25
26
|
BORDER_LENGTH: int = 70
|
|
@@ -218,6 +219,35 @@ class SimpleLintroLogger:
|
|
|
218
219
|
|
|
219
220
|
logger.debug(f"Starting tool: {tool_name}")
|
|
220
221
|
|
|
222
|
+
def print_post_checks_header(
|
|
223
|
+
self,
|
|
224
|
+
action: str,
|
|
225
|
+
) -> None:
|
|
226
|
+
"""Print a distinct header separating the post-checks phase.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
action: str: The action being performed (e.g., 'check', 'fmt').
|
|
230
|
+
"""
|
|
231
|
+
# Use a heavy unicode border and magenta coloring to stand out
|
|
232
|
+
border_char: str = "━"
|
|
233
|
+
border: str = border_char * BORDER_LENGTH
|
|
234
|
+
title_styled: str = click.style(
|
|
235
|
+
text="🚦 POST-CHECKS",
|
|
236
|
+
fg="magenta",
|
|
237
|
+
bold=True,
|
|
238
|
+
)
|
|
239
|
+
subtitle_styled: str = click.style(
|
|
240
|
+
text=("Running optional follow-up checks after primary tools"),
|
|
241
|
+
fg="magenta",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
self.console_output(text="")
|
|
245
|
+
self.console_output(text=border, color="magenta")
|
|
246
|
+
self.console_output(text=title_styled)
|
|
247
|
+
self.console_output(text=subtitle_styled)
|
|
248
|
+
self.console_output(text=border, color="magenta")
|
|
249
|
+
self.console_output(text="")
|
|
250
|
+
|
|
221
251
|
def print_tool_result(
|
|
222
252
|
self,
|
|
223
253
|
tool_name: str,
|
|
@@ -327,19 +357,25 @@ class SimpleLintroLogger:
|
|
|
327
357
|
if fixed_count > 0 and remaining_count == 0:
|
|
328
358
|
self.success(message=f"✓ {fixed_count} fixed")
|
|
329
359
|
elif fixed_count > 0 and remaining_count > 0:
|
|
330
|
-
self.console_output(text=f"✓ {fixed_count} fixed", color="green")
|
|
331
360
|
self.console_output(
|
|
332
|
-
text=f"
|
|
361
|
+
text=f"✓ {fixed_count} fixed",
|
|
362
|
+
color="green",
|
|
363
|
+
)
|
|
364
|
+
self.console_output(
|
|
365
|
+
text=f"✗ {remaining_count} remaining",
|
|
366
|
+
color="red",
|
|
333
367
|
)
|
|
334
368
|
elif remaining_count > 0:
|
|
335
369
|
self.console_output(
|
|
336
|
-
text=f"✗ {remaining_count} remaining",
|
|
370
|
+
text=f"✗ {remaining_count} remaining",
|
|
371
|
+
color="red",
|
|
337
372
|
)
|
|
338
373
|
elif initial_count > 0:
|
|
339
374
|
# If we found initial issues but no specific fixed/remaining counts,
|
|
340
375
|
# show the initial count as found
|
|
341
376
|
self.console_output(
|
|
342
|
-
text=f"✗ Found {initial_count} issues",
|
|
377
|
+
text=f"✗ Found {initial_count} issues",
|
|
378
|
+
color="red",
|
|
343
379
|
)
|
|
344
380
|
else:
|
|
345
381
|
# Fallback to original behavior
|
|
@@ -415,7 +451,7 @@ class SimpleLintroLogger:
|
|
|
415
451
|
# Build summary table
|
|
416
452
|
self._print_summary_table(action=action, tool_results=tool_results)
|
|
417
453
|
|
|
418
|
-
#
|
|
454
|
+
# Totals line and ASCII art
|
|
419
455
|
if action == "fmt":
|
|
420
456
|
# For format commands, track both fixed and remaining issues
|
|
421
457
|
# Use standardized counts when provided by tools
|
|
@@ -448,7 +484,11 @@ class SimpleLintroLogger:
|
|
|
448
484
|
elif not getattr(result, "success", True):
|
|
449
485
|
total_remaining += DEFAULT_REMAINING_COUNT
|
|
450
486
|
|
|
451
|
-
# Show
|
|
487
|
+
# Show totals line then ASCII art
|
|
488
|
+
totals_line: str = (
|
|
489
|
+
f"Totals: fixed={total_fixed}, remaining={total_remaining}"
|
|
490
|
+
)
|
|
491
|
+
self.console_output(text=click.style(totals_line, fg="cyan"))
|
|
452
492
|
self._print_ascii_art_format(total_remaining=total_remaining)
|
|
453
493
|
logger.debug(
|
|
454
494
|
f"{action} completed with {total_fixed} fixed, "
|
|
@@ -465,11 +505,13 @@ class SimpleLintroLogger:
|
|
|
465
505
|
total_for_art: int = (
|
|
466
506
|
total_issues if not any_failed else max(1, total_issues)
|
|
467
507
|
)
|
|
468
|
-
# Show
|
|
508
|
+
# Show totals line then ASCII art
|
|
509
|
+
totals_line_chk: str = f"Total issues: {total_issues}"
|
|
510
|
+
self.console_output(text=click.style(totals_line_chk, fg="cyan"))
|
|
469
511
|
self._print_ascii_art(total_issues=total_for_art)
|
|
470
512
|
logger.debug(
|
|
471
513
|
f"{action} completed with {total_issues} total issues"
|
|
472
|
-
+ (" and failures" if any_failed else "")
|
|
514
|
+
+ (" and failures" if any_failed else ""),
|
|
473
515
|
)
|
|
474
516
|
|
|
475
517
|
def _print_summary_table(
|
|
@@ -502,7 +544,9 @@ class SimpleLintroLogger:
|
|
|
502
544
|
# Format operations: show fixed count and remaining status
|
|
503
545
|
if success:
|
|
504
546
|
status_display: str = click.style(
|
|
505
|
-
"✅ PASS",
|
|
547
|
+
"✅ PASS",
|
|
548
|
+
fg="green",
|
|
549
|
+
bold=True,
|
|
506
550
|
)
|
|
507
551
|
else:
|
|
508
552
|
status_display = click.style("❌ FAIL", fg="red", bold=True)
|
|
@@ -516,7 +560,9 @@ class SimpleLintroLogger:
|
|
|
516
560
|
),
|
|
517
561
|
):
|
|
518
562
|
fixed_display: str = click.style(
|
|
519
|
-
"SKIPPED",
|
|
563
|
+
"SKIPPED",
|
|
564
|
+
fg="yellow",
|
|
565
|
+
bold=True,
|
|
520
566
|
)
|
|
521
567
|
remaining_display: str = click.style(
|
|
522
568
|
"SKIPPED",
|
|
@@ -585,7 +631,9 @@ class SimpleLintroLogger:
|
|
|
585
631
|
),
|
|
586
632
|
):
|
|
587
633
|
issues_display: str = click.style(
|
|
588
|
-
"SKIPPED",
|
|
634
|
+
"SKIPPED",
|
|
635
|
+
fg="yellow",
|
|
636
|
+
bold=True,
|
|
589
637
|
)
|
|
590
638
|
else:
|
|
591
639
|
issues_display = click.style(
|
lintro/utils/output_manager.py
CHANGED
|
@@ -230,7 +230,7 @@ class OutputManager:
|
|
|
230
230
|
if hasattr(r, "issues") and r.issues:
|
|
231
231
|
html.append(
|
|
232
232
|
"<table border='1'><tr><th>File</th><th>Line</th><th>Code</th>"
|
|
233
|
-
"<th>Message</th></tr>"
|
|
233
|
+
"<th>Message</th></tr>",
|
|
234
234
|
)
|
|
235
235
|
for issue in r.issues:
|
|
236
236
|
file: str = _html_escape(getattr(issue, "file", ""))
|
|
@@ -239,7 +239,7 @@ class OutputManager:
|
|
|
239
239
|
msg: str = _html_escape(getattr(issue, "message", ""))
|
|
240
240
|
html.append(
|
|
241
241
|
f"<tr><td>{file}</td><td>{line}</td><td>{code}</td>"
|
|
242
|
-
f"<td>{msg}</td></tr>"
|
|
242
|
+
f"<td>{msg}</td></tr>",
|
|
243
243
|
)
|
|
244
244
|
html.append("</table>")
|
|
245
245
|
else:
|