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.

Files changed (85) hide show
  1. lintro/__init__.py +3 -0
  2. lintro/__main__.py +6 -0
  3. lintro/ascii-art/fail.txt +404 -0
  4. lintro/ascii-art/success.txt +484 -0
  5. lintro/cli.py +70 -0
  6. lintro/cli_utils/__init__.py +7 -0
  7. lintro/cli_utils/commands/__init__.py +7 -0
  8. lintro/cli_utils/commands/check.py +210 -0
  9. lintro/cli_utils/commands/format.py +167 -0
  10. lintro/cli_utils/commands/list_tools.py +114 -0
  11. lintro/enums/__init__.py +0 -0
  12. lintro/enums/action.py +29 -0
  13. lintro/enums/darglint_strictness.py +22 -0
  14. lintro/enums/group_by.py +31 -0
  15. lintro/enums/hadolint_enums.py +46 -0
  16. lintro/enums/output_format.py +40 -0
  17. lintro/enums/tool_name.py +36 -0
  18. lintro/enums/tool_type.py +27 -0
  19. lintro/enums/yamllint_format.py +22 -0
  20. lintro/exceptions/__init__.py +0 -0
  21. lintro/exceptions/errors.py +15 -0
  22. lintro/formatters/__init__.py +0 -0
  23. lintro/formatters/core/__init__.py +0 -0
  24. lintro/formatters/core/output_style.py +21 -0
  25. lintro/formatters/core/table_descriptor.py +24 -0
  26. lintro/formatters/styles/__init__.py +17 -0
  27. lintro/formatters/styles/csv.py +41 -0
  28. lintro/formatters/styles/grid.py +91 -0
  29. lintro/formatters/styles/html.py +48 -0
  30. lintro/formatters/styles/json.py +61 -0
  31. lintro/formatters/styles/markdown.py +41 -0
  32. lintro/formatters/styles/plain.py +39 -0
  33. lintro/formatters/tools/__init__.py +35 -0
  34. lintro/formatters/tools/darglint_formatter.py +72 -0
  35. lintro/formatters/tools/hadolint_formatter.py +84 -0
  36. lintro/formatters/tools/prettier_formatter.py +76 -0
  37. lintro/formatters/tools/ruff_formatter.py +116 -0
  38. lintro/formatters/tools/yamllint_formatter.py +87 -0
  39. lintro/models/__init__.py +0 -0
  40. lintro/models/core/__init__.py +0 -0
  41. lintro/models/core/tool.py +104 -0
  42. lintro/models/core/tool_config.py +23 -0
  43. lintro/models/core/tool_result.py +39 -0
  44. lintro/parsers/__init__.py +0 -0
  45. lintro/parsers/darglint/__init__.py +0 -0
  46. lintro/parsers/darglint/darglint_issue.py +9 -0
  47. lintro/parsers/darglint/darglint_parser.py +62 -0
  48. lintro/parsers/hadolint/__init__.py +1 -0
  49. lintro/parsers/hadolint/hadolint_issue.py +24 -0
  50. lintro/parsers/hadolint/hadolint_parser.py +65 -0
  51. lintro/parsers/prettier/__init__.py +0 -0
  52. lintro/parsers/prettier/prettier_issue.py +10 -0
  53. lintro/parsers/prettier/prettier_parser.py +60 -0
  54. lintro/parsers/ruff/__init__.py +1 -0
  55. lintro/parsers/ruff/ruff_issue.py +43 -0
  56. lintro/parsers/ruff/ruff_parser.py +89 -0
  57. lintro/parsers/yamllint/__init__.py +0 -0
  58. lintro/parsers/yamllint/yamllint_issue.py +24 -0
  59. lintro/parsers/yamllint/yamllint_parser.py +68 -0
  60. lintro/tools/__init__.py +40 -0
  61. lintro/tools/core/__init__.py +0 -0
  62. lintro/tools/core/tool_base.py +320 -0
  63. lintro/tools/core/tool_manager.py +167 -0
  64. lintro/tools/implementations/__init__.py +0 -0
  65. lintro/tools/implementations/tool_darglint.py +245 -0
  66. lintro/tools/implementations/tool_hadolint.py +302 -0
  67. lintro/tools/implementations/tool_prettier.py +270 -0
  68. lintro/tools/implementations/tool_ruff.py +618 -0
  69. lintro/tools/implementations/tool_yamllint.py +240 -0
  70. lintro/tools/tool_enum.py +17 -0
  71. lintro/utils/__init__.py +0 -0
  72. lintro/utils/ascii_normalize_cli.py +84 -0
  73. lintro/utils/config.py +39 -0
  74. lintro/utils/console_logger.py +783 -0
  75. lintro/utils/formatting.py +173 -0
  76. lintro/utils/output_manager.py +301 -0
  77. lintro/utils/path_utils.py +41 -0
  78. lintro/utils/tool_executor.py +443 -0
  79. lintro/utils/tool_utils.py +431 -0
  80. lintro-0.3.2.dist-info/METADATA +338 -0
  81. lintro-0.3.2.dist-info/RECORD +85 -0
  82. lintro-0.3.2.dist-info/WHEEL +5 -0
  83. lintro-0.3.2.dist-info/entry_points.txt +2 -0
  84. lintro-0.3.2.dist-info/licenses/LICENSE +21 -0
  85. lintro-0.3.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,443 @@
