lintro 0.13.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.
- lintro/__init__.py +1 -1
- lintro/cli.py +226 -16
- lintro/cli_utils/commands/__init__.py +8 -1
- lintro/cli_utils/commands/check.py +1 -0
- lintro/cli_utils/commands/config.py +325 -0
- lintro/cli_utils/commands/init.py +361 -0
- lintro/cli_utils/commands/list_tools.py +180 -42
- lintro/cli_utils/commands/test.py +316 -0
- lintro/cli_utils/commands/versions.py +81 -0
- lintro/config/__init__.py +62 -0
- lintro/config/config_loader.py +420 -0
- lintro/config/lintro_config.py +189 -0
- lintro/config/tool_config_generator.py +403 -0
- lintro/enums/tool_name.py +2 -0
- lintro/enums/tool_type.py +2 -0
- lintro/formatters/tools/__init__.py +12 -0
- lintro/formatters/tools/eslint_formatter.py +108 -0
- lintro/formatters/tools/markdownlint_formatter.py +88 -0
- lintro/formatters/tools/pytest_formatter.py +201 -0
- lintro/parsers/__init__.py +69 -9
- lintro/parsers/bandit/__init__.py +6 -0
- lintro/parsers/bandit/bandit_issue.py +49 -0
- lintro/parsers/bandit/bandit_parser.py +99 -0
- lintro/parsers/black/black_issue.py +4 -0
- lintro/parsers/eslint/__init__.py +6 -0
- lintro/parsers/eslint/eslint_issue.py +26 -0
- lintro/parsers/eslint/eslint_parser.py +63 -0
- lintro/parsers/markdownlint/__init__.py +6 -0
- lintro/parsers/markdownlint/markdownlint_issue.py +22 -0
- lintro/parsers/markdownlint/markdownlint_parser.py +113 -0
- lintro/parsers/pytest/__init__.py +21 -0
- lintro/parsers/pytest/pytest_issue.py +28 -0
- lintro/parsers/pytest/pytest_parser.py +483 -0
- lintro/tools/__init__.py +2 -0
- lintro/tools/core/timeout_utils.py +112 -0
- lintro/tools/core/tool_base.py +255 -45
- lintro/tools/core/tool_manager.py +77 -24
- lintro/tools/core/version_requirements.py +482 -0
- lintro/tools/implementations/pytest/pytest_command_builder.py +311 -0
- lintro/tools/implementations/pytest/pytest_config.py +200 -0
- lintro/tools/implementations/pytest/pytest_error_handler.py +128 -0
- lintro/tools/implementations/pytest/pytest_executor.py +122 -0
- lintro/tools/implementations/pytest/pytest_handlers.py +375 -0
- lintro/tools/implementations/pytest/pytest_option_validators.py +212 -0
- lintro/tools/implementations/pytest/pytest_output_processor.py +408 -0
- lintro/tools/implementations/pytest/pytest_result_processor.py +113 -0
- lintro/tools/implementations/pytest/pytest_utils.py +697 -0
- lintro/tools/implementations/tool_actionlint.py +106 -16
- lintro/tools/implementations/tool_bandit.py +23 -7
- lintro/tools/implementations/tool_black.py +236 -29
- lintro/tools/implementations/tool_darglint.py +180 -21
- lintro/tools/implementations/tool_eslint.py +374 -0
- lintro/tools/implementations/tool_hadolint.py +94 -25
- lintro/tools/implementations/tool_markdownlint.py +354 -0
- lintro/tools/implementations/tool_prettier.py +313 -26
- lintro/tools/implementations/tool_pytest.py +327 -0
- lintro/tools/implementations/tool_ruff.py +247 -70
- lintro/tools/implementations/tool_yamllint.py +448 -34
- lintro/tools/tool_enum.py +6 -0
- lintro/utils/config.py +41 -18
- lintro/utils/console_logger.py +211 -25
- lintro/utils/path_utils.py +42 -0
- lintro/utils/tool_executor.py +336 -39
- lintro/utils/tool_utils.py +38 -2
- lintro/utils/unified_config.py +926 -0
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/METADATA +131 -29
- lintro-0.17.2.dist-info/RECORD +134 -0
- lintro-0.13.2.dist-info/RECORD +0 -96
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
- {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/top_level.txt +0 -0
lintro/utils/console_logger.py
CHANGED
|
@@ -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
|
-
|
|
107
|
+
# Only capture WARNING and ERROR for console
|
|
86
108
|
logger.add(
|
|
87
109
|
sys.stderr,
|
|
88
|
-
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
|
-
|
|
621
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
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
|
-
|
|
641
|
-
fg="
|
|
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 == "
|
|
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"]
|
lintro/utils/path_utils.py
CHANGED
|
@@ -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:
|