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
@@ -17,10 +17,12 @@ from lintro.utils.formatting import read_ascii_art
17
17
  TOOL_EMOJIS: dict[str, str] = {
18
18
  "ruff": "🦀",
19
19
  "prettier": "💅",
20
+ "eslint": "🔍",
20
21
  "darglint": "📝",
21
22
  "hadolint": "🐳",
22
23
  "yamllint": "📄",
23
24
  "black": "🖤",
25
+ "pytest": "🧪",
24
26
  }
25
27
  DEFAULT_EMOJI: str = "🔧"
26
28
  BORDER_LENGTH: int = 70
@@ -76,20 +78,37 @@ class SimpleLintroLogger:
76
78
  # Configure Loguru
77
79
  self._setup_loguru()
78
80
 
81
+ @staticmethod
82
+ def _get_summary_value(
83
+ summary: dict | object,
84
+ key: str,
85
+ default: int | float = 0,
86
+ ) -> int | float:
87
+ """Extract value from summary dict or object.
88
+
89
+ Args:
90
+ summary: Summary data as dict or dataclass.
91
+ key: Attribute/key name.
92
+ default: Default value if not found.
93
+
94
+ Returns:
95
+ int | float: The extracted value or default.
96
+ """
97
+ if isinstance(summary, dict):
98
+ return summary.get(key, default)
99
+ return getattr(summary, key, default)
100
+
79
101
  def _setup_loguru(self) -> None:
80
102
  """Configure Loguru with clean, simple handlers."""
81
103
  # Remove default handler
82
104
  logger.remove()
83
105
 
84
106
  # Add console handler (for immediate display)
85
- console_level: str = "DEBUG" if self.verbose else "INFO"
107
+ # Only capture WARNING and ERROR for console
86
108
  logger.add(
87
109
  sys.stderr,
88
- level=console_level,
89
- format=(
90
- "<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | "
91
- "{message}"
92
- ),
110
+ level="WARNING", # Only show warnings and errors
111
+ format="{message}", # Simple format without timestamps/log levels
93
112
  colorize=True,
94
113
  )
95
114
 
@@ -115,6 +134,18 @@ class SimpleLintroLogger:
115
134
  self.console_messages.append(message)
116
135
  logger.info(message, **kwargs)
117
136
 
137
+ def info_blue(self, message: str, **kwargs) -> None:
138
+ """Log an info message to the console in blue color.
139
+
140
+ Args:
141
+ message: str: The message to log.
142
+ **kwargs: Additional keyword arguments for formatting.
143
+ """
144
+ styled_message = click.style(message, fg="cyan", bold=True)
145
+ click.echo(styled_message)
146
+ self.console_messages.append(message)
147
+ logger.info(message, **kwargs)
148
+
118
149
  def debug(self, message: str, **kwargs) -> None:
119
150
  """Log debug message.
120
151
 
@@ -270,6 +301,42 @@ class SimpleLintroLogger:
270
301
  the result is treated as a failure even if no issues were
271
302
  counted (e.g., parse or runtime errors).
272
303
  """
304
+ # Add section header for pytest/test results
305
+ if tool_name.lower() == "pytest":
306
+ self.console_output(text="")
307
+ self.console_output(text="🧪 Test Results")
308
+ self.console_output(text="-" * INFO_BORDER_LENGTH)
309
+
310
+ # Display formatted test failures table if present
311
+ # Skip JSON lines but keep tables
312
+ if output and output.strip():
313
+ lines = output.split("\n")
314
+ display_lines = []
315
+ skip_json = False
316
+ for line in lines:
317
+ if line.startswith("{"):
318
+ # Skip JSON summary line
319
+ skip_json = True
320
+ continue
321
+ if skip_json and line.strip() == "":
322
+ # Skip blank line after JSON
323
+ skip_json = False
324
+ continue
325
+ if skip_json:
326
+ # Skip remaining JSON content
327
+ continue
328
+ # Keep everything else including table headers and content
329
+ display_lines.append(line)
330
+
331
+ if display_lines:
332
+ self.console_output(text="\n".join(display_lines))
333
+
334
+ # Don't show summary line here - it will be in the Execution Summary table
335
+ if issues_count == 0 and not output:
336
+ self.success(message="✓ No issues found.")
337
+
338
+ return
339
+
273
340
  if output and output.strip():
274
341
  # Display the output (either raw or formatted, depending on what was passed)
275
342
  self.console_output(text=output)
@@ -441,6 +508,9 @@ class SimpleLintroLogger:
441
508
  action: str: The action being performed ("check" or "fmt").
442
509
  tool_results: list[object]: The list of tool results.