1
+ """Simplified runner for lintro commands.
2
+
3
+ Clean, straightforward approach using Loguru with rich formatting:
4
+ 1. OutputManager - handles structured output files only
5
+ 2. SimpleLintroLogger - handles console display AND logging with Loguru + rich
6
+ formatting
7
+ 3. No tee, no stream redirection, no complex state management
8
+ """
9
+
10
+ from lintro.enums.group_by import GroupBy, normalize_group_by
11
+ from lintro.enums.output_format import OutputFormat, normalize_output_format
12
+ from lintro.tools import tool_manager
13
+ from lintro.tools.tool_enum import ToolEnum
14
+ from lintro.utils.config import load_lintro_tool_config
15
+ from lintro.utils.console_logger import create_logger
16
+ from lintro.utils.output_manager import OutputManager
17
+ from lintro.utils.tool_utils import format_tool_output
18
+
19
+ # Constants
20
+ DEFAULT_EXIT_CODE_SUCCESS: int = 0
21
+ DEFAULT_EXIT_CODE_FAILURE: int = 1
22
+ DEFAULT_REMAINING_COUNT: int = 1
23
+
24
+
25
+ def _get_tools_to_run(
26
+ tools: str | None,
27
+ action: str,
28
+ ) -> list[ToolEnum]:
29
+ """Get the list of tools to run based on the tools string and action.
30
+
31
+ Args:
32
+ tools: str | None: Comma-separated tool names, "all", or None.
33
+ action: str: "check" or "fmt".
34
+
35
+ Returns:
36
+ list[ToolEnum]: List of ToolEnum instances to run.
37
+
38
+ Raises:
39
+ ValueError: If unknown tool names are provided.
40
+ """
41
+ if tools == "all" or tools is None:
42
+ # Get all available tools for the action
43
+ if action == "fmt":
44
+ available_tools = tool_manager.get_fix_tools()
45
+ else: # check
46
+ available_tools = tool_manager.get_check_tools()
47
+ return list(available_tools.keys())
48
+
49
+ # Parse specific tools
50
+ tool_names: list[str] = [name.strip().upper() for name in tools.split(",")]
51
+ tools_to_run: list[ToolEnum] = []
52
+
53
+ for name in tool_names:
54
+ try:
55
+ tool_enum = ToolEnum[name]
56
+ # Verify the tool supports the requested action
57
+ if action == "fmt":
58
+ tool_instance = tool_manager.get_tool(tool_enum)
59
+ if not tool_instance.can_fix:
60
+ raise ValueError(
61
+ f"Tool '{name.lower()}' does not support formatting",
62
+ )
63
+ tools_to_run.append(tool_enum)
64
+ except KeyError:
65
+ available_names: list[str] = [e.name.lower() for e in ToolEnum]
66
+ raise ValueError(
67
+ f"Unknown tool '{name.lower()}'. Available tools: {available_names}",
68
+ )
69
+
70
+ return tools_to_run
71
+
72
+
73
+ def _coerce_value(raw: str) -> object:
74
+ """Coerce a raw CLI value into a typed Python value.
75
+
76
+ Rules:
77
+ - "all"/"none" (case-insensitive) -> list[str]
78
+ - "True"/"False" (case-insensitive) -> bool
79
+ - "None"/"null" (case-insensitive) -> None
80
+ - integer (e.g., 88) -> int
81
+ - float (e.g., 0.75) -> float
82
+ - list via pipe-delimited values (e.g., "E|F|W") -> list[str]
83
+ Pipe is chosen to avoid conflict with the top-level comma separator.
84
+ - otherwise -> original string
85
+
86
+ Args:
87
+ raw: str: Raw CLI value to coerce.
88
+
89
+ Returns:
90
+ object: Coerced value.
91
+ """
92
+ s = raw.strip()
93
+ # Lists via pipe (e.g., select=E|F)
94
+ if "|" in s:
95
+ return [part.strip() for part in s.split("|") if part.strip()]
96
+
97
+ low = s.lower()
98
+ if low == "true":
99
+ return True
100
+ if low == "false":
101
+ return False
102
+ if low in {"none", "null"}:
103
+ return None
104
+
105
+ # Try int
106
+ try:
107
+ return int(s)
108
+ except ValueError:
109
+ pass
110
+
111
+ # Try float
112
+ try:
113
+ return float(s)
114
+ except ValueError:
115
+ pass
116
+
117
+ return s
118
+
119
+
120
+ def _parse_tool_options(tool_options: str | None) -> dict[str, dict[str, object]]:
121
+ """Parse tool options string into a typed dictionary.
122
+
123
+ Args:
124
+ tool_options: str | None: String in format
125
+ "tool:option=value,tool2:option=value". Multiple values for a single
126
+ option can be provided using pipe separators (e.g., select=E|F).
127
+
128
+ Returns:
129
+ dict[str, dict[str, object]]: Mapping tool names to typed options.
130
+ """
131
+ if not tool_options:
132
+ return {}
133
+
134
+ tool_option_dict: dict[str, dict[str, object]] = {}
135
+ for opt in tool_options.split(","):
136
+ opt = opt.strip()
137
+ if not opt:
138
+ continue
139
+ if ":" not in opt:
140
+ # Skip malformed fragment
141
+ continue
142
+ tool_name, tool_opt = opt.split(":", 1)
143
+ if "=" not in tool_opt:
144
+ # Skip malformed fragment
145
+ continue
146
+ opt_name, opt_value = tool_opt.split("=", 1)
147
+ tool_name = tool_name.strip()
148
+ opt_name = opt_name.strip()
149
+ opt_value = opt_value.strip()
150
+ if not tool_name or not opt_name:
151
+ continue
152
+ if tool_name not in tool_option_dict:
153
+ tool_option_dict[tool_name] = {}
154
+ tool_option_dict[tool_name][opt_name] = _coerce_value(opt_value)
155
+
156
+ return tool_option_dict
157
+
158
+
159
+ def run_lint_tools_simple(
160
+ *,
161
+ action: str,
162
+ paths: list[str],
163
+ tools: str | None,
164
+ tool_options: str | None,
165
+ exclude: str | None,
166
+ include_venv: bool,
167
+ group_by: str,
168
+ output_format: str,
169
+ verbose: bool,
170
+ raw_output: bool = False,
171
+ ) -> int:
172
+ """Simplified runner using Loguru-based logging with rich formatting.
173
+
174
+ Clean approach with beautiful output:
175
+ - SimpleLintroLogger handles ALL console output and file logging with rich
176
+ formatting
177
+ - OutputManager handles structured output files
178
+ - No tee, no complex state management
179
+
180
+ Args:
181
+ action: str: "check" or "fmt".
182
+ paths: list[str]: List of paths to check.
183
+ tools: str | None: Comma-separated list of tools to run.
184
+ tool_options: str | None: Additional tool options.
185
+ exclude: str | None: Patterns to exclude.
186
+ include_venv: bool: Whether to include virtual environments.
187
+ group_by: str: How to group results.
188
+ output_format: str: Output format for results.
189
+ verbose: bool: Whether to enable verbose output.
190
+ raw_output: bool: Whether to show raw tool output instead of formatted output.
191
+
192
+ Returns:
193
+ int: Exit code (0 for success, 1 for failures).
194
+ """
195
+ # Initialize output manager for this run
196
+ output_manager = OutputManager()
197
+ run_dir: str = output_manager.run_dir
198
+
199
+ # Create simplified logger with rich formatting
200
+ logger = create_logger(run_dir=run_dir, verbose=verbose, raw_output=raw_output)
201
+
202
+ logger.debug(f"Starting {action} command")
203
+ logger.debug(f"Paths: {paths}")
204
+ logger.debug(f"Tools: {tools}")
205
+ logger.debug(f"Run directory: {run_dir}")
206
+
207
+ # For JSON output format, we'll collect results and output JSON at the end
208
+ # Normalize enums while maintaining backward compatibility
209
+ output_fmt_enum: OutputFormat = normalize_output_format(output_format)
210
+ group_by_enum: GroupBy = normalize_group_by(group_by)
211
+ json_output_mode = output_fmt_enum == OutputFormat.JSON
212
+
213
+ try:
214
+ # Get tools to run
215
+ try:
216
+ tools_to_run = _get_tools_to_run(tools=tools, action=action)
217
+ except ValueError as e:
218
+ logger.error(str(e))
219
+ logger.save_console_log()
220
+ return DEFAULT_EXIT_CODE_FAILURE
221
+
222
+ if not tools_to_run:
223
+ logger.warning("No tools found to run")
224
+ logger.save_console_log()
225
+ return DEFAULT_EXIT_CODE_FAILURE
226
+
227
+ # Parse tool options
228
+ tool_option_dict = _parse_tool_options(tool_options)
229
+
230
+ # Print main header (skip for JSON mode)
231
+ tools_list: str = ", ".join(t.name.lower() for t in tools_to_run)
232
+ if not json_output_mode:
233
+ logger.print_lintro_header(
234
+ action=action,
235
+ tool_count=len(tools_to_run),
236
+ tools_list=tools_list,
237
+ )
238
+
239
+ # Print verbose info if requested
240
+ paths_list: str = ", ".join(paths)
241
+ logger.print_verbose_info(
242
+ action=action,
243
+ tools_list=tools_list,
244
+ paths_list=paths_list,
245
+ output_format=output_format,
246
+ )
247
+
248
+ all_results: list = []
249
+ total_issues: int = 0
250
+ total_fixed: int = 0
251
+ total_remaining: int = 0
252
+
253
+ # Run each tool with rich formatting
254
+ for tool_enum in tools_to_run:
255
+ tool = tool_manager.get_tool(tool_enum)
256
+ tool_name: str = tool_enum.name.lower()
257
+
258
+ # Print rich tool header (skip for JSON mode)
259
+ if not json_output_mode:
260
+ logger.print_tool_header(tool_name=tool_name, action=action)
261
+
262
+ try:
263
+ # Configure tool options
264
+ # 1) Load config from pyproject.toml / lintro.toml
265
+ cfg: dict = load_lintro_tool_config(tool_name)
266
+ if cfg:
267
+ try:
268
+ tool.set_options(**cfg)
269
+ except Exception:
270
+ pass
271
+ # 2) CLI --tool-options overrides config file
272
+ if tool_name in tool_option_dict:
273
+ tool.set_options(**tool_option_dict[tool_name])
274
+
275
+ # Set common options
276
+ if exclude:
277
+ exclude_patterns: list[str] = [
278
+ pattern.strip() for pattern in exclude.split(",")
279
+ ]
280
+ tool.set_options(exclude_patterns=exclude_patterns)
281
+
282
+ tool.set_options(include_venv=include_venv)
283
+
284
+ # Run the tool
285
+ logger.debug(f"Executing {tool_name}")
286
+
287
+ if action == "fmt":
288
+ # Respect tool defaults; allow overrides via --tool-options
289
+ result = tool.fix(paths=paths)
290
+ # Prefer standardized counters when present
291
+ fixed_count: int = (
292
+ getattr(result, "fixed_issues_count", None)
293
+ if hasattr(result, "fixed_issues_count")
294
+ else None
295
+ )
296
+ if fixed_count is None:
297
+ fixed_count = 0
298
+ total_fixed += fixed_count
299
+
300
+ remaining_count: int = (
301
+ getattr(result, "remaining_issues_count", None)
302
+ if hasattr(result, "remaining_issues_count")
303
+ else None
304
+ )
305
+ if remaining_count is None:
306
+ # Fallback to issues_count if standardized field absent
307
+ remaining_count = getattr(result, "issues_count", 0)
308
+ total_remaining += max(0, remaining_count)
309
+
310
+ # For display in per-tool logger call below
311
+ issues_count: int = remaining_count
312
+ else: # check
313
+ result = tool.check(paths=paths)
314
+ issues_count = getattr(result, "issues_count", 0)
315
+ total_issues += issues_count
316
+
317
+ # Format and display output
318
+ output = getattr(result, "output", None)
319
+ issues = getattr(result, "issues", None)
320
+ formatted_output: str = ""
321
+
322
+ # Call format_tool_output if we have output or issues
323
+ if (output and output.strip()) or issues:
324
+ formatted_output = format_tool_output(
325
+ tool_name=tool_name,
326
+ output=output or "",
327
+ group_by=group_by_enum.value,
328
+ output_format=output_fmt_enum.value,
329
+ issues=issues,
330
+ )
331
+
332
+ # Print tool results with rich formatting (skip for JSON mode)
333
+ if not json_output_mode:
334
+ # Use raw output if raw_output is true, otherwise use
335
+ # formatted output
336
+ if raw_output:
337
+ display_output = output
338
+ else:
339
+ display_output = formatted_output
340
+ logger.print_tool_result(
341
+ tool_name=tool_name,
342
+ output=display_output,
343
+ issues_count=issues_count,
344
+ raw_output_for_meta=output,
345
+ action=action,
346
+ )
347
+
348
+ # Store result
349
+ all_results.append(result)
350
+
351
+ if action == "fmt":
352
+ # Pull standardized counts again for debug log
353
+ fixed_dbg = getattr(result, "fixed_issues_count", fixed_count)
354
+ remaining_dbg = getattr(
355
+ result, "remaining_issues_count", issues_count
356
+ )
357
+ logger.debug(
358
+ f"Completed {tool_name}: {fixed_dbg} fixed, "
359
+ f"{remaining_dbg} remaining"
360
+ )
361
+ else:
362
+ logger.debug(f"Completed {tool_name}: {issues_count} issues found")
363
+
364
+ except Exception as e:
365
+ logger.error(f"Error running {tool_name}: {e}")
366
+ return DEFAULT_EXIT_CODE_FAILURE
367
+
368
+ # Handle output based on format
369
+ if json_output_mode:
370
+ # For JSON output, print JSON directly to stdout
371
+ import datetime
372
+ import json
373
+
374
+ # Create a simple JSON structure with all results
375
+ json_data = {
376
+ "action": action,
377
+ "timestamp": datetime.datetime.now().isoformat(),
378
+ "tools": [result.name for result in all_results],
379
+ "total_issues": sum(
380
+ getattr(result, "issues_count", 0) for result in all_results
381
+ ),
382
+ "total_fixed": (
383
+ sum((getattr(r, "fixed_issues_count", 0) or 0) for r in all_results)
384
+ if action == "fmt"
385
+ else None
386
+ ),
387
+ "total_remaining": (
388
+ sum(
389
+ (getattr(r, "remaining_issues_count", 0) or 0)
390
+ for r in all_results
391
+ )
392
+ if action == "fmt"
393
+ else None
394
+ ),
395
+ "results": [],
396
+ }
397
+
398
+ for result in all_results:
399
+ result_data = {
400
+ "tool": result.name,
401
+ "success": getattr(result, "success", True),
402
+ "issues_count": getattr(result, "issues_count", 0),
403
+ "output": getattr(result, "output", ""),
404
+ "initial_issues_count": getattr(
405
+ result, "initial_issues_count", None
406
+ ),
407
+ "fixed_issues_count": getattr(result, "fixed_issues_count", None),
408
+ "remaining_issues_count": getattr(
409
+ result, "remaining_issues_count", None
410
+ ),
411
+ }
412
+ json_data["results"].append(result_data)
413
+
414
+ print(json.dumps(json_data, indent=2))
415
+ else:
416
+ # Print rich execution summary with table and ASCII art
417
+ logger.print_execution_summary(action=action, tool_results=all_results)
418
+
419
+ # Save outputs
420
+ try:
421
+ output_manager.write_reports_from_results(results=all_results)
422
+ logger.save_console_log()
423
+ logger.debug("Saved all output files")
424
+ except Exception as e:
425
+ logger.error(f"Error saving outputs: {e}")
426
+
427
+ # Return appropriate exit code
428
+ if action == "fmt":
429
+ # Format operations succeed if they complete successfully
430
+ # (even if there are remaining unfixable issues)
431
+ return DEFAULT_EXIT_CODE_SUCCESS
432
+ else: # check
433
+ # Check operations fail if issues are found
434
+ return (
435
+ DEFAULT_EXIT_CODE_SUCCESS
436
+ if total_issues == 0
437
+ else DEFAULT_EXIT_CODE_FAILURE
438
+ )
439
+
440
+ except Exception as e:
441
+ logger.error(f"Unexpected error: {e}")
442
+ logger.save_console_log()
443
+ return DEFAULT_EXIT_CODE_FAILURE