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,320 @@
1
+ """Base core implementation for Lintro."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass, field
8
+
9
+ from lintro.enums.tool_type import ToolType
10
+ from lintro.models.core.tool import ToolConfig, ToolResult
11
+
12
+ # Constants for default values
13
+ DEFAULT_TIMEOUT: int = 30
14
+ DEFAULT_EXCLUDE_PATTERNS: list[str] = [
15
+ ".git",
16
+ ".hg",
17
+ ".svn",
18
+ "__pycache__",
19
+ "*.pyc",
20
+ "*.pyo",
21
+ "*.pyd",
22
+ ".pytest_cache",
23
+ ".coverage",
24
+ "htmlcov",
25
+ "dist",
26
+ "build",
27
+ "*.egg-info",
28
+ ]
29
+
30
+
31
+ @dataclass
32
+ class BaseTool(ABC):
33
+ """Base class for all tools.
34
+
35
+ This class provides common functionality for all tools and implements
36
+ the Tool protocol. Tool implementations should inherit from this class
37
+ and implement the abstract methods.
38
+
39
+ Attributes:
40
+ name: str: Tool name.
41
+ description: str: Tool description.
42
+ can_fix: bool: Whether the core can fix issues.
43
+ config: ToolConfig: Tool configuration.
44
+ exclude_patterns: list[str]: List of patterns to exclude.
45
+ include_venv: bool: Whether to include virtual environment files.
46
+ _default_timeout: int: Default timeout for core execution in seconds.
47
+ _default_exclude_patterns: list[str]: Default patterns to exclude.
48
+
49
+ Raises:
50
+ ValueError: If the configuration is invalid.
51
+ """
52
+
53
+ name: str
54
+ description: str
55
+ can_fix: bool
56
+ config: ToolConfig = field(default_factory=ToolConfig)
57
+ exclude_patterns: list[str] = field(default_factory=list)
58
+ include_venv: bool = False
59
+
60
+ _default_timeout: int = DEFAULT_TIMEOUT
61
+ _default_exclude_patterns: list[str] = field(
62
+ default_factory=lambda: DEFAULT_EXCLUDE_PATTERNS
63
+ )
64
+
65
+ def __post_init__(self) -> None:
66
+ """Initialize core options and validate configuration."""
67
+ self.options: dict[str, object] = {}
68
+ self._validate_config()
69
+ self._setup_defaults()
70
+
71
+ def _validate_config(self) -> None:
72
+ """Validate core configuration.
73
+
74
+ Raises:
75
+ ValueError: If the configuration is invalid.
76
+ """
77
+ if not self.name:
78
+ raise ValueError("Tool name cannot be empty")
79
+ if not self.description:
80
+ raise ValueError("Tool description cannot be empty")
81
+ if not isinstance(self.config, ToolConfig):
82
+ raise ValueError("Tool config must be a ToolConfig instance")
83
+ if not isinstance(self.config.priority, int):
84
+ raise ValueError("Tool priority must be an integer")
85
+ if not isinstance(self.config.conflicts_with, list):
86
+ raise ValueError("Tool conflicts_with must be a list")
87
+ if not isinstance(self.config.file_patterns, list):
88
+ raise ValueError("Tool file_patterns must be a list")
89
+ if not isinstance(self.config.tool_type, ToolType):
90
+ raise ValueError("Tool tool_type must be a ToolType instance")
91
+
92
+ def _setup_defaults(self) -> None:
93
+ """Set up default core options and patterns."""
94
+ # Add default exclude patterns if not already present
95
+ for pattern in self._default_exclude_patterns:
96
+ if pattern not in self.exclude_patterns:
97
+ self.exclude_patterns.append(pattern)
98
+
99
+ # Add .lintro-ignore patterns (project-wide) if present
100
+ try:
101
+ lintro_ignore_path = os.path.abspath(".lintro-ignore")
102
+ if os.path.exists(lintro_ignore_path):
103
+ with open(lintro_ignore_path, "r", encoding="utf-8") as f:
104
+ for line in f:
105
+ line_stripped = line.strip()
106
+ if not line_stripped or line_stripped.startswith("#"):
107
+ continue
108
+ if line_stripped not in self.exclude_patterns:
109
+ self.exclude_patterns.append(line_stripped)
110
+ except Exception:
111
+ # Non-fatal if ignore file can't be read
112
+ pass
113
+
114
+ # Load default options from config
115
+ if hasattr(self.config, "options") and self.config.options:
116
+ for key, value in self.config.options.items():
117
+ if key not in self.options:
118
+ self.options[key] = value
119
+
120
+ # Set default timeout if not specified
121
+ if "timeout" not in self.options:
122
+ self.options["timeout"] = self._default_timeout
123
+
124
+ def _run_subprocess(
125
+ self,
126
+ cmd: list[str],
127
+ timeout: int | None = None,
128
+ cwd: str | None = None,
129
+ ) -> tuple[bool, str]:
130
+ """Run a subprocess command.
131
+
132
+ Args:
133
+ cmd: list[str]: Command to run.
134
+ timeout: int | None: Command timeout in seconds (defaults to core's \
135
+ timeout).
136
+ cwd: str | None: Working directory to run the command in (optional).
137
+
138
+ Returns:
139
+ tuple[bool, str]: Tuple of (success, output)
140
+ - success: True if the command succeeded, False otherwise.
141
+ - output: Command output (stdout + stderr).
142
+
143
+ Raises:
144
+ CalledProcessError: If command fails.
145
+ TimeoutExpired: If command times out.
146
+ FileNotFoundError: If command executable is not found.
147
+ """
148
+ try:
149
+ result = subprocess.run(
150
+ cmd,
151
+ capture_output=True,
152
+ text=True,
153
+ timeout=timeout
154
+ or self.options.get(
155
+ "timeout",
156
+ self._default_timeout,
157
+ ),
158
+ check=False,
159
+ cwd=cwd,
160
+ )
161
+ return result.returncode == 0, result.stdout + result.stderr
162
+ except subprocess.TimeoutExpired as e:
163
+ raise subprocess.TimeoutExpired(
164
+ cmd=cmd,
165
+ timeout=timeout
166
+ or self.options.get(
167
+ "timeout",
168
+ self._default_timeout,
169
+ ),
170
+ output=str(e),
171
+ ) from e
172
+ except subprocess.CalledProcessError as e:
173
+ raise subprocess.CalledProcessError(
174
+ returncode=e.returncode,
175
+ cmd=cmd,
176
+ output=e.output,
177
+ stderr=e.stderr,
178
+ ) from e
179
+ except FileNotFoundError as e:
180
+ raise FileNotFoundError(
181
+ f"Command not found: {cmd[0]}. "
182
+ f"Please ensure it is installed and in your PATH.",
183
+ ) from e
184
+
185
+ def set_options(self, **kwargs) -> None:
186
+ """Set core options.
187
+
188
+ Args:
189
+ **kwargs: Tool-specific options.
190
+
191
+ Raises:
192
+ ValueError: If an option value is invalid.
193
+ """
194
+ for key, value in kwargs.items():
195
+ if key == "timeout" and not isinstance(value, (int, type(None))):
196
+ raise ValueError("Timeout must be an integer or None")
197
+ if key == "exclude_patterns" and not isinstance(value, list):
198
+ raise ValueError("Exclude patterns must be a list")
199
+ if key == "include_venv" and not isinstance(value, bool):
200
+ raise ValueError("Include venv must be a boolean")
201
+
202
+ # Update options dict
203
+ self.options.update(kwargs)
204
+
205
+ # Update specific attributes for exclude_patterns and include_venv
206
+ if "exclude_patterns" in kwargs:
207
+ self.exclude_patterns = kwargs["exclude_patterns"]
208
+ if "include_venv" in kwargs:
209
+ self.include_venv = kwargs["include_venv"]
210
+
211
+ def _validate_paths(
212
+ self,
213
+ paths: list[str],
214
+ ) -> None:
215
+ """Validate that paths exist and are accessible.
216
+
217
+ Args:
218
+ paths: list[str]: List of paths to validate.
219
+
220
+ Raises:
221
+ FileNotFoundError: If any path does not exist.
222
+ PermissionError: If any path is not accessible.
223
+ """
224
+ for path in paths:
225
+ if not os.path.exists(path):
226
+ raise FileNotFoundError(f"Path does not exist: {path}")
227
+ if not os.access(path, os.R_OK):
228
+ raise PermissionError(f"Path is not accessible: {path}")
229
+
230
+ def get_cwd(
231
+ self,
232
+ paths: list[str],
233
+ ) -> str | None:
234
+ """Return the common parent directory for the given paths, or None if not
235
+ applicable.
236
+
237
+ Args:
238
+ paths: list[str]: List of file paths to find common parent directory for.
239
+
240
+ Returns:
241
+ str | None: Common parent directory path, or None if not applicable.
242
+ """
243
+ if paths:
244
+ parent_dirs: set[str] = {os.path.dirname(os.path.abspath(p)) for p in paths}
245
+ if len(parent_dirs) == 1:
246
+ return parent_dirs.pop()
247
+ else:
248
+ return os.path.commonpath(list(parent_dirs))
249
+ return None
250
+
251
+ def _get_executable_command(
252
+ self,
253
+ tool_name: str,
254
+ ) -> list[str]:
255
+ """Get the command prefix to execute a tool.
256
+
257
+ This method provides common logic for tool executable detection.
258
+ It first tries to find the tool directly in PATH, and if not found,
259
+ falls back to running via 'uv run' if uv is available.
260
+
261
+ Args:
262
+ tool_name: str: Name of the tool executable to find.
263
+
264
+ Returns:
265
+ list[str]: Command prefix to execute the tool.
266
+
267
+ Examples:
268
+ >>> self._get_executable_command("ruff")
269
+ ["ruff"] # if ruff is directly available
270
+
271
+ >>> self._get_executable_command("ruff")
272
+ ["uv", "run", "ruff"] # if ruff not available but uv is
273
+ """
274
+ # First try direct tool execution
275
+ if shutil.which(tool_name):
276
+ return [tool_name]
277
+
278
+ # If tool not directly available, try via uv
279
+ if shutil.which("uv"):
280
+ return ["uv", "run", tool_name]
281
+
282
+ # Fallback to direct tool (will likely fail but gives clear error)
283
+ return [tool_name]
284
+
285
+ @abstractmethod
286
+ def check(
287
+ self,
288
+ paths: list[str],
289
+ ) -> ToolResult:
290
+ """Check files for issues.
291
+
292
+ Args:
293
+ paths: list[str]: List of file paths to check.
294
+
295
+ Returns:
296
+ ToolResult: ToolResult instance.
297
+
298
+ Raises:
299
+ FileNotFoundError: If any path does not exist or is not accessible.
300
+ subprocess.TimeoutExpired: If the core execution times out.
301
+ subprocess.CalledProcessError: If the core execution fails.
302
+ """
303
+ ...
304
+
305
+ @abstractmethod
306
+ def fix(
307
+ self,
308
+ paths: list[str],
309
+ ) -> ToolResult:
310
+ """Fix issues in files.
311
+
312
+ Args:
313
+ paths: list[str]: List of file paths to fix.
314
+
315
+ Raises:
316
+ NotImplementedError: If the core does not support fixing issues.
317
+ """
318
+ if not self.can_fix:
319
+ raise NotImplementedError(f"{self.name} does not support fixing issues")
320
+ ...
@@ -0,0 +1,167 @@
1
+ """Tool manager for Lintro."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from lintro.models.core.tool import Tool
7
+ from lintro.tools.tool_enum import ToolEnum
8
+
9
+
10
+ @dataclass
11
+ class ToolManager:
12
+ """Manager for core registration and execution.
13
+
14
+ This class is responsible for:
15
+ - Tool registration
16
+ - Tool conflict resolution
17
+ - Tool execution order
18
+ - Tool configuration management
19
+
20
+ Attributes:
21
+ _tools: Dictionary mapping core names to core classes
22
+ _check_tools: Dictionary mapping core names to core classes that can check
23
+ _fix_tools: Dictionary mapping core names to core classes that can fix
24
+ """
25
+
26
+ _tools: dict[ToolEnum, type[Tool]] = field(default_factory=dict)
27
+ _check_tools: dict[ToolEnum, type[Tool]] = field(default_factory=dict)
28
+ _fix_tools: dict[ToolEnum, type[Tool]] = field(default_factory=dict)
29
+
30
+ def register_tool(
31
+ self,
32
+ tool_class: type[Tool],
33
+ ) -> None:
34
+ """Register a core class.
35
+
36
+ Args:
37
+ tool_class: The core class to register.
38
+
39
+ Raises:
40
+ ValueError: If the tool class is not found in ToolEnum.
41
+ """
42
+ tool = tool_class()
43
+ # Find the ToolEnum member for this class
44
+ tool_enum = next((e for e in ToolEnum if e.value is tool_class), None)
45
+ if tool_enum is None:
46
+ raise ValueError(f"Tool class {tool_class} not found in ToolEnum")
47
+ self._tools[tool_enum] = tool_class
48
+ # All tools can check (they all inherit from BaseTool with check method)
49
+ self._check_tools[tool_enum] = tool_class
50
+ # Only tools with can_fix=True can actually fix issues
51
+ if tool.can_fix:
52
+ self._fix_tools[tool_enum] = tool_class
53
+
54
+ def get_tool(
55
+ self,
56
+ name: ToolEnum,
57
+ ) -> Tool:
58
+ """Get a core instance by name.
59
+
60
+ Args:
61
+ name: The name of the core to get
62
+
63
+ Returns:
64
+ The core instance
65
+
66
+ Raises:
67
+ ValueError: If the core is not found
68
+ """
69
+ if name not in self._tools:
70
+ raise ValueError(f"Tool {name} not found")
71
+ return self._tools[name]()
72
+
73
+ def get_tool_execution_order(
74
+ self,
75
+ tool_list: list[ToolEnum],
76
+ ignore_conflicts: bool = False,
77
+ ) -> list[ToolEnum]:
78
+ """Get the order in which tools should be executed.
79
+
80
+ This method takes into account:
81
+ - Tool conflicts
82
+ - Alphabetical ordering
83
+ - Tool dependencies
84
+
85
+ Args:
86
+ tool_list: List of core names to execute
87
+ ignore_conflicts: Whether to ignore core conflicts
88
+
89
+ Returns:
90
+ List of core names in alphabetical execution order
91
+ """
92
+ if not tool_list:
93
+ return []
94
+
95
+ # Get core instances
96
+ tools = {name: self.get_tool(name) for name in tool_list}
97
+
98
+ # Sort tools alphabetically by name
99
+ if ignore_conflicts:
100
+ return sorted(
101
+ tool_list,
102
+ key=lambda name: name.name,
103
+ )
104
+
105
+ # Build conflict graph
106
+ conflict_graph: dict[ToolEnum, set[ToolEnum]] = {
107
+ name: set() for name in tool_list
108
+ }
109
+ for name, tool in tools.items():
110
+ for conflict in tool.config.conflicts_with:
111
+ if conflict in tool_list:
112
+ conflict_graph[name].add(conflict)
113
+ conflict_graph[conflict].add(name)
114
+
115
+ # Sort tools alphabetically by name
116
+ sorted_tools = sorted(
117
+ tool_list,
118
+ key=lambda name: name.name,
119
+ )
120
+
121
+ # Resolve conflicts by keeping the first alphabetical tool
122
+ result = []
123
+ for tool_name in sorted_tools:
124
+ # Check if this core conflicts with any already selected tools
125
+ conflicts = conflict_graph[tool_name] & set(result)
126
+ if not conflicts:
127
+ result.append(tool_name)
128
+
129
+ return result
130
+
131
+ def set_tool_options(
132
+ self,
133
+ name: ToolEnum,
134
+ **options: Any,
135
+ ) -> None:
136
+ """Set options for a core.
137
+
138
+ Args:
139
+ name: The name of the core
140
+ **options: The options to set
141
+ """
142
+ tool = self.get_tool(name)
143
+ tool.set_options(**options)
144
+
145
+ def get_available_tools(self) -> dict[ToolEnum, Tool]:
146
+ """Get all available tools.
147
+
148
+ Returns:
149
+ Dictionary mapping core names to core classes
150
+ """
151
+ return {name: tool_class() for name, tool_class in self._tools.items()}
152
+
153
+ def get_check_tools(self) -> dict[ToolEnum, Tool]:
154
+ """Get all tools that can check files.
155
+
156
+ Returns:
157
+ Dictionary mapping core names to core instances
158
+ """
159
+ return {name: tool_class() for name, tool_class in self._check_tools.items()}
160
+
161
+ def get_fix_tools(self) -> dict[ToolEnum, Tool]:
162
+ """Get all tools that can fix files.
163
+
164
+ Returns:
165
+ Dictionary mapping core names to core instances
166
+ """
167
+ return {name: tool_class() for name, tool_class in self._fix_tools.items()}
File without changes