443
510
  """
511
+ # Add separation before Execution Summary
512
+ self.console_output(text="")
513
+
444
514
  # Execution summary section
445
515
  summary_header: str = click.style("📋 EXECUTION SUMMARY", fg="cyan", bold=True)
446
516
  border_line: str = click.style("=" * 50, fg="cyan")
@@ -460,6 +530,8 @@ class SimpleLintroLogger:
460
530
  for result in tool_results:
461
531
  fixed_std = getattr(result, "fixed_issues_count", None)
462
532
  remaining_std = getattr(result, "remaining_issues_count", None)
533
+ success = getattr(result, "success", True)
534
+
463
535
  if fixed_std is not None:
464
536
  total_fixed += fixed_std
465
537
  else:
@@ -467,6 +539,10 @@ class SimpleLintroLogger:
467
539
 
468
540
  if remaining_std is not None:
469
541
  total_remaining += remaining_std
542
+ elif not success:
543
+ # Tool failed - treat as having remaining issues
544
+ # This covers execution errors, config errors, timeouts, etc.
545
+ total_remaining += DEFAULT_REMAINING_COUNT
470
546
  else:
471
547
  # Fallback to parsing when standardized remaining isn't provided
472
548
  output = getattr(result, "output", "")
@@ -481,8 +557,6 @@ class SimpleLintroLogger:
481
557
  )
482
558
  if remaining_match:
483
559
  total_remaining += int(remaining_match.group(1))
484
- elif not getattr(result, "success", True):
485
- total_remaining += DEFAULT_REMAINING_COUNT
486
560
 
487
561
  # Show totals line then ASCII art
488
562
  totals_line: str = (
@@ -537,6 +611,64 @@ class SimpleLintroLogger:
537
611
  emoji: str = get_tool_emoji(tool_name)
538
612
  tool_display: str = f"{emoji} {tool_name}"
539
613
 
614
+ # Special handling for pytest/test action
615
+ if action == "test" and tool_name.lower() == "pytest":
616
+ pytest_summary = getattr(result, "pytest_summary", None)
617
+ if pytest_summary:
618
+ # Use pytest summary data for more detailed display
619
+ passed = int(
620
+ self._get_summary_value(pytest_summary, "passed", 0),
621
+ )
622
+ failed = int(
623
+ self._get_summary_value(pytest_summary, "failed", 0),
624
+ )
625
+ skipped = int(
626
+ self._get_summary_value(pytest_summary, "skipped", 0),
627
+ )
628
+ docker_skipped = int(
629
+ self._get_summary_value(
630
+ pytest_summary,
631
+ "docker_skipped",
632
+ 0,
633
+ ),
634
+ )
635
+ duration = float(
636
+ self._get_summary_value(pytest_summary, "duration", 0.0),
637
+ )
638
+ total = int(
639
+ self._get_summary_value(pytest_summary, "total", 0),
640
+ )
641
+
642
+ # Create detailed status display
643
+ status_display = (
644
+ click.style("✅ PASS", fg="green", bold=True)
645
+ if failed == 0
646
+ else click.style("❌ FAIL", fg="red", bold=True)
647
+ )
648
+
649
+ # Format duration with proper units
650
+ duration_str = f"{duration:.2f}s"
651
+
652
+ # Format skipped count to include docker skipped info
653
+ if docker_skipped > 0:
654
+ skipped_display = f"{skipped} ({docker_skipped} docker)"
655
+ else:
656
+ skipped_display = str(skipped)
657
+
658
+ # Create row with separate columns for each metric
659
+ summary_data.append(
660
+ [
661
+ tool_display,
662
+ status_display,
663
+ str(passed),
664
+ str(failed),
665
+ skipped_display,
666
+ str(total),
667
+ duration_str,
668
+ ],
669
+ )
670
+ continue
671
+
540
672
  # For format operations, success means tool ran
541
673
  # (regardless of fixes made)
542
674
  # For check operations, success means no issues found
@@ -617,30 +749,73 @@ class SimpleLintroLogger:
617
749
  bold=True,
618
750
  )
619
751
  else: # check
620
- status_display = (
621
- click.style("✅ PASS", fg="green", bold=True)
622
- if (success and issues_count == 0)
623
- else click.style("❌ FAIL", fg="red", bold=True)
624
- )
625
- # Check if files were excluded
752
+ # Check if this is an execution failure (timeout/error)
753
+ # vs linting issues
626
754
  result_output = getattr(result, "output", "")
627
- if result_output and any(
628
- (
629
- msg in result_output
630
- for msg in ["No files to", "No Python files found to"]
631
- ),
632
- ):
633
- issues_display: str = click.style(
755
+
756
+ # Check if tool was skipped (version check failure, etc.)
757
+ is_skipped = result_output and "skipping" in result_output.lower()
758
+
759
+ has_execution_failure = result_output and (
760
+ "timeout" in result_output.lower()
761
+ or "error processing" in result_output.lower()
762
+ or "tool execution failed" in result_output.lower()
763
+ )
764
+
765
+ # If tool was skipped, show SKIPPED status
766
+ if is_skipped:
767
+ status_display = click.style(
768
+ "⏭️ SKIPPED",
769
+ fg="yellow",
770
+ bold=True,
771
+ )
772
+ issues_display = click.style(
634
773
  "SKIPPED",
635
774
  fg="yellow",
636
775
  bold=True,
637
776
  )
638
- else:
777
+ # If there are execution failures but no parsed issues,
778
+ # show special status
779
+ elif has_execution_failure and issues_count == 0:
780
+ # This shouldn't happen with our fix, but handle gracefully
781
+ status_display = click.style("❌ FAIL", fg="red", bold=True)
639
782
  issues_display = click.style(
640
- str(issues_count),
641
- fg="green" if issues_count == 0 else "red",
783
+ "ERROR",
784
+ fg="red",
642
785
  bold=True,
643
786
  )
787
+ elif not success and issues_count == 0:
788
+ # Execution failure with no issues parsed - show as failure
789
+ status_display = click.style("❌ FAIL", fg="red", bold=True)
790
+ issues_display = click.style(
791
+ "ERROR",
792
+ fg="red",
793
+ bold=True,
794
+ )
795
+ else:
796
+ status_display = (
797
+ click.style("✅ PASS", fg="green", bold=True)
798
+ if (success and issues_count == 0)
799
+ else click.style("❌ FAIL", fg="red", bold=True)
800
+ )
801
+ # Check if files were excluded
802
+ if result_output and any(
803
+ (
804
+ msg in result_output
805
+ for msg in ["No files to", "No Python files found to"]
806
+ ),
807
+ ):
808
+ issues_display = click.style(
809
+ "SKIPPED",
810
+ fg="yellow",
811
+ bold=True,
812
+ )
813
+ else:
814
+ issues_display = click.style(
815
+ str(issues_count),
816
+ fg="green" if issues_count == 0 else "red",
817
+ bold=True,
818
+ )
644
819
 
645
820
  if action == "fmt":
646
821
  summary_data.append(
@@ -657,7 +832,18 @@ class SimpleLintroLogger:
657
832
  # Set headers based on action
658
833
  # Use plain headers to avoid ANSI/emojis width misalignment
659
834
  headers: list[str]
660
- if action == "fmt":
835
+ if action == "test":
836
+ # Special table for test action with separate columns for test metrics
837
+ headers = [
838
+ "Tool",
839
+ "Status",
840
+ "Passed",
841
+ "Failed",
842
+ "Skipped",
843
+ "Total",
844
+ "Duration",
845
+ ]
846
+ elif action == "fmt":
661
847
  headers = ["Tool", "Status", "Fixed", "Remaining"]
662
848
  else:
663
849
  headers = ["Tool", "Status", "Issues"]
@@ -4,6 +4,48 @@ Small helpers to normalize paths for display consistency.
4
4
  """
