lintro 0.3.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.
Potentially problematic release.
This version of lintro might be problematic. Click here for more details.
- lintro/__init__.py +3 -0
- lintro/__main__.py +6 -0
- lintro/ascii-art/fail.txt +404 -0
- lintro/ascii-art/success.txt +484 -0
- lintro/cli.py +70 -0
- lintro/cli_utils/__init__.py +7 -0
- lintro/cli_utils/commands/__init__.py +7 -0
- lintro/cli_utils/commands/check.py +210 -0
- lintro/cli_utils/commands/format.py +167 -0
- lintro/cli_utils/commands/list_tools.py +114 -0
- lintro/enums/__init__.py +0 -0
- lintro/enums/action.py +29 -0
- lintro/enums/darglint_strictness.py +22 -0
- lintro/enums/group_by.py +31 -0
- lintro/enums/hadolint_enums.py +46 -0
- lintro/enums/output_format.py +40 -0
- lintro/enums/tool_name.py +36 -0
- lintro/enums/tool_type.py +27 -0
- lintro/enums/yamllint_format.py +22 -0
- lintro/exceptions/__init__.py +0 -0
- lintro/exceptions/errors.py +15 -0
- lintro/formatters/__init__.py +0 -0
- lintro/formatters/core/__init__.py +0 -0
- lintro/formatters/core/output_style.py +21 -0
- lintro/formatters/core/table_descriptor.py +24 -0
- lintro/formatters/styles/__init__.py +17 -0
- lintro/formatters/styles/csv.py +41 -0
- lintro/formatters/styles/grid.py +91 -0
- lintro/formatters/styles/html.py +48 -0
- lintro/formatters/styles/json.py +61 -0
- lintro/formatters/styles/markdown.py +41 -0
- lintro/formatters/styles/plain.py +39 -0
- lintro/formatters/tools/__init__.py +35 -0
- lintro/formatters/tools/darglint_formatter.py +72 -0
- lintro/formatters/tools/hadolint_formatter.py +84 -0
- lintro/formatters/tools/prettier_formatter.py +76 -0
- lintro/formatters/tools/ruff_formatter.py +116 -0
- lintro/formatters/tools/yamllint_formatter.py +87 -0
- lintro/models/__init__.py +0 -0
- lintro/models/core/__init__.py +0 -0
- lintro/models/core/tool.py +104 -0
- lintro/models/core/tool_config.py +23 -0
- lintro/models/core/tool_result.py +39 -0
- lintro/parsers/__init__.py +0 -0
- lintro/parsers/darglint/__init__.py +0 -0
- lintro/parsers/darglint/darglint_issue.py +9 -0
- lintro/parsers/darglint/darglint_parser.py +62 -0
- lintro/parsers/hadolint/__init__.py +1 -0
- lintro/parsers/hadolint/hadolint_issue.py +24 -0
- lintro/parsers/hadolint/hadolint_parser.py +65 -0
- lintro/parsers/prettier/__init__.py +0 -0
- lintro/parsers/prettier/prettier_issue.py +10 -0
- lintro/parsers/prettier/prettier_parser.py +60 -0
- lintro/parsers/ruff/__init__.py +1 -0
- lintro/parsers/ruff/ruff_issue.py +43 -0
- lintro/parsers/ruff/ruff_parser.py +89 -0
- lintro/parsers/yamllint/__init__.py +0 -0
- lintro/parsers/yamllint/yamllint_issue.py +24 -0
- lintro/parsers/yamllint/yamllint_parser.py +68 -0
- lintro/tools/__init__.py +40 -0
- lintro/tools/core/__init__.py +0 -0
- lintro/tools/core/tool_base.py +320 -0
- lintro/tools/core/tool_manager.py +167 -0
- lintro/tools/implementations/__init__.py +0 -0
- lintro/tools/implementations/tool_darglint.py +245 -0
- lintro/tools/implementations/tool_hadolint.py +302 -0
- lintro/tools/implementations/tool_prettier.py +270 -0
- lintro/tools/implementations/tool_ruff.py +618 -0
- lintro/tools/implementations/tool_yamllint.py +240 -0
- lintro/tools/tool_enum.py +17 -0
- lintro/utils/__init__.py +0 -0
- lintro/utils/ascii_normalize_cli.py +84 -0
- lintro/utils/config.py +39 -0
- lintro/utils/console_logger.py +783 -0
- lintro/utils/formatting.py +173 -0
- lintro/utils/output_manager.py +301 -0
- lintro/utils/path_utils.py +41 -0
- lintro/utils/tool_executor.py +443 -0
- lintro/utils/tool_utils.py +431 -0
- lintro-0.3.2.dist-info/METADATA +338 -0
- lintro-0.3.2.dist-info/RECORD +85 -0
- lintro-0.3.2.dist-info/WHEEL +5 -0
- lintro-0.3.2.dist-info/entry_points.txt +2 -0
- lintro-0.3.2.dist-info/licenses/LICENSE +21 -0
- lintro-0.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
"""Simplified Loguru-based logging utility for Lintro.
|
|
2
|
+
|
|
3
|
+
Single responsibility: Handle console display and file logging using Loguru.
|
|
4
|
+
No tee, no stream redirection, clean and simple with rich formatting.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
from lintro.utils.formatting import read_ascii_art
|
|
15
|
+
|
|
16
|
+
# Constants
|
|
17
|
+
TOOL_EMOJIS: dict[str, str] = {
|
|
18
|
+
"ruff": "🦀",
|
|
19
|
+
"prettier": "💅",
|
|
20
|
+
"darglint": "📝",
|
|
21
|
+
"hadolint": "🐳",
|
|
22
|
+
"yamllint": "📄",
|
|
23
|
+
}
|
|
24
|
+
DEFAULT_EMOJI: str = "🔧"
|
|
25
|
+
BORDER_LENGTH: int = 70
|
|
26
|
+
INFO_BORDER_LENGTH: int = 70
|
|
27
|
+
DEFAULT_REMAINING_COUNT: int = 1
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Regex patterns used to parse tool outputs for remaining issue counts
|
|
31
|
+
# Centralized to avoid repeated long literals and to keep matching logic
|
|
32
|
+
# consistent across the module.
|
|
33
|
+
RE_CANNOT_AUTOFIX: re.Pattern[str] = re.compile(
|
|
34
|
+
r"Found\s+(\d+)\s+issue\(s\)\s+that\s+cannot\s+be\s+auto-fixed",
|
|
35
|
+
)
|
|
36
|
+
RE_REMAINING_OR_CANNOT: re.Pattern[str] = re.compile(
|
|
37
|
+
r"(\d+)\s+(?:issue\(s\)\s+)?(?:that\s+cannot\s+be\s+auto-fixed|remaining)",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_tool_emoji(tool_name: str) -> str:
|
|
42
|
+
"""Get emoji for a tool.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
tool_name: str: Name of the tool.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
str: Emoji for the tool.
|
|
49
|
+
"""
|
|
50
|
+
return TOOL_EMOJIS.get(tool_name, DEFAULT_EMOJI)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SimpleLintroLogger:
|
|
54
|
+
"""Simplified logger for lintro using Loguru with rich console output."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
run_dir: Path,
|
|
59
|
+
verbose: bool = False,
|
|
60
|
+
raw_output: bool = False,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Initialize the logger.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
run_dir: Path: Directory for log files.
|
|
66
|
+
verbose: bool: Whether to enable verbose logging.
|
|
67
|
+
raw_output: bool: Whether to show raw tool output instead of \
|
|
68
|
+
formatted output.
|
|
69
|
+
"""
|
|
70
|
+
self.run_dir = run_dir
|
|
71
|
+
self.verbose = verbose
|
|
72
|
+
self.raw_output = raw_output
|
|
73
|
+
self.console_messages: list[str] = [] # Track console output for console.log
|
|
74
|
+
|
|
75
|
+
# Configure Loguru
|
|
76
|
+
self._setup_loguru()
|
|
77
|
+
|
|
78
|
+
def _setup_loguru(self) -> None:
|
|
79
|
+
"""Configure Loguru with clean, simple handlers."""
|
|
80
|
+
# Remove default handler
|
|
81
|
+
logger.remove()
|
|
82
|
+
|
|
83
|
+
# Add console handler (for immediate display)
|
|
84
|
+
console_level: str = "DEBUG" if self.verbose else "INFO"
|
|
85
|
+
logger.add(
|
|
86
|
+
sys.stderr,
|
|
87
|
+
level=console_level,
|
|
88
|
+
format=(
|
|
89
|
+
"<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | "
|
|
90
|
+
"{message}"
|
|
91
|
+
),
|
|
92
|
+
colorize=True,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Add debug.log handler (captures everything)
|
|
96
|
+
debug_log_path: Path = self.run_dir / "debug.log"
|
|
97
|
+
logger.add(
|
|
98
|
+
debug_log_path,
|
|
99
|
+
level="DEBUG",
|
|
100
|
+
format=(
|
|
101
|
+
"{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | "
|
|
102
|
+
"{name}:{function}:{line} | {message}"
|
|
103
|
+
),
|
|
104
|
+
rotation=None, # Don't rotate, each run gets its own file
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def info(self, message: str, **kwargs) -> None:
|
|
108
|
+
"""Log an info message to the console.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
message: str: The message to log.
|
|
112
|
+
**kwargs: Additional keyword arguments for formatting.
|
|
113
|
+
"""
|
|
114
|
+
self.console_messages.append(message)
|
|
115
|
+
logger.info(message, **kwargs)
|
|
116
|
+
|
|
117
|
+
def debug(self, message: str, **kwargs) -> None:
|
|
118
|
+
"""Log debug message.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
message: str: The debug message to log.
|
|
122
|
+
**kwargs: Additional keyword arguments for formatting.
|
|
123
|
+
"""
|
|
124
|
+
logger.debug(message, **kwargs)
|
|
125
|
+
|
|
126
|
+
def warning(self, message: str, **kwargs) -> None:
|
|
127
|
+
"""Log a warning message to the console.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
message: str: The message to log.
|
|
131
|
+
**kwargs: Additional keyword arguments for formatting.
|
|
132
|
+
"""
|
|
133
|
+
self.console_messages.append(f"WARNING: {message}")
|
|
134
|
+
logger.warning(message, **kwargs)
|
|
135
|
+
|
|
136
|
+
def error(self, message: str, **kwargs) -> None:
|
|
137
|
+
"""Log an error message to the console.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
message: str: The message to log.
|
|
141
|
+
**kwargs: Additional keyword arguments for formatting.
|
|
142
|
+
"""
|
|
143
|
+
self.console_messages.append(f"ERROR: {message}")
|
|
144
|
+
logger.error(message, **kwargs)
|
|
145
|
+
|
|
146
|
+
def console_output(
|
|
147
|
+
self,
|
|
148
|
+
text: str,
|
|
149
|
+
color: str | None = None,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Display text on console and track for console.log.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
text: str: Text to display.
|
|
155
|
+
color: str | None: Optional color for the text.
|
|
156
|
+
"""
|
|
157
|
+
if color:
|
|
158
|
+
click.echo(click.style(text, fg=color))
|
|
159
|
+
else:
|
|
160
|
+
click.echo(text)
|
|
161
|
+
|
|
162
|
+
# Track for console.log (without color codes)
|
|
163
|
+
self.console_messages.append(text)
|
|
164
|
+
|
|
165
|
+
def success(self, message: str, **kwargs) -> None:
|
|
166
|
+
"""Log a success message to the console.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
message: str: The message to log.
|
|
170
|
+
**kwargs: Additional keyword arguments for formatting.
|
|
171
|
+
"""
|
|
172
|
+
self.console_output(text=message, color="green")
|
|
173
|
+
logger.debug(f"SUCCESS: {message}")
|
|
174
|
+
|
|
175
|
+
def print_lintro_header(
|
|
176
|
+
self,
|
|
177
|
+
action: str,
|
|
178
|
+
tool_count: int,
|
|
179
|
+
tools_list: str,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Print the main LINTRO header.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
action: str: The action being performed.
|
|
185
|
+
tool_count: int: The number of tools being run.
|
|
186
|
+
tools_list: str: The list of tools being run.
|
|
187
|
+
"""
|
|
188
|
+
header_msg: str = click.style(
|
|
189
|
+
f"[LINTRO] All output formats will be auto-generated in {self.run_dir}",
|
|
190
|
+
fg="cyan",
|
|
191
|
+
bold=True,
|
|
192
|
+
)
|
|
193
|
+
self.console_output(text=header_msg)
|
|
194
|
+
logger.debug(f"Starting {action} with {tool_count} tools: {tools_list}")
|
|
195
|
+
|
|
196
|
+
def print_tool_header(
|
|
197
|
+
self,
|
|
198
|
+
tool_name: str,
|
|
199
|
+
action: str,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Print the header for a tool's output.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
tool_name: str: The name of the tool.
|
|
205
|
+
action: str: The action being performed (e.g., 'check', 'fmt').
|
|
206
|
+
"""
|
|
207
|
+
emoji: str = get_tool_emoji(tool_name)
|
|
208
|
+
emojis: str = (emoji + " ") * 5
|
|
209
|
+
|
|
210
|
+
border: str = "=" * BORDER_LENGTH
|
|
211
|
+
header: str = f"✨ Running {tool_name} ({action}) {emojis}"
|
|
212
|
+
|
|
213
|
+
self.console_output(text="")
|
|
214
|
+
self.console_output(text=border)
|
|
215
|
+
self.console_output(text=header)
|
|
216
|
+
self.console_output(text=border)
|
|
217
|
+
self.console_output(text="")
|
|
218
|
+
|
|
219
|
+
logger.debug(f"Starting tool: {tool_name}")
|
|
220
|
+
|
|
221
|
+
def print_tool_result(
|
|
222
|
+
self,
|
|
223
|
+
tool_name: str,
|
|
224
|
+
output: str,
|
|
225
|
+
issues_count: int,
|
|
226
|
+
raw_output_for_meta: str | None = None,
|
|
227
|
+
action: str = "check",
|
|
228
|
+
) -> None:
|
|
229
|
+
"""Print the result for a tool.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
tool_name: str: The name of the tool.
|
|
233
|
+
output: str: The output from the tool.
|
|
234
|
+
issues_count: int: The number of issues found.
|
|
235
|
+
raw_output_for_meta: str | None: Raw tool output used to extract
|
|
236
|
+
fixable/remaining hints when available.
|
|
237
|
+
action: str: The action being performed ("check" or "fmt").
|
|
238
|
+
"""
|
|
239
|
+
if output and output.strip():
|
|
240
|
+
# Display the output (either raw or formatted, depending on what was passed)
|
|
241
|
+
self.console_output(text=output)
|
|
242
|
+
logger.debug(f"Tool {tool_name} output: {len(output)} characters")
|
|
243
|
+
else:
|
|
244
|
+
logger.debug(f"Tool {tool_name} produced no output")
|
|
245
|
+
|
|
246
|
+
# Print result status
|
|
247
|
+
if issues_count == 0:
|
|
248
|
+
# For format action, prefer consolidated fixed summary if present
|
|
249
|
+
if action == "fmt" and output and output.strip():
|
|
250
|
+
# If output contains a consolidated fixed count, surface it
|
|
251
|
+
m_fixed = re.search(r"Fixed (\d+) issue\(s\)", output)
|
|
252
|
+
m_remaining = re.search(
|
|
253
|
+
r"Found (\d+) issue\(s\) that cannot be auto-fixed",
|
|
254
|
+
output,
|
|
255
|
+
)
|
|
256
|
+
fixed_val = int(m_fixed.group(1)) if m_fixed else 0
|
|
257
|
+
remaining_val = int(m_remaining.group(1)) if m_remaining else 0
|
|
258
|
+
if fixed_val > 0 or remaining_val > 0:
|
|
259
|
+
if fixed_val > 0:
|
|
260
|
+
self.console_output(text=f"✓ {fixed_val} fixed", color="green")
|
|
261
|
+
if remaining_val > 0:
|
|
262
|
+
self.console_output(
|
|
263
|
+
text=f"✗ {remaining_val} remaining",
|
|
264
|
+
color="red",
|
|
265
|
+
)
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
# Check if the output indicates no files were processed
|
|
269
|
+
if output and any(
|
|
270
|
+
(msg in output for msg in ["No files to", "No Python files found to"]),
|
|
271
|
+
):
|
|
272
|
+
self.console_output(
|
|
273
|
+
text=("⚠️ No files processed (excluded by patterns)"),
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
# For format operations, check if there are remaining issues that
|
|
277
|
+
# couldn't be auto-fixed
|
|
278
|
+
if output and "cannot be auto-fixed" in output.lower():
|
|
279
|
+
# Don't show "No issues found" if there are remaining issues
|
|
280
|
+
pass
|
|
281
|
+
else:
|
|
282
|
+
self.success(message="✓ No issues found.")
|
|
283
|
+
else:
|
|
284
|
+
# For format operations, parse the output to show better messages
|
|
285
|
+
if output and ("Fixed" in output or "issue(s)" in output):
|
|
286
|
+
# This is a format operation - parse for better messaging
|
|
287
|
+
# Prefer standardized counters if present in the output object
|
|
288
|
+
fixed_count: int = (
|
|
289
|
+
getattr(output, "fixed_issues_count", None)
|
|
290
|
+
if hasattr(output, "fixed_issues_count")
|
|
291
|
+
else None
|
|
292
|
+
)
|
|
293
|
+
remaining_count: int = (
|
|
294
|
+
getattr(output, "remaining_issues_count", None)
|
|
295
|
+
if hasattr(output, "remaining_issues_count")
|
|
296
|
+
else None
|
|
297
|
+
)
|
|
298
|
+
initial_count: int = (
|
|
299
|
+
getattr(output, "initial_issues_count", None)
|
|
300
|
+
if hasattr(output, "initial_issues_count")
|
|
301
|
+
else None
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Fallback to regex parsing when standardized counts are not available
|
|
305
|
+
if fixed_count is None:
|
|
306
|
+
fixed_match = re.search(r"Fixed (\d+) issue\(s\)", output)
|
|
307
|
+
fixed_count = int(fixed_match.group(1)) if fixed_match else 0
|
|
308
|
+
if remaining_count is None:
|
|
309
|
+
remaining_match = re.search(
|
|
310
|
+
r"Found (\d+) issue\(s\) that cannot be auto-fixed",
|
|
311
|
+
output,
|
|
312
|
+
)
|
|
313
|
+
remaining_count = (
|
|
314
|
+
int(remaining_match.group(1)) if remaining_match else 0
|
|
315
|
+
)
|
|
316
|
+
if initial_count is None:
|
|
317
|
+
initial_match = re.search(r"Found (\d+) errors?", output)
|
|
318
|
+
initial_count = int(initial_match.group(1)) if initial_match else 0
|
|
319
|
+
|
|
320
|
+
if fixed_count > 0 and remaining_count == 0:
|
|
321
|
+
self.success(message=f"✓ {fixed_count} fixed")
|
|
322
|
+
elif fixed_count > 0 and remaining_count > 0:
|
|
323
|
+
self.console_output(text=f"✓ {fixed_count} fixed", color="green")
|
|
324
|
+
self.console_output(
|
|
325
|
+
text=f"✗ {remaining_count} remaining", color="red"
|
|
326
|
+
)
|
|
327
|
+
elif remaining_count > 0:
|
|
328
|
+
self.console_output(
|
|
329
|
+
text=f"✗ {remaining_count} remaining", color="red"
|
|
330
|
+
)
|
|
331
|
+
elif initial_count > 0:
|
|
332
|
+
# If we found initial issues but no specific fixed/remaining counts,
|
|
333
|
+
# show the initial count as found
|
|
334
|
+
self.console_output(
|
|
335
|
+
text=f"✗ Found {initial_count} issues", color="red"
|
|
336
|
+
)
|
|
337
|
+
else:
|
|
338
|
+
# Fallback to original behavior
|
|
339
|
+
error_msg: str = f"✗ Found {issues_count} issues"
|
|
340
|
+
self.console_output(text=error_msg, color="red")
|
|
341
|
+
else:
|
|
342
|
+
# Show issue count with action-aware phrasing
|
|
343
|
+
if action == "fmt":
|
|
344
|
+
error_msg: str = f"✗ {issues_count} issue(s) cannot be auto-fixed"
|
|
345
|
+
else:
|
|
346
|
+
error_msg = f"✗ Found {issues_count} issues"
|
|
347
|
+
self.console_output(text=error_msg, color="red")
|
|
348
|
+
|
|
349
|
+
# Check if there are fixable issues and show warning
|
|
350
|
+
raw_text = (
|
|
351
|
+
raw_output_for_meta if raw_output_for_meta is not None else output
|
|
352
|
+
)
|
|
353
|
+
# Sum all fixable counts if multiple sections are present
|
|
354
|
+
if raw_text and action != "fmt":
|
|
355
|
+
# Sum any reported fixable lint issues
|
|
356
|
+
matches = re.findall(r"\[\*\]\s+(\d+)\s+fixable", raw_text)
|
|
357
|
+
fixable_count: int = sum(int(m) for m in matches) if matches else 0
|
|
358
|
+
# Add formatting issues as fixable by fmt when ruff reports them
|
|
359
|
+
if tool_name == "ruff" and (
|
|
360
|
+
"Formatting issues:" in raw_text or "Would reformat" in raw_text
|
|
361
|
+
):
|
|
362
|
+
# Count files listed in 'Would reformat:' lines
|
|
363
|
+
reformat_files = re.findall(r"Would reformat:\s+(.+)", raw_text)
|
|
364
|
+
fixable_count += len(reformat_files)
|
|
365
|
+
# Or try summary line like: "N files would be reformatted"
|
|
366
|
+
if fixable_count == 0:
|
|
367
|
+
m_sum = re.search(
|
|
368
|
+
r"(\d+)\s+file(?:s)?\s+would\s+be\s+reformatted",
|
|
369
|
+
raw_text,
|
|
370
|
+
)
|
|
371
|
+
if m_sum:
|
|
372
|
+
fixable_count += int(m_sum.group(1))
|
|
373
|
+
|
|
374
|
+
if fixable_count > 0:
|
|
375
|
+
hint_a: str = "💡 "
|
|
376
|
+
hint_b: str = (
|
|
377
|
+
f"{fixable_count} formatting/linting issue(s) "
|
|
378
|
+
"can be auto-fixed "
|
|
379
|
+
)
|
|
380
|
+
hint_c: str = "with `lintro format`"
|
|
381
|
+
self.console_output(
|
|
382
|
+
text=hint_a + hint_b + hint_c,
|
|
383
|
+
color="yellow",
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Remove redundant tip; consolidated above as a single auto-fix message
|
|
387
|
+
|
|
388
|
+
self.console_output(text="") # Blank line after each tool
|
|
389
|
+
|
|
390
|
+
def print_execution_summary(
|
|
391
|
+
self,
|
|
392
|
+
action: str,
|
|
393
|
+
tool_results: list[object],
|
|
394
|
+
) -> None:
|
|
395
|
+
"""Print the execution summary for all tools.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
action: str: The action being performed ("check" or "fmt").
|
|
399
|
+
tool_results: list[object]: The list of tool results.
|
|
400
|
+
"""
|
|
401
|
+
# Execution summary section
|
|
402
|
+
summary_header: str = click.style("📋 EXECUTION SUMMARY", fg="cyan", bold=True)
|
|
403
|
+
border_line: str = click.style("=" * 50, fg="cyan")
|
|
404
|
+
|
|
405
|
+
self.console_output(text=summary_header)
|
|
406
|
+
self.console_output(text=border_line)
|
|
407
|
+
|
|
408
|
+
# Build summary table
|
|
409
|
+
self._print_summary_table(action=action, tool_results=tool_results)
|
|
410
|
+
|
|
411
|
+
# Final status and ASCII art
|
|
412
|
+
if action == "fmt":
|
|
413
|
+
# For format commands, track both fixed and remaining issues
|
|
414
|
+
# Use standardized counts when provided by tools
|
|
415
|
+
total_fixed: int = 0
|
|
416
|
+
total_remaining: int = 0
|
|
417
|
+
for result in tool_results:
|
|
418
|
+
fixed_std = getattr(result, "fixed_issues_count", None)
|
|
419
|
+
remaining_std = getattr(result, "remaining_issues_count", None)
|
|
420
|
+
if fixed_std is not None:
|
|
421
|
+
total_fixed += fixed_std
|
|
422
|
+
else:
|
|
423
|
+
total_fixed += getattr(result, "issues_count", 0)
|
|
424
|
+
|
|
425
|
+
if remaining_std is not None:
|
|
426
|
+
total_remaining += remaining_std
|
|
427
|
+
else:
|
|
428
|
+
# Fallback to parsing when standardized remaining isn't provided
|
|
429
|
+
output = getattr(result, "output", "")
|
|
430
|
+
if output and (
|
|
431
|
+
"remaining" in output.lower()
|
|
432
|
+
or "cannot be auto-fixed" in output.lower()
|
|
433
|
+
):
|
|
434
|
+
remaining_match = RE_CANNOT_AUTOFIX.search(output)
|
|
435
|
+
if not remaining_match:
|
|
436
|
+
remaining_match = RE_REMAINING_OR_CANNOT.search(
|
|
437
|
+
output.lower(),
|
|
438
|
+
)
|
|
439
|
+
if remaining_match:
|
|
440
|
+
total_remaining += int(remaining_match.group(1))
|
|
441
|
+
elif not getattr(result, "success", True):
|
|
442
|
+
total_remaining += DEFAULT_REMAINING_COUNT
|
|
443
|
+
|
|
444
|
+
# Show ASCII art as the last item; no status text after art
|
|
445
|
+
self._print_ascii_art_format(total_remaining=total_remaining)
|
|
446
|
+
logger.debug(
|
|
447
|
+
f"{action} completed with {total_fixed} fixed, "
|
|
448
|
+
f"{total_remaining} remaining",
|
|
449
|
+
)
|
|
450
|
+
else:
|
|
451
|
+
# For check commands, use total issues
|
|
452
|
+
total_issues: int = sum(
|
|
453
|
+
(getattr(result, "issues_count", 0) for result in tool_results),
|
|
454
|
+
)
|
|
455
|
+
# Show ASCII art as the last item; no status text after art
|
|
456
|
+
self._print_ascii_art(total_issues=total_issues)
|
|
457
|
+
logger.debug(f"{action} completed with {total_issues} total issues")
|
|
458
|
+
|
|
459
|
+
def _print_summary_table(
|
|
460
|
+
self,
|
|
461
|
+
action: str,
|
|
462
|
+
tool_results: list[object],
|
|
463
|
+
) -> None:
|
|
464
|
+
"""Print the summary table for the run.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
action: str: The action being performed.
|
|
468
|
+
tool_results: list[object]: The list of tool results.
|
|
469
|
+
"""
|
|
470
|
+
try:
|
|
471
|
+
from tabulate import tabulate
|
|
472
|
+
|
|
473
|
+
summary_data: list[list[str]] = []
|
|
474
|
+
for result in tool_results:
|
|
475
|
+
tool_name: str = getattr(result, "name", "unknown")
|
|
476
|
+
issues_count: int = getattr(result, "issues_count", 0)
|
|
477
|
+
success: bool = getattr(result, "success", True)
|
|
478
|
+
|
|
479
|
+
emoji: str = get_tool_emoji(tool_name)
|
|
480
|
+
tool_display: str = f"{emoji} {tool_name}"
|
|
481
|
+
|
|
482
|
+
# For format operations, success means tool ran
|
|
483
|
+
# (regardless of fixes made)
|
|
484
|
+
# For check operations, success means no issues found
|
|
485
|
+
if action == "fmt":
|
|
486
|
+
# Format operations: show fixed count and remaining status
|
|
487
|
+
if success:
|
|
488
|
+
status_display: str = click.style(
|
|
489
|
+
"✅ PASS", fg="green", bold=True
|
|
490
|
+
)
|
|
491
|
+
else:
|
|
492
|
+
status_display = click.style("❌ FAIL", fg="red", bold=True)
|
|
493
|
+
|
|
494
|
+
# Check if files were excluded
|
|
495
|
+
result_output: str = getattr(result, "output", "")
|
|
496
|
+
if result_output and any(
|
|
497
|
+
(
|
|
498
|
+
msg in result_output
|
|
499
|
+
for msg in ["No files to", "No Python files found to"]
|
|
500
|
+
),
|
|
501
|
+
):
|
|
502
|
+
fixed_display: str = click.style(
|
|
503
|
+
"SKIPPED", fg="yellow", bold=True
|
|
504
|
+
)
|
|
505
|
+
remaining_display: str = click.style(
|
|
506
|
+
"SKIPPED",
|
|
507
|
+
fg="yellow",
|
|
508
|
+
bold=True,
|
|
509
|
+
)
|
|
510
|
+
else:
|
|
511
|
+
# Prefer standardized counts from ToolResult
|
|
512
|
+
remaining_std = getattr(result, "remaining_issues_count", None)
|
|
513
|
+
fixed_std = getattr(result, "fixed_issues_count", None)
|
|
514
|
+
|
|
515
|
+
if remaining_std is not None:
|
|
516
|
+
remaining_count: int = int(remaining_std)
|
|
517
|
+
else:
|
|
518
|
+
# Parse output to determine remaining issues
|
|
519
|
+
remaining_count = 0
|
|
520
|
+
if result_output and (
|
|
521
|
+
"remaining" in result_output.lower()
|
|
522
|
+
or "cannot be auto-fixed" in result_output.lower()
|
|
523
|
+
):
|
|
524
|
+
# Try multiple patterns to match different
|
|
525
|
+
# output formats
|
|
526
|
+
remaining_match = RE_CANNOT_AUTOFIX.search(
|
|
527
|
+
result_output,
|
|
528
|
+
)
|
|
529
|
+
if not remaining_match:
|
|
530
|
+
remaining_match = RE_REMAINING_OR_CANNOT.search(
|
|
531
|
+
result_output.lower(),
|
|
532
|
+
)
|
|
533
|
+
if remaining_match:
|
|
534
|
+
remaining_count = int(remaining_match.group(1))
|
|
535
|
+
elif not success:
|
|
536
|
+
remaining_count = DEFAULT_REMAINING_COUNT
|
|
537
|
+
|
|
538
|
+
if fixed_std is not None:
|
|
539
|
+
fixed_display_value = int(fixed_std)
|
|
540
|
+
else:
|
|
541
|
+
# Fall back to issues_count when fixed is unknown
|
|
542
|
+
fixed_display_value = int(issues_count)
|
|
543
|
+
|
|
544
|
+
# Fixed issues display
|
|
545
|
+
fixed_display = click.style(
|
|
546
|
+
str(fixed_display_value),
|
|
547
|
+
fg="green",
|
|
548
|
+
bold=True,
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# Remaining issues display
|
|
552
|
+
remaining_display = click.style(
|
|
553
|
+
str(remaining_count),
|
|
554
|
+
fg="red" if remaining_count > 0 else "green",
|
|
555
|
+
bold=True,
|
|
556
|
+
)
|
|
557
|
+
else: # check
|
|
558
|
+
status_display = (
|
|
559
|
+
click.style("✅ PASS", fg="green", bold=True)
|
|
560
|
+
if issues_count == 0
|
|
561
|
+
else click.style("❌ FAIL", fg="red", bold=True)
|
|
562
|
+
)
|
|
563
|
+
# Check if files were excluded
|
|
564
|
+
result_output = getattr(result, "output", "")
|
|
565
|
+
if result_output and any(
|
|
566
|
+
(
|
|
567
|
+
msg in result_output
|
|
568
|
+
for msg in ["No files to", "No Python files found to"]
|
|
569
|
+
),
|
|
570
|
+
):
|
|
571
|
+
issues_display: str = click.style(
|
|
572
|
+
"SKIPPED", fg="yellow", bold=True
|
|
573
|
+
)
|
|
574
|
+
else:
|
|
575
|
+
issues_display = click.style(
|
|
576
|
+
str(issues_count),
|
|
577
|
+
fg="green" if issues_count == 0 else "red",
|
|
578
|
+
bold=True,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
if action == "fmt":
|
|
582
|
+
summary_data.append(
|
|
583
|
+
[
|
|
584
|
+
tool_display,
|
|
585
|
+
status_display,
|
|
586
|
+
fixed_display,
|
|
587
|
+
remaining_display,
|
|
588
|
+
],
|
|
589
|
+
)
|
|
590
|
+
else:
|
|
591
|
+
summary_data.append([tool_display, status_display, issues_display])
|
|
592
|
+
|
|
593
|
+
# Set headers based on action
|
|
594
|
+
# Use plain headers to avoid ANSI/emojis width misalignment
|
|
595
|
+
headers: list[str]
|
|
596
|
+
if action == "fmt":
|
|
597
|
+
headers = ["Tool", "Status", "Fixed", "Remaining"]
|
|
598
|
+
else:
|
|
599
|
+
headers = ["Tool", "Status", "Issues"]
|
|
600
|
+
|
|
601
|
+
# Render with plain values to ensure proper alignment across terminals
|
|
602
|
+
table: str = tabulate(
|
|
603
|
+
tabular_data=summary_data,
|
|
604
|
+
headers=headers,
|
|
605
|
+
tablefmt="grid",
|
|
606
|
+
stralign="left",
|
|
607
|
+
disable_numparse=True,
|
|
608
|
+
)
|
|
609
|
+
self.console_output(text=table)
|
|
610
|
+
self.console_output(text="")
|
|
611
|
+
|
|
612
|
+
except ImportError:
|
|
613
|
+
# Fallback if tabulate not available
|
|
614
|
+
self.console_output(text="Summary table requires tabulate package")
|
|
615
|
+
logger.warning("tabulate not available for summary table")
|
|
616
|
+
|
|
617
|
+
def _print_final_status(
|
|
618
|
+
self,
|
|
619
|
+
action: str,
|
|
620
|
+
total_issues: int,
|
|
621
|
+
) -> None:
|
|
622
|
+
"""Print the final status for the run.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
action: str: The action being performed.
|
|
626
|
+
total_issues: int: The total number of issues found.
|
|
627
|
+
"""
|
|
628
|
+
if action == "fmt":
|
|
629
|
+
# Format operations: show success regardless of fixes made
|
|
630
|
+
if total_issues == 0:
|
|
631
|
+
final_msg: str = "✓ No issues found."
|
|
632
|
+
else:
|
|
633
|
+
final_msg = f"✓ Fixed {total_issues} issues."
|
|
634
|
+
self.console_output(text=click.style(final_msg, fg="green", bold=True))
|
|
635
|
+
else: # check
|
|
636
|
+
# Check operations: show failure if issues found
|
|
637
|
+
if total_issues == 0:
|
|
638
|
+
final_msg = "✓ No issues found."
|
|
639
|
+
self.console_output(text=click.style(final_msg, fg="green", bold=True))
|
|
640
|
+
else:
|
|
641
|
+
final_msg = f"✗ Found {total_issues} issues"
|
|
642
|
+
self.console_output(text=click.style(final_msg, fg="red", bold=True))
|
|
643
|
+
|
|
644
|
+
self.console_output(text="")
|
|
645
|
+
|
|
646
|
+
def _print_final_status_format(
|
|
647
|
+
self,
|
|
648
|
+
total_fixed: int,
|
|
649
|
+
total_remaining: int,
|
|
650
|
+
) -> None:
|
|
651
|
+
"""Print the final status for format operations.
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
total_fixed: int: The total number of issues fixed.
|
|
655
|
+
total_remaining: int: The total number of remaining issues.
|
|
656
|
+
"""
|
|
657
|
+
if total_remaining == 0:
|
|
658
|
+
if total_fixed == 0:
|
|
659
|
+
final_msg: str = "✓ No issues found."
|
|
660
|
+
else:
|
|
661
|
+
final_msg = f"✓ {total_fixed} fixed"
|
|
662
|
+
self.console_output(text=click.style(final_msg, fg="green", bold=True))
|
|
663
|
+
else:
|
|
664
|
+
if total_fixed > 0:
|
|
665
|
+
fixed_msg: str = f"✓ {total_fixed} fixed"
|
|
666
|
+
self.console_output(text=click.style(fixed_msg, fg="green", bold=True))
|
|
667
|
+
remaining_msg: str = f"✗ {total_remaining} remaining"
|
|
668
|
+
self.console_output(text=click.style(remaining_msg, fg="red", bold=True))
|
|
669
|
+
|
|
670
|
+
self.console_output(text="")
|
|
671
|
+
|
|
672
|
+
def _print_ascii_art_format(
|
|
673
|
+
self,
|
|
674
|
+
total_remaining: int,
|
|
675
|
+
) -> None:
|
|
676
|
+
"""Print ASCII art for format operations based on remaining issues.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
total_remaining: int: The total number of remaining issues.
|
|
680
|
+
"""
|
|
681
|
+
try:
|
|
682
|
+
if total_remaining == 0:
|
|
683
|
+
ascii_art = read_ascii_art(filename="success.txt")
|
|
684
|
+
else:
|
|
685
|
+
ascii_art = read_ascii_art(filename="fail.txt")
|
|
686
|
+
|
|
687
|
+
if ascii_art:
|
|
688
|
+
art_text: str = "\n".join(ascii_art)
|
|
689
|
+
self.console_output(text=art_text)
|
|
690
|
+
except Exception as e:
|
|
691
|
+
logger.debug(f"Could not load ASCII art: {e}")
|
|
692
|
+
|
|
693
|
+
def _print_ascii_art(
|
|
694
|
+
self,
|
|
695
|
+
total_issues: int,
|
|
696
|
+
) -> None:
|
|
697
|
+
"""Print ASCII art based on the number of issues.
|
|
698
|
+
|
|
699
|
+
Args:
|
|
700
|
+
total_issues: int: The total number of issues found.
|
|
701
|
+
"""
|
|
702
|
+
try:
|
|
703
|
+
if total_issues == 0:
|
|
704
|
+
ascii_art = read_ascii_art(filename="success.txt")
|
|
705
|
+
else:
|
|
706
|
+
ascii_art = read_ascii_art(filename="fail.txt")
|
|
707
|
+
|
|
708
|
+
if ascii_art:
|
|
709
|
+
art_text: str = "\n".join(ascii_art)
|
|
710
|
+
self.console_output(text=art_text)
|
|
711
|
+
except Exception as e:
|
|
712
|
+
logger.debug(f"Could not load ASCII art: {e}")
|
|
713
|
+
|
|
714
|
+
def print_verbose_info(
|
|
715
|
+
self,
|
|
716
|
+
action: str,
|
|
717
|
+
tools_list: str,
|
|
718
|
+
paths_list: str,
|
|
719
|
+
output_format: str,
|
|
720
|
+
) -> None:
|
|
721
|
+
"""Print verbose information about the run.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
action: str: The action being performed.
|
|
725
|
+
tools_list: str: The list of tools being run.
|
|
726
|
+
paths_list: str: The list of paths being checked/formatted.
|
|
727
|
+
output_format: str: The output format being used.
|
|
728
|
+
"""
|
|
729
|
+
if not self.verbose:
|
|
730
|
+
return
|
|
731
|
+
|
|
732
|
+
info_border: str = "=" * INFO_BORDER_LENGTH
|
|
733
|
+
info_title: str = (
|
|
734
|
+
"🔧 Format Configuration" if action == "fmt" else "🔍 Check Configuration"
|
|
735
|
+
)
|
|
736
|
+
info_emojis: str = ("🔧 " if action == "fmt" else "🔍 ") * 5
|
|
737
|
+
|
|
738
|
+
self.console_output(text=info_border)
|
|
739
|
+
self.console_output(text=f"{info_title} {info_emojis}")
|
|
740
|
+
self.console_output(text=info_border)
|
|
741
|
+
self.console_output(text="")
|
|
742
|
+
|
|
743
|
+
self.console_output(text=f"🔧 Running tools: {tools_list}")
|
|
744
|
+
self.console_output(
|
|
745
|
+
text=(
|
|
746
|
+
f"📁 {'Formatting' if action == 'fmt' else 'Checking'} "
|
|
747
|
+
f"paths: {paths_list}"
|
|
748
|
+
),
|
|
749
|
+
)
|
|
750
|
+
self.console_output(text=f"📊 Output format: {output_format}")
|
|
751
|
+
self.console_output(text="")
|
|
752
|
+
|
|
753
|
+
def save_console_log(
|
|
754
|
+
self,
|
|
755
|
+
) -> None:
|
|
756
|
+
"""Save tracked console messages to console.log."""
|
|
757
|
+
console_log_path: Path = self.run_dir / "console.log"
|
|
758
|
+
with open(console_log_path, "w", encoding="utf-8") as f:
|
|
759
|
+
for message in self.console_messages:
|
|
760
|
+
f.write(f"{message}\n")
|
|
761
|
+
logger.debug(f"Saved console output to {console_log_path}")
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def create_logger(
|
|
765
|
+
run_dir: Path,
|
|
766
|
+
verbose: bool = False,
|
|
767
|
+
raw_output: bool = False,
|
|
768
|
+
) -> SimpleLintroLogger:
|
|
769
|
+
"""Create a SimpleLintroLogger instance.
|
|
770
|
+
|
|
771
|
+
Args:
|
|
772
|
+
run_dir: Path: Directory for log files.
|
|
773
|
+
verbose: bool: Whether to enable verbose logging.
|
|
774
|
+
raw_output: bool: Whether to show raw tool output instead of formatted output.
|
|
775
|
+
|
|
776
|
+
Returns:
|
|
777
|
+
SimpleLintroLogger: Configured SimpleLintroLogger instance.
|
|
778
|
+
"""
|
|
779
|
+
return SimpleLintroLogger(
|
|
780
|
+
run_dir=run_dir,
|
|
781
|
+
verbose=verbose,
|
|
782
|
+
raw_output=raw_output,
|
|
783
|
+
)
|