Coverage for  / opt / hostedtoolcache / Python / 3.11.14 / x64 / lib / python3.11 / site-packages / rattlesnake / cicd / report_pylint.py: 97%

103 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-27 18:22 +0000

1#!/usr/bin/env python3 

2""" 

3Pylint HTML Report Generator (Functional Version) 

4 

5This module extracts pylint output and generates a custom HTML report. 

6""" 

7 

8import argparse 

9import html 

10import re 

11import sys 

12 

13from typing import Dict, List, Tuple 

14 

15 

16from rattlesnake.cicd.utilities import ( 

17 get_score_color_lint, 

18 extend_timestamp, 

19 write_report, 

20) 

21 

22 

23def get_pylint_content(input_file: str) -> str: 

24 """ 

25 Read pylint output from file. 

26 

27 Args: 

28 input_file: Path to the pylint output file 

29 

30 Returns: 

31 Content of the pylint output file 

32 

33 Raises: 

34 FileNotFoundError: If the input file is not found. 

35 IOError: If there is an error reading the file. 

36 """ 

37 try: 

38 with open(input_file, "r", encoding="utf-8") as f: 

39 return f.read() 

40 except FileNotFoundError as e: 

41 raise FileNotFoundError(f'Input file not found: "{input_file}"') from e 

42 except IOError as e: 

43 # Re-raise with a more informative message if needed, or just re-raise 

44 raise IOError(f'Error reading input file "{input_file}": {e}') from e 

45 

46 

47def get_pylint_sections(pylint_content: str) -> Tuple[List[str], List[str]]: 

48 """ 

49 Parse pylint output to extract issues and summary. 

50 

51 Args: 

52 pylint_content: Raw pylint output content 

53 

54 Returns: 

55 Tuple of (issues_list, summary_lines) 

56 """ 

57 lines: List[str] = pylint_content.split("\n") 

58 issues: List[str] = [] 

59 summary_started: bool = False 

60 summary_lines: List[str] = [] 

61 

62 for line in lines: 

63 if line.startswith("************* Module"): 

64 _: str = line.replace("************* Module ", "") 

65 # Extract any line with codes that match convention, warning, error, 

66 # or refactor, e.g., C0114:, W0611:, E1101:, or R0913:, for example. 

67 elif re.search(r"\b[CWER]\d{4}:", line): 

68 # Extract issues that match the pylint format 

69 issues.append(line) 

70 elif summary_started or "Your code has been rated at" in line: 

71 summary_started = True 

72 summary_lines.append(line) 

73 

74 return issues, summary_lines 

75 

76 

77def get_score_from_summary(summary_lines: List[str]) -> str: 

78 """ 

79 Extract the pylint score from summary lines. 

80 

81 Args: 

82 summary_lines: List of summary lines from pylint output 

83 

84 Returns: 

85 Pylint score as a string, e.g., "6.00" (e.g., from "6.00/10 string) 

86 """ 

87 for line in summary_lines: 

88 if "Your code has been rated at" in line: 

89 match = re.search(r"(\d+\.\d+)/10", line) 

90 if match: 

91 return match.group(1) 

92 return "0.00" 

93 

94 

95def get_issue_counts(issues: List[str]) -> Dict[str, int]: 

96 """ 

97 Count issues by type (convention, warning, error, refactor). 

98 

99 Args: 

100 issues: List of pylint issue strings 

101 

102 Returns: 

103 Dictionary with counts for each issue type 

104 """ 

105 convention_count: int = len([i for i in issues if re.search(r"C\d{4}:", i)]) 

106 warning_count: int = len([i for i in issues if re.search(r"W\d{4}:", i)]) 

107 error_count: int = len([i for i in issues if re.search(r"E\d{4}:", i)]) 

108 refactor_count: int = len([i for i in issues if re.search(r"R\d{4}:", i)]) 

109 

110 return { 

111 "convention": convention_count, 

112 "warning": warning_count, 

113 "error": error_count, 

114 "refactor": refactor_count, 

115 } 

116 

117 

118def get_issues_list_html(issues: List[str]) -> str: 

119 """ 

120 Create HTML for the issues section. 

121 

122 Args: 

123 issues: List of pylint issues 

124 

125 Returns: 

126 HTML string for issues section 

127 """ 

