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,240 @@
1
+ """Yamllint YAML linter integration."""
2
+
3
+ import os
4
+ import subprocess
5
+ from dataclasses import dataclass, field
6
+
7
+ from loguru import logger
8
+
9
+ from lintro.enums.tool_type import ToolType
10
+ from lintro.enums.yamllint_format import (
11
+ YamllintFormat,
12
+ normalize_yamllint_format,
13
+ )
14
+ from lintro.models.core.tool import ToolConfig, ToolResult
15
+ from lintro.parsers.yamllint.yamllint_parser import parse_yamllint_output
16
+ from lintro.tools.core.tool_base import BaseTool
17
+ from lintro.utils.tool_utils import walk_files_with_excludes
18
+
19
+ # Constants
20
+ YAMLLINT_DEFAULT_TIMEOUT: int = 15
21
+ YAMLLINT_DEFAULT_PRIORITY: int = 40
22
+ YAMLLINT_FILE_PATTERNS: list[str] = [
23
+ "*.yml",
24
+ "*.yaml",
25
+ ".yamllint",
26
+ ".yamllint.yml",
27
+ ".yamllint.yaml",
28
+ ]
29
+ YAMLLINT_FORMATS: tuple[str, ...] = tuple(m.name.lower() for m in YamllintFormat)
30
+
31
+
32
+ @dataclass
33
+ class YamllintTool(BaseTool):
34
+ """Yamllint YAML linter integration.
35
+
36
+ Yamllint is a linter for YAML files that checks for syntax errors,
37
+ formatting issues, and other YAML best practices.
38
+
39
+ Attributes:
40
+ name: Tool name
41
+ description: Tool description
42
+ can_fix: Whether the tool can fix issues
43
+ config: Tool configuration
44
+ exclude_patterns: List of patterns to exclude
45
+ include_venv: Whether to include virtual environment files
46
+ """
47
+
48
+ name: str = "yamllint"
49
+ description: str = "YAML linter for syntax and style checking"
50
+ can_fix: bool = False
51
+ config: ToolConfig = field(
52
+ default_factory=lambda: ToolConfig(
53
+ priority=YAMLLINT_DEFAULT_PRIORITY,
54
+ conflicts_with=[],
55
+ file_patterns=YAMLLINT_FILE_PATTERNS,
56
+ tool_type=ToolType.LINTER,
57
+ options={
58
+ "timeout": YAMLLINT_DEFAULT_TIMEOUT,
59
+ # Use parsable by default; aligns with parser expectations
60
+ "format": "parsable",
61
+ "config_file": None,
62
+ "config_data": None,
63
+ "strict": False,
64
+ "relaxed": False,
65
+ "no_warnings": False,
66
+ },
67
+ ),
68
+ )
69
+
70
+ def __post_init__(self) -> None:
71
+ """Initialize the tool."""
72
+ super().__post_init__()
73
+
74
+ def set_options(
75
+ self,
76
+ format: str | YamllintFormat | None = None,
77
+ config_file: str | None = None,
78
+ config_data: str | None = None,
79
+ strict: bool | None = None,
80
+ relaxed: bool | None = None,
81
+ no_warnings: bool | None = None,
82
+ **kwargs,
83
+ ) -> None:
84
+ """Set Yamllint-specific options.
85
+
86
+ Args:
87
+ format: Output format (parsable, standard, colored, github, auto)
88
+ config_file: Path to yamllint config file
89
+ config_data: Inline config data (YAML string)
90
+ strict: Return non-zero exit code on warnings as well as errors
91
+ relaxed: Use relaxed configuration
92
+ no_warnings: Output only error level problems
93
+ **kwargs: Other tool options
94
+
95
+ Raises:
96
+ ValueError: If an option value is invalid
97
+ """
98
+ if format is not None:
99
+ # Accept both enum and string values for backward compatibility
100
+ fmt_enum = normalize_yamllint_format(format) # type: ignore[arg-type]
101
+ format = fmt_enum.name.lower()
102
+ if config_file is not None and not isinstance(config_file, str):
103
+ raise ValueError("config_file must be a string path")
104
+ if config_data is not None and not isinstance(config_data, str):
105
+ raise ValueError("config_data must be a YAML string")
106
+ if strict is not None and not isinstance(strict, bool):
107
+ raise ValueError("strict must be a boolean")
108
+ if relaxed is not None and not isinstance(relaxed, bool):
109
+ raise ValueError("relaxed must be a boolean")
110
+ if no_warnings is not None and not isinstance(no_warnings, bool):
111
+ raise ValueError("no_warnings must be a boolean")
112
+ options = {
113
+ "format": format,
114
+ "config_file": config_file,
115
+ "config_data": config_data,
116
+ "strict": strict,
117
+ "relaxed": relaxed,
118
+ "no_warnings": no_warnings,
119
+ }
120
+ options = {k: v for k, v in options.items() if v is not None}
121
+ super().set_options(**options, **kwargs)
122
+
123
+ def _build_command(self) -> list[str]:
124
+ """Build the yamllint command.
125
+
126
+ Returns:
127
+ list[str]: Command arguments for yamllint.
128
+ """
129
+ cmd: list[str] = ["yamllint"]
130
+ format_option: str = self.options.get("format", YAMLLINT_FORMATS[0])
131
+ cmd.extend(["--format", format_option])
132
+ config_file: str | None = self.options.get("config_file")
133
+ if config_file:
134
+ cmd.extend(["--config-file", config_file])
135
+ config_data: str | None = self.options.get("config_data")
136
+ if config_data:
137
+ cmd.extend(["--config-data", config_data])
138
+ if self.options.get("strict", False):
139
+ cmd.append("--strict")
140
+ if self.options.get("relaxed", False):
141
+ cmd.append("--relaxed")
142
+ if self.options.get("no_warnings", False):
143
+ cmd.append("--no-warnings")
144
+ return cmd
145
+
146
+ def check(
147
+ self,
148
+ paths: list[str],
149
+ ) -> ToolResult:
150
+ """Check files with Yamllint.
151
+
152
+ Args:
153
+ paths: list[str]: List of file or directory paths to check.
154
+
155
+ Returns:
156
+ ToolResult: Result of the check operation.
157
+ """
158
+ self._validate_paths(paths=paths)
159
+ if not paths:
160
+ return ToolResult(
161
+ name=self.name,
162
+ success=True,
163
+ output="No files to check.",
164
+ issues_count=0,
165
+ )
166
+ yaml_files: list[str] = walk_files_with_excludes(
167
+ paths=paths,
168
+ file_patterns=self.config.file_patterns,
169
+ exclude_patterns=self.exclude_patterns,
170
+ include_venv=self.include_venv,
171
+ )
172
+ logger.debug(f"Files to check: {yaml_files}")
173
+ timeout: int = self.options.get("timeout", YAMLLINT_DEFAULT_TIMEOUT)
174
+ # Aggregate parsed issues across files and rely on table renderers upstream
175
+ all_success: bool = True
176
+ all_issues: list = []
177
+ skipped_files: list[str] = []
178
+ total_issues: int = 0
179
+ for file_path in yaml_files:
180
+ # Use absolute path; run with the file's parent as cwd so that
181
+ # yamllint discovers any local .yamllint config beside the file.
182
+ abs_file: str = os.path.abspath(file_path)
183
+ cmd: list[str] = self._build_command() + [abs_file]
184
+ try:
185
+ success, output = self._run_subprocess(
186
+ cmd=cmd,
187
+ timeout=timeout,
188
+ cwd=self.get_cwd(paths=[abs_file]),
189
+ )
190
+ issues = parse_yamllint_output(output=output)
191
+ issues_count: int = len(issues)
192
+ # Yamllint returns 1 on errors/warnings unless --no-warnings/relaxed
193
+ # Use parsed issues to determine success and counts reliably.
194
+ if issues_count > 0:
195
+ all_success = False
196
+ total_issues += issues_count
197
+ if issues:
198
+ all_issues.extend(issues)
199
+ except subprocess.TimeoutExpired:
200
+ skipped_files.append(file_path)
201
+ all_success = False
202
+ except Exception as e:
203
+ # Suppress missing file noise in console output; keep as debug
204
+ err_msg = str(e)
205
+ if "No such file or directory" in err_msg:
206
+ # treat as skipped/missing silently for user; do not fail run
207
+ continue
208
+ # Do not add raw errors to user-facing output; mark failure only
209
+ all_success = False
210
+ # Let the unified formatter render a table from issues; no raw output
211
+ output = None
212
+ return ToolResult(
213
+ name=self.name,
214
+ success=all_success,
215
+ output=output,
216
+ issues_count=total_issues,
217
+ issues=all_issues,
218
+ )
219
+
220
+ def fix(
221
+ self,
222
+ paths: list[str],
223
+ ) -> ToolResult:
224
+ """Yamllint cannot fix issues, only report them.
225
+
226
+ Args:
227
+ paths: list[str]: List of file or directory paths to fix.
228
+
229
+ Returns:
230
+ ToolResult: Result indicating that fixing is not supported.
231
+ """
232
+ return ToolResult(
233
+ name=self.name,
234
+ success=False,
235
+ output=(
236
+ "Yamllint is a linter only and cannot fix issues. Use a YAML "
237
+ "formatter like Prettier for formatting."
238
+ ),
239
+ issues_count=0,
240
+ )
@@ -0,0 +1,17 @@
1
+ """ToolEnum for all Lintro tools, mapping to their classes."""
2
+
3
+ from enum import Enum
4
+
5
+ from lintro.tools.implementations.tool_darglint import DarglintTool
6
+ from lintro.tools.implementations.tool_hadolint import HadolintTool
7
+ from lintro.tools.implementations.tool_prettier import PrettierTool
8
+ from lintro.tools.implementations.tool_ruff import RuffTool
9
+ from lintro.tools.implementations.tool_yamllint import YamllintTool
10
+
11
+
12
+ class ToolEnum(Enum):
13
+ DARGLINT = DarglintTool
14
+ HADOLINT = HadolintTool
15
+ PRETTIER = PrettierTool
16
+ RUFF = RuffTool
17
+ YAMLLINT = YamllintTool
File without changes
@@ -0,0 +1,84 @@
1
+ """CLI to normalize ASCII art files to a standard size.
2
+
3
+ Usage (via uv):
4
+ uv run python -m lintro.utils.ascii_normalize_cli --width 80 --height 20
5
+
6
+ By default processes all .txt files under lintro/ascii-art.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ from pathlib import Path
13
+
14
+ from lintro.utils.formatting import normalize_ascii_file_sections
15
+
16
+
17
+ def _ascii_art_dir() -> Path:
18
+ return Path(__file__).resolve().parents[1] / "ascii-art"
19
+
20
+
21
+ def _write_sections(
22
+ file_path: Path,
23
+ sections: list[list[str]],
24
+ ) -> None:
25
+ # Join sections with a single blank line between them
26
+ lines: list[str] = []
27
+ for idx, sec in enumerate(sections):
28
+ lines.extend(sec)
29
+ if idx != len(sections) - 1:
30
+ lines.append("")
31
+ file_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
32
+
33
+
34
+ def main() -> int:
35
+ parser = argparse.ArgumentParser(description="Normalize ASCII art files.")
36
+ parser.add_argument("files", nargs="*", help="Specific ASCII art files to process")
37
+ parser.add_argument("--width", type=int, default=80)
38
+ parser.add_argument("--height", type=int, default=20)
39
+ parser.add_argument(
40
+ "--align", choices=["left", "center", "right"], default="center"
41
+ )
42
+ parser.add_argument(
43
+ "--valign",
44
+ choices=["top", "middle", "bottom"],
45
+ default="middle",
46
+ )
47
+ args = parser.parse_args()
48
+
49
+ base_dir = _ascii_art_dir()
50
+ if not base_dir.exists():
51
+ print(f"ASCII art directory not found: {base_dir}")
52
+ return 1
53
+
54
+ targets: list[Path]
55
+ if args.files:
56
+ targets = [base_dir / f for f in args.files]
57
+ else:
58
+ targets = sorted(base_dir.glob("*.txt"))
59
+
60
+ updated = 0
61
+ for fp in targets:
62
+ sections = normalize_ascii_file_sections(
63
+ file_path=fp,
64
+ width=args.width,
65
+ height=args.height,
66
+ align=args.align,
67
+ valign=args.valign,
68
+ )
69
+ if not sections:
70
+ print(f"Skipping (no sections or unreadable): {fp.name}")
71
+ continue
72
+ _write_sections(
73
+ arg1=fp,
74
+ arg2=sections,
75
+ )
76
+ updated += 1
77
+ print(f"Normalized: {fp.name} -> {args.width}x{args.height}")
78
+
79
+ print(f"Done. Updated {updated} file(s).")
80
+ return 0
81
+
82
+
83
+ if __name__ == "__main__": # pragma: no cover - exercised via CLI in practice
84
+ raise SystemExit(main())
lintro/utils/config.py ADDED
@@ -0,0 +1,39 @@
1
+ """Project configuration helpers for Lintro.
2
+
3
+ Reads configuration from `pyproject.toml` under the `[tool.lintro]` table.
4
+ Allows tool-specific defaults via `[tool.lintro.<tool>]` (e.g., `[tool.lintro.ruff]`).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import tomllib
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ def _load_pyproject() -> dict[str, Any]:
15
+ pyproject_path = Path("pyproject.toml")
16
+ if not pyproject_path.exists():
17
+ return {}
18
+ try:
19
+ with pyproject_path.open("rb") as f:
20
+ data = tomllib.load(f)
21
+ return data.get("tool", {}).get("lintro", {})
22
+ except Exception:
23
+ return {}
24
+
25
+
26
+ def load_lintro_tool_config(tool_name: str) -> dict[str, Any]:
27
+ """Load tool-specific config from pyproject.
28
+
29
+ Args:
30
+ tool_name: Tool name (e.g., "ruff").
31
+
32
+ Returns:
33
+ A dict of options for the given tool, or an empty dict if none.
34
+ """
35
+ cfg = _load_pyproject()
36
+ section = cfg.get(tool_name, {})
37
+ if isinstance(section, dict):
38
+ return section
39
+ return {}