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

1"""Unified formatter for all tool issues. 

2 

3This module provides a unified formatting approach that works with any 

4tool's issues by using the BaseIssue.to_display_row() method. 

5 

6Instead of having tool-specific formatters, this module allows any issue 

7that inherits from BaseIssue to be formatted consistently. 

8 

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""" 

18 

19from __future__ import annotations 

20 

21from collections.abc import Sequence 

22 

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 

28 

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} 

40 

41 

42class UnifiedTableDescriptor(TableDescriptor): 

43 """Table descriptor that works with any BaseIssue subclass. 

44 

45 Uses the to_display_row() method to extract data, making it 

46 compatible with all issue types. 

47 """ 

48 

49 def __init__( 

50 self, 

51 columns: list[DisplayColumn] | None = None, 

52 ) -> None: 

53 """Initialize the descriptor. 

54 

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 

59 

60 def get_columns(self) -> list[str]: 

61 """Return the column names. 

62 

63 Returns: 

64 List of column header names. 

65 """ 

66 return [str(col) for col in self._columns] 

67 

68 def get_rows(self, issues: Sequence[BaseIssue]) -> list[list[str]]: 

69 """Extract row data from issues using to_display_row(). 

70 

71 Args: 

72 issues: List of issues (any BaseIssue subclass). 

73 

74 Returns: 

75 List of rows, each row being a list of column values. 

76 """ 

77 rows: list[list[str]] = [] 

78 

79 for issue in issues: 

80 display_data = issue.to_display_row() 

81 

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 ) 

87 

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 "") 

93 

94 rows.append(row) 

95 

96 return rows 

97 

98 

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. 

107 

108 This function can format issues from any tool that uses BaseIssue, 

109 replacing the need for tool-specific formatters. 

110 

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. 

116 

117 Returns: 

118 Formatted string. 

119 

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." 

126 

127 normalized_format = normalize_output_format(output_format) 

128 

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] 

133 

134 descriptor = UnifiedTableDescriptor(columns=effective_columns) 

135 

136 style = get_style(normalized_format) 

137 cols = descriptor.get_columns() 

138 rows = descriptor.get_rows(list(issues)) 

139 

140 return style.format(columns=cols, rows=rows, tool_name=tool_name) 

141 

142 

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. 

151 

152 This function groups issues by their fixable status and formats 

153 them in separate sections (except for JSON format). 

154 

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. 

160 

161 Returns: 

162 Formatted string with sections. 

163 

164 Example: 

165 >>> print(format_issues_with_sections(issues, group_by_fixable=True)) 

166 Auto-fixable issues 

167 ... table ... 

168 

169 Not auto-fixable issues 

170 ... table ... 

171 """ 

172 if not issues: 

173 return "No issues found." 

174 

175 normalized_format = normalize_output_format(output_format) 

176 

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 ) 

187 

188 # Partition issues by fixable status 

189 fixable: list[BaseIssue] = [] 

190 non_fixable: list[BaseIssue] = [] 

191 

192 for issue in issues: 

193 if getattr(issue, "fixable", False): 

194 fixable.append(issue) 

195 else: 

196 non_fixable.append(issue) 

197 

198 sections: list[str] = [] 

199 

200 if fixable: 

201 fixable_output = format_issues(fixable, output_format=normalized_format) 

202 sections.append("Auto-fixable issues\n" + fixable_output) 

203 

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) 

207 

208 if not sections: 

209 return "No issues found." 

210 

211 return "\n\n".join(sections) 

212 

213 

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. 

222 

223 This is a convenience function that combines formatting with 

224 tool-specific defaults. 

225 

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. 

231 

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 ) 

242 

243 return format_issues( 

244 issues, 

245 output_format=output_format, 

246 tool_name=tool_name, 

247 )