128 if not issues: 

129 return "<p>No issues found! 🎉</p>" 

130 

131 issues_list: List[str] = [] 

132 

133 for issue in issues: 

134 # TODO: exactly match to "refactor" via r"R\d{4}:" and add an "unknown" 

135 # css_class. Review Python version to see if we can use a match statement 

136 # instead of nested if statements. 

137 if re.search(r"C\d{4}:", issue): 

138 css_class = "convention" 

139 elif re.search(r"W\d{4}:", issue): 

140 css_class = "warning" 

141 elif re.search(r"E\d{4}:", issue): 

142 css_class = "error" 

143 else: 

144 css_class = "refactor" 

145 issues_list.append(f'<div class="issue {css_class}">{html.escape(issue)}</div>') 

146 

147 return f'<div class="issues-list">{"".join(issues_list)}</div>' 

148 

149 

150def get_report_html( 

151 pylint_content: str, 

152 issues: List[str], 

153 summary_lines: List[str], 

154 pylint_score: str, 

155 timestamp: str, 

156 run_id: str, 

157 ref_name: str, 

158 github_sha: str, 

159 github_repo: str, 

160) -> str: 

161 """ 

162 Create the complete HTML report. 

163 

164 Args: 

165 pylint_content: Raw pylint output content 

166 issues: List of pylint issues 

167 summary_lines: Summary lines from pylint output 

168 pylint_score: Pylint score 

169 run_id: GitHub run ID 

170 ref_name: Git reference name 

171 github_sha: GitHub SHA 

172 github_repo: GitHub repository name 

173 

174 Returns: 

175 Complete HTML report as string 

176 """ 

177 # timestamp: str = get_timestamp() 

178 timestamp_ext: str = extend_timestamp(short=timestamp) 

179 score_color: str = get_score_color_lint(pylint_score) 

180 issue_counts: Dict[str, int] = get_issue_counts(issues) 

181 issues_html: str = get_issues_list_html(issues) 

182 

183 html_content: str = f"""<!DOCTYPE html> 

184<html lang="en"> 

185<head> 

186 <meta charset="UTF-8"> 

187 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 

188 <title>Pylint Report</title> 

189 <style> 

190 body {{ 

191 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 

192 margin: 0; padding: 20px; background: #f6f8fa; line-height: 1.6; 

193 background: lightgray; 

194 }} 

195 .container {{ 

196 max-width: 1200px; margin: 0 auto; 

197 }} 

198 .header {{ 

199 background: white; padding: 30px; border-radius: 8px; 

200 box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; 

201 }} 

202 .score {{ 

203 font-size: 2.5em; font-weight: bold; color: {score_color}; 

204 }} 

205 .metadata {{ 

206 color: #6a737d; font-size: 0.9em; margin-top: 10px; 

207 }} 

208 .nav {{ 

209 background: white; padding: 20px; border-radius: 8px; 

210 box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; 

211 }} 

212 .nav a {{ 

213 background: #0366d6; color: white; padding: 10px 20px; 

214 text-decoration: none; border-radius: 6px; margin-right: 10px; 

215 display: inline-block; margin-bottom: 5px; 

216 }} 

217 .nav a:hover {{ 

218 background: #0256cc; 

219 }} 

220 .section {{ 

221 background: white; padding: 25px; border-radius: 8px; 

222 box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; 

223 }} 

224 .issues-list {{ 

225 max-height: 500px; overflow-y: auto; 

226 border: 1px solid #e1e4e8; border-radius: 6px; 

227 }} 

228 .issue {{ 

229 padding: 10px; border-bottom: 1px solid #e1e4e8; 

230 font-family: 'SFMono-Regular', 'Consolas', monospace; 

231 font-size: 0.9em; 

232 }} 

233 .issue:last-child {{ 

234 border-bottom: none; 

235 }} 

236 .issue.error {{ background: #ffeef0; }} 

237 .issue.warning {{ background: #fff8e1; }} 

238 .issue.convention {{ background: #e8f4fd; }} 

239 .issue.refactor {{ background: #f0f9ff; }} 

240 .summary {{ 

241 background: #f6f8fa; padding: 20px; border-radius: 6px; 

242 border-left: 4px solid #0366d6; font-family: monospace; 

243 white-space: pre-wrap; 

244 }} 

245 .stats {{ 

246 display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 

247 gap: 15px; margin-top: 20px; 

248 }} 

249 .stat-card {{ 

250 background: #f6f8fa; padding: 15px; border-radius: 6px; text-align: center; 

251 }} 

252 .stat-number {{ 

253 font-size: 1.8em; font-weight: bold; color: #0366d6; 

254 }} 

255 </style> 

256</head> 

257<body> 

258 <div class="container"> 

259 <div class="header"> 

260 <h1>Pylint Report</h1> 

261 <div class="score">Score: {pylint_score}/10</div> 

262 <div class="metadata"> 

263 <div><strong>Generated:</strong> {timestamp_ext}</div> 

264 <div><strong>Run ID:</strong> <a href="https://github.com/{github_repo}/actions/runs/{run_id}"> {run_id}</a></div> 

265 <div><strong>Branch:</strong> <a href="https://github.com/{github_repo}/tree/{ref_name}"> {ref_name}</a></div> 

266 <div><strong>Commit:</strong> <a href="https://github.com/{github_repo}/commit/{github_sha}"> {github_sha[:7]}</a></div> 

267 <div><strong>Repository:</strong> <a href="https://github.com/{github_repo}">{github_repo}</a></div> 

268 </div> 

269 </div> 

270  

271 <div class="section"> 

272 <h2 id="summary">Summary</h2> 

273 <div class="summary">{"".join(summary_lines)}</div> 

274  

275 <div class="stats"> 

276 <div class="stat-card"> 

277 <div class="stat-number">{len(issues)}</div> 

278 <div>Total Issues</div> 

279 </div> 

280 <div class="stat-card"> 

281 <div class="stat-number">{issue_counts["convention"]}</div> 

282 <div>Conventions</div> 

283 </div> 

284 <div class="stat-card"> 

285 <div class="stat-number">{issue_counts["warning"]}</div> 

286 <div>Warnings</div> 

287 </div> 

288 <div class="stat-card"> 

289 <div class="stat-number">{issue_counts["error"]}</div> 

290 <div>Errors</div> 

291 </div> 

292 <div class="stat-card"> 

293 <div class="stat-number">{issue_counts["refactor"]}</div> 

294 <div>Refactors</div> 

295 </div> 

296 </div> 

297 </div> 

298  

299 <!--div class="section"> 

300 <h2 id="issues">Issues Detail</h2> 

301 {issues_html} 

302 </div--> 

303  

304 <div class="section"> 

305 <h2 id="statistics">Full Report</h2> 

306 <details> 

307 <summary>Click to view complete pylint output</summary> 

308 <pre style="background: #f6f8fa; padding: 20px; border-radius: 6px; overflow-x: auto;">{html.escape(pylint_content)}</pre> 

309 </details> 

310 </div> 

311 </div> 

312 <footer style="text-align: center; margin: 40px 0; color: #6a737d;"> 

313 <p>Generated by GitHub Actions</p> 

314 </footer> 

315</body> 

316</html>""" 

317 

318 return html_content 

319 

320 

321# def write_report(html_content: str, output_file: str) -> None: 

322# """ 

323# Write HTML content to file. 

324# 

325# Args: 

326# html_content: The HTML content to write 

327# output_file: Path for the output HTML file 

328# 

329# Raises: 

330# IOError: If the file cannot be written. 

331# """ 

332# try: 

333# with open(output_file, "w", encoding="utf-8") as f: 

334# f.write(html_content) 

335# except IOError as e: 

336# raise IOError(f'Error writing output file "{output_file}": {e}') from e 

337 

338 

339def run_pylint_report( 

340 input_file: str, 

341 output_file: str, 

342 timestamp: str, 

343 run_id: str, 

344 ref_name: str, 

345 github_sha: str, 

346 github_repo: str, 

347) -> Tuple[int, Dict[str, int], float]: 

348 """ 

349 Main function to create HTML report from pylint output. 

350 

351 Args: 

352 input_file: Path to the pylint output text file 

353 output_file: Path for the generated HTML report 

354 timestamp: The timestampe from bash when pylint ran, in format YYYYMMDD_HHMMSS_UTC 

355 e.g., 20250815_211112_UTC 

356 run_id: GitHub Actions run ID 

357 ref_name: Git reference name (branch) 

358 github_sha: GitHub commit SHA 

359 github_repo: GitHub repository name 

360 

361 Returns: 

362 Tuple of (total_issues, issue_counts_dict) 

363 """ 

364 # Read the pylint output 

365 pylint_content: str = get_pylint_content(input_file) 

366 

367 # Parse pylint output 

368 issues, summary_lines = get_pylint_sections(pylint_content=pylint_content) 

369 

370 # Extract pylint score from summary lines 

371 pylint_score: str = get_score_from_summary(summary_lines) 

372 

373 # Generate HTML report 

374 html_content: str = get_report_html( 

375 pylint_content, 

376 issues, 

377 summary_lines, 

378 pylint_score, 

379 timestamp, 

380 run_id, 

381 ref_name, 

382 github_sha, 

383 github_repo, 

384 ) 

385 

386 # Write the HTML report 

387 write_report(html_content, output_file) 

388 

389 # Count issues by type 

390 issue_counts: Dict[str, int] = get_issue_counts(issues) 

391 

392 return len(issues), issue_counts, float(pylint_score) 

393 

394 

395def parse_arguments() -> argparse.Namespace: 

396 """ 

397 Parse command line arguments. 

398 

399 Returns: 

400 Parsed arguments namespace 

401 """ 

402 parser: argparse.ArgumentParser = argparse.ArgumentParser( 

403 description="Generate enhanced HTML report from pylint output", 

404 formatter_class=argparse.RawDescriptionHelpFormatter, 

405 epilog=""" 

406Example: 

407 python pylint_report.py \ 

408 --input_file pylint_output_20240101_120000_UTC.txt \ 

409 --output_file pylint_report.html \ 

410 --timestamp 20240101_120000_UTC \ 

411 --run_id 1234567890 \ 

412 --ref_name main \ 

413 --github_sha abc123def456 \ 

414 --github_repo owner/repo-name 

415 """, 

416 ) 

417 

418 parser.add_argument("--input_file", required=True, help="Input pylint output file") 

419 

420 parser.add_argument("--output_file", required=True, help="Output HTML report file") 

421 

422 parser.add_argument( 

423 "--timestamp", required=True, help="UTC timestamp, e.g., 20240101_120000_UTC" 

424 ) 

425 

426 parser.add_argument("--run_id", required=True, help="GitHub Actions run ID") 

427 

428 parser.add_argument( 

429 "--ref_name", required=True, help="Git reference name (branch name)" 

430 ) 

431 

432 parser.add_argument("--github_sha", required=True, help="GitHub commit SHA") 

433 

434 parser.add_argument( 

435 "--github_repo", required=True, help="GitHub repository name (owner/repo-name)" 

436 ) 

437 

438 return parser.parse_args() 

439 

440 

441def main() -> int: 

442 """ 

443 Main entry point for the script. 

444 

445 Returns: 

446 Exit code (0 for success, 1 for failure) 

447 """ 

448 args: argparse.Namespace = parse_arguments() 

449 

450 try: 

451 total_issues, issue_counts, pylint_score = run_pylint_report( 

452 args.input_file, 

453 args.output_file, 

454 args.timestamp, 

455 args.run_id, 

456 args.ref_name, 

457 args.github_sha, 

458 args.github_repo, 

459 ) 

460 

461 print(f"✅ Pylint HTML report generated: {args.output_file}") 

462 print(f"📊 Pylint score: {pylint_score}/10") 

463 print(f"🔍 Total issues found: {total_issues}") 

464 print(f" - Conventions: {issue_counts['convention']}") 

465 print(f" - Warnings: {issue_counts['warning']}") 

466 print(f" - Errors: {issue_counts['error']}") 

467 print(f" - Refactors: {issue_counts['refactor']}") 

468 

469 except FileNotFoundError: 

470 print(f"❌ Error: The input file '{args.input_file}' was not found.") 

471 return 1 

472 except IOError as e: 

473 print(f"❌ I/O error occurred: {e}") 

474 return 1 

475 except Exception as e: 

476 print(f"❌ An unexpected error occurred: {e}") 

477 return 1 

478 

479 return 0 # Success exit code 

480 

481 

482if __name__ == "__main__": 

483 sys.exit(main())