Coverage for lintro / formatters / formatter.py: 94%
64 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Unified formatter for all tool issues.
3This module provides a unified formatting approach that works with any
4tool's issues by using the BaseIssue.to_display_row() method.
6Instead of having tool-specific formatters, this module allows any issue
7that inherits from BaseIssue to be formatted consistently.
9Example:
10 >>> from lintro.formatters.formatter import format_issues
11 >>> from lintro.parsers.ruff.ruff_issue import RuffIssue
12 >>>
13 >>> issue = RuffIssue(file="foo.py", line=1, code="E501", message="Long")
14 >>> issues = [issue]
15 >>> output = format_issues(issues, output_format="grid")
16 >>> print(output)
17"""
19from __future__ import annotations
21from collections.abc import Sequence
23from lintro.enums.display_column import STANDARD_COLUMNS, DisplayColumn
24from lintro.enums.output_format import OutputFormat, normalize_output_format
25from lintro.formatters.core.format_registry import TableDescriptor, get_style
26from lintro.parsers.base_issue import BaseIssue
27from lintro.utils.path_utils import normalize_file_path_for_display
29# Map DisplayColumn enum to row dict keys
30_COLUMN_KEY_MAP: dict[DisplayColumn, str] = {
31 DisplayColumn.FILE: "file",
32 DisplayColumn.LINE: "line",
33 DisplayColumn.COLUMN: "column",
34 DisplayColumn.CODE: "code",
35 DisplayColumn.MESSAGE: "message",
36 DisplayColumn.SEVERITY: "severity",
37 DisplayColumn.FIXABLE: "fixable",
38 DisplayColumn.DOC_URL: "doc_url",
39}
42class UnifiedTableDescriptor(TableDescriptor):
43 """Table descriptor that works with any BaseIssue subclass.
45 Uses the to_display_row() method to extract data, making it
46 compatible with all issue types.
47 """
49 def __init__(
50 self,
51 columns: list[DisplayColumn] | None = None,
52 ) -> None:
53 """Initialize the descriptor.
55 Args:
56 columns: Custom column list, or None to use STANDARD_COLUMNS.
57 """
58 self._columns = columns if columns is not None else STANDARD_COLUMNS
60 def get_columns(self) -> list[str]:
61 """Return the column names.
63 Returns:
64 List of column header names.
65 """
66 return [str(col) for col in self._columns]
68 def get_rows(self, issues: Sequence[BaseIssue]) -> list[list[str]]:
69 """Extract row data from issues using to_display_row().
71 Args:
72 issues: List of issues (any BaseIssue subclass).
74 Returns:
75 List of rows, each row being a list of column values.
76 """
77 rows: list[list[str]] = []
79 for issue in issues:
80 display_data = issue.to_display_row()
82 # Normalize file path for display
83 if "file" in display_data and display_data["file"]:
84 display_data["file"] = normalize_file_path_for_display(
85 display_data["file"],
86 )
88 row = []
89 for col in self._columns:
90 key = _COLUMN_KEY_MAP.get(col, str(col).lower())
91 value = display_data.get(key, "")
92 row.append(str(value) if value else "")
94 rows.append(row)
96 return rows
99def format_issues(
100 issues: Sequence[BaseIssue],
101 output_format: OutputFormat | str = OutputFormat.GRID,
102 *,
103 columns: list[DisplayColumn] | None = None,
104 tool_name: str | None = None,
105) -> str:
106 """Format any issues using unified display.
108 This function can format issues from any tool that uses BaseIssue,
109 replacing the need for tool-specific formatters.
111 Args:
112 issues: List of issues (any BaseIssue subclass).
113 output_format: Output format (grid, json, plain, etc.).
114 columns: Custom column list (defaults to STANDARD_COLUMNS).
115 tool_name: Tool name for JSON output.
117 Returns:
118 Formatted string.
120 Example:
121 >>> issues = [RuffIssue(file="foo.py", line=1, code="E501", message="Too long")]
122 >>> print(format_issues(issues))
123 """
124 if not issues:
125 return "No issues found."
127 normalized_format = normalize_output_format(output_format)
129 # Conditionally include DOC_URL column when at least one issue has a doc_url
130 effective_columns = columns
131 if columns is None and any(getattr(issue, "doc_url", "") for issue in issues):
132 effective_columns = [*STANDARD_COLUMNS, DisplayColumn.DOC_URL]
134 descriptor = UnifiedTableDescriptor(columns=effective_columns)
136 style = get_style(normalized_format)
137 cols = descriptor.get_columns()
138 rows = descriptor.get_rows(list(issues))
140 return style.format(columns=cols, rows=rows, tool_name=tool_name)
143def format_issues_with_sections(
144 issues: Sequence[BaseIssue],
145 output_format: OutputFormat | str = OutputFormat.GRID,
146 *,
147 group_by_fixable: bool = True,
148 tool_name: str | None = None,
149) -> str:
150 """Format issues with optional fixable/non-fixable sections.
152 This function groups issues by their fixable status and formats
153 them in separate sections (except for JSON format).
155 Args:
156 issues: List of issues (any BaseIssue subclass).
157 output_format: Output format (grid, json, plain, etc.).
158 group_by_fixable: Whether to group by fixable status.
159 tool_name: Tool name for JSON output.
161 Returns:
162 Formatted string with sections.
164 Example:
165 >>> print(format_issues_with_sections(issues, group_by_fixable=True))
166 Auto-fixable issues
167 ... table ...
169 Not auto-fixable issues
170 ... table ...
171 """
172 if not issues:
173 return "No issues found."
175 normalized_format = normalize_output_format(output_format)
177 # JSON/GITHUB format: return single table for compatibility
178 if (
179 normalized_format in {OutputFormat.JSON, OutputFormat.GITHUB}
180 or not group_by_fixable
181 ):
182 return format_issues(
183 issues,
184 output_format=normalized_format,
185 tool_name=tool_name,
186 )
188 # Partition issues by fixable status
189 fixable: list[BaseIssue] = []
190 non_fixable: list[BaseIssue] = []
192 for issue in issues:
193 if getattr(issue, "fixable", False):
194 fixable.append(issue)
195 else:
196 non_fixable.append(issue)
198 sections: list[str] = []
200 if fixable:
201 fixable_output = format_issues(fixable, output_format=normalized_format)
202 sections.append("Auto-fixable issues\n" + fixable_output)
204 if non_fixable:
205 non_fixable_output = format_issues(non_fixable, output_format=normalized_format)
206 sections.append("Not auto-fixable issues\n" + non_fixable_output)
208 if not sections:
209 return "No issues found."
211 return "\n\n".join(sections)
214def format_tool_result(
215 tool_name: str,
216 issues: Sequence[BaseIssue],
217 output_format: OutputFormat | str = OutputFormat.GRID,
218 *,
219 group_by_fixable: bool = False,
220) -> str:
221 """Format a tool's results with appropriate sections and metadata.
223 This is a convenience function that combines formatting with
224 tool-specific defaults.
226 Args:
227 tool_name: Name of the tool.
228 issues: List of issues from the tool.
229 output_format: Output format.
230 group_by_fixable: Group by fixable status.
232 Returns:
233 Formatted string.
234 """
235 if group_by_fixable:
236 return format_issues_with_sections(
237 issues,
238 output_format=output_format,
239 group_by_fixable=True,
240 tool_name=tool_name,
241 )
243 return format_issues(
244 issues,
245 output_format=output_format,
246 tool_name=tool_name,
247 )