5
5
 
6
6
  import os
7
+ from pathlib import Path
8
+
9
+
10
+ def find_lintro_ignore() -> Path | None:
11
+ """Find .lintro-ignore file by searching upward from current directory.
12
+
13
+ Searches upward from the current working directory to find the project root
14
+ by looking for .lintro-ignore or pyproject.toml files.
15
+
16
+ Returns:
17
+ Path | None: Path to .lintro-ignore file if found, None otherwise.
18
+ """
19
+ current_dir = Path.cwd()
20
+ # Limit search to prevent infinite loops (e.g., if we're in /)
21
+ max_depth = 20
22
+ depth = 0
23
+
24
+ while depth < max_depth:
25
+ lintro_ignore_path = current_dir / ".lintro-ignore"
26
+ if lintro_ignore_path.exists():
27
+ return lintro_ignore_path
28
+
29
+ # Also check for pyproject.toml as project root indicator
30
+ pyproject_path = current_dir / "pyproject.toml"
31
+ if pyproject_path.exists():
32
+ # If pyproject.toml exists, check for .lintro-ignore in same directory
33
+ lintro_ignore_path = current_dir / ".lintro-ignore"
34
+ if lintro_ignore_path.exists():
35
+ return lintro_ignore_path
36
+ # Even if .lintro-ignore doesn't exist, we found project root
37
+ # Return None to indicate no .lintro-ignore found
38
+ return None
39
+
40
+ # Move up one directory
41
+ parent_dir = current_dir.parent
42
+ if parent_dir == current_dir:
43
+ # Reached filesystem root
44
+ break
45
+ current_dir = parent_dir
46
+ depth += 1
47
+
48
+ return None
7
49
 
8
50
 
9
51
  def normalize_file_path_for_display(file_path: str) -> str: