Coverage for lintro / utils / post_checks.py: 86%

80 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Post-check execution utilities. 

2 

3Handles optional post-check tools that run after primary linting tools. 

4""" 

5 

6from __future__ import annotations 

7 

8from typing import TYPE_CHECKING 

9 

10from lintro.enums.action import Action 

11from lintro.enums.group_by import normalize_group_by 

12from lintro.enums.output_format import OutputFormat, normalize_output_format 

13from lintro.plugins.registry import ToolRegistry 

14from lintro.tools import tool_manager 

15from lintro.utils.config import load_post_checks_config 

16from lintro.utils.output import format_tool_output 

17from lintro.utils.unified_config import UnifiedConfigManager 

18 

19if TYPE_CHECKING: 

20 from lintro.models.core.tool_result import ToolResult 

21 from lintro.utils.console import ThreadSafeConsoleLogger 

22 

23 

24def execute_post_checks( 

25 *, 

26 action: Action, 

27 paths: list[str], 

28 exclude: str | None, 

29 include_venv: bool, 

30 group_by: str, 

31 output_format: str, 

32 verbose: bool, 

33 raw_output: bool, 

34 logger: ThreadSafeConsoleLogger, 

35 all_results: list[ToolResult], 

36 total_issues: int, 

37 total_fixed: int, 

38 total_remaining: int, 

39) -> tuple[int, int, int]: 

40 """Execute post-check tools after primary linting. 

41 

42 Args: 

43 action: The action being performed. 

44 paths: List of paths to check. 

45 exclude: Patterns to exclude. 

46 include_venv: Whether to include virtual environments. 

47 group_by: How to group results. 

48 output_format: Output format for results. 

49 verbose: Whether to enable verbose output. 

50 raw_output: Whether to show raw tool output. 

51 logger: Logger instance for output. 

52 all_results: List to append results to. 

53 total_issues: Current total issues count. 

54 total_fixed: Current total fixed count. 

55 total_remaining: Current total remaining count. 

56 

57 Returns: 

58 tuple[int, int, int]: Updated (total_issues, total_fixed, total_remaining) 

59 """ 

60 # Skip post-checks for test action - test is independent from linting/formatting 

61 if action == Action.TEST: 

62 return (total_issues, total_fixed, total_remaining) 

63 

64 # Normalize enums while maintaining backward compatibility 

65 output_fmt_enum: OutputFormat = normalize_output_format(output_format) 

66 _ = normalize_group_by(group_by) # Normalize for validation, return value unused 

67 json_output_mode = output_fmt_enum == OutputFormat.JSON 

68 

69 # Load post-checks config 

70 post_cfg = load_post_checks_config() 

71 post_enabled = bool(post_cfg.get("enabled", False)) 

72 post_tools = list(post_cfg.get("tools", [])) if post_enabled else [] 

73 enforce_failure = bool(post_cfg.get("enforce_failure", action == Action.CHECK)) 

74 

75 # In JSON mode, we still need exit-code enforcement even if we skip 

76 # rendering post-check outputs. If a post-check tool is unavailable 

77 # and enforce_failure is enabled during check, append a failure result 

78 # so summaries and exit codes reflect the condition. 

79 if post_tools and json_output_mode and action == Action.CHECK and enforce_failure: 

80 for post_tool_name in post_tools: 

81 tool_name_lower = post_tool_name.lower() 

82 if not ToolRegistry.is_registered(tool_name_lower): 

83 from lintro.models.core.tool_result import ToolResult 

84 

85 all_results.append( 

86 ToolResult( 

87 name=post_tool_name, 

88 success=False, 

89 output=f"Tool '{post_tool_name}' not registered", 

90 issues_count=1, 

91 ), 

92 ) 

93 

94 if post_tools: 

95 # Print a clear post-checks section header (only when not in JSON mode) 

96 if not json_output_mode: 

97 logger.print_post_checks_header() 

98 

99 for post_tool_name in post_tools: 

100 tool_name_lower = post_tool_name.lower() 

101 

102 if not ToolRegistry.is_registered(tool_name_lower): 

103 logger.console_output( 

104 text=f"Warning: Unknown post-check tool: {post_tool_name}", 

105 color="yellow", 

106 ) 

107 continue 

108 

109 # If the tool isn't available in the current environment (e.g., unit 

110 # tests that stub a limited set of tools), skip without enforcing 

111 # failure. Post-checks are optional when the tool cannot be resolved 

112 # from the tool manager. 

113 try: 

114 tool = tool_manager.get_tool(tool_name_lower) 

115 except (KeyError, ValueError, RuntimeError) as e: 

116 logger.console_output( 

117 text=f"Warning: Post-check '{post_tool_name}' unavailable: {e}", 

118 color="yellow", 

119 ) 

120 continue 

121 

122 # Post-checks run with explicit headers (reuse standard header) 

123 if not json_output_mode: 

124 logger.print_tool_header(tool_name=tool_name_lower, action=action) 

125 

126 try: 

127 # Configure post-check tool using UnifiedConfigManager 

128 # This replaces manual sync logic with unified config management 

129 post_config_manager = UnifiedConfigManager() 

130 post_config_manager.apply_config_to_tool(tool=tool) 

131 

132 tool.set_options(include_venv=include_venv) 

133 if exclude: 

134 exclude_patterns: list[str] = [ 

135 p.strip() for p in exclude.split(",") 

136 ] 

137 tool.set_options(exclude_patterns=exclude_patterns) 

138 

139 # For check: Black should run in check mode; for fmt: run fix 

140 if action == Action.FIX and tool.definition.can_fix: 

141 result = tool.fix(paths=paths, options={}) 

142 issues_count = getattr(result, "issues_count", 0) 

143 fixed_count = getattr(result, "fixed_issues_count", None) 

144 total_fixed += fixed_count if fixed_count is not None else 0 

145 remaining_count = getattr(result, "remaining_issues_count", None) 

146 total_remaining += ( 

147 remaining_count if remaining_count is not None else issues_count 

148 ) 

149 else: 

150 result = tool.check(paths=paths, options={}) 

151 issues_count = getattr(result, "issues_count", 0) 

152 total_issues += issues_count 

153 

154 # Format and display output 

155 output = getattr(result, "output", None) 

156 issues = getattr(result, "issues", None) 

157 formatted_output: str = "" 

158 if (output and output.strip()) or issues: 

159 formatted_output = format_tool_output( 

160 tool_name=tool_name_lower, 

161 output=output or "", 

162 output_format=output_fmt_enum.value, 

163 issues=issues, 

164 ) 

165 

166 if not json_output_mode: 

167 from lintro.utils.result_formatters import print_tool_result 

168 

169 def success_func(message: str) -> None: 

170 logger.console_output(text=message, color="green") 

171 

172 if formatted_output and formatted_output.strip(): 

173 print_tool_result( 

174 console_output_func=logger.console_output, 

175 success_func=success_func, 

176 tool_name=tool_name_lower, 

177 output=( 

178 formatted_output if not raw_output else (output or "") 

179 ), 

180 issues_count=issues_count, 

181 raw_output_for_meta=output, 

182 action=action, 

183 success=getattr(result, "success", None), 

184 ) 

185 elif issues_count == 0 and getattr(result, "success", True): 

186 # Show success message when no issues found 

187 logger.console_output(text="Processing files") 

188 logger.console_output(text="✓ No issues found.", color="green") 

189 logger.console_output(text="") 

190 

191 all_results.append(result) 

192 except (OSError, ValueError, RuntimeError, TypeError, AttributeError) as e: 

193 # Do not crash the entire run due to missing optional post-check 

194 # tool 

195 logger.console_output( 

196 text=f"Warning: Post-check '{post_tool_name}' failed: {e}", 

197 color="yellow", 

198 ) 

199 # Only enforce failure when the tool was available and executed 

200 if enforce_failure and action == Action.CHECK: 

201 from lintro.models.core.tool_result import ToolResult 

202 

203 all_results.append( 

204 ToolResult( 

205 name=post_tool_name, 

206 success=False, 

207 output=str(e), 

208 issues_count=1, 

209 ), 

210 ) 

211 

212 return total_issues, total_fixed, total_remaining