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
« 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)
5This module extracts pylint output and generates a custom HTML report.
6"""
8import argparse
9import html
10import re
11import sys
13from typing import Dict, List, Tuple
16from rattlesnake.cicd.utilities import (
17 get_score_color_lint,
18 extend_timestamp,
19 write_report,
20)
23def get_pylint_content(input_file: str) -> str:
24 """
25 Read pylint output from file.
27 Args:
28 input_file: Path to the pylint output file
30 Returns:
31 Content of the pylint output file
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
47def get_pylint_sections(pylint_content: str) -> Tuple[List[str], List[str]]:
48 """
49 Parse pylint output to extract issues and summary.
51 Args:
52 pylint_content: Raw pylint output content
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] = []
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)
74 return issues, summary_lines
77def get_score_from_summary(summary_lines: List[str]) -> str:
78 """
79 Extract the pylint score from summary lines.
81 Args:
82 summary_lines: List of summary lines from pylint output
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"
95def get_issue_counts(issues: List[str]) -> Dict[str, int]:
96 """
97 Count issues by type (convention, warning, error, refactor).
99 Args:
100 issues: List of pylint issue strings
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)])
110 return {
111 "convention": convention_count,
112 "warning": warning_count,
113 "error": error_count,
114 "refactor": refactor_count,
115 }
118def get_issues_list_html(issues: List[str]) -> str:
119 """
120 Create HTML for the issues section.
122 Args:
123 issues: List of pylint issues
125 Returns:
126 HTML string for issues section
127 """
128 if not issues:
129 return "<p>No issues found! 🎉</p>"
131 issues_list: List[str] = []
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>')
147 return f'<div class="issues-list">{"".join(issues_list)}</div>'
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.
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
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)
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>
271 <div class="section">
272 <h2 id="summary">Summary</h2>
273 <div class="summary">{"".join(summary_lines)}</div>
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>
299 <!--div class="section">
300 <h2 id="issues">Issues Detail</h2>
301 {issues_html}
302 </div-->
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>"""
318 return html_content
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
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.
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
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)
367 # Parse pylint output
368 issues, summary_lines = get_pylint_sections(pylint_content=pylint_content)
370 # Extract pylint score from summary lines
371 pylint_score: str = get_score_from_summary(summary_lines)
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 )
386 # Write the HTML report
387 write_report(html_content, output_file)
389 # Count issues by type
390 issue_counts: Dict[str, int] = get_issue_counts(issues)
392 return len(issues), issue_counts, float(pylint_score)
395def parse_arguments() -> argparse.Namespace:
396 """
397 Parse command line arguments.
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 )
418 parser.add_argument("--input_file", required=True, help="Input pylint output file")
420 parser.add_argument("--output_file", required=True, help="Output HTML report file")
422 parser.add_argument(
423 "--timestamp", required=True, help="UTC timestamp, e.g., 20240101_120000_UTC"
424 )
426 parser.add_argument("--run_id", required=True, help="GitHub Actions run ID")
428 parser.add_argument(
429 "--ref_name", required=True, help="Git reference name (branch name)"
430 )
432 parser.add_argument("--github_sha", required=True, help="GitHub commit SHA")
434 parser.add_argument(
435 "--github_repo", required=True, help="GitHub repository name (owner/repo-name)"
436 )
438 return parser.parse_args()
441def main() -> int:
442 """
443 Main entry point for the script.
445 Returns:
446 Exit code (0 for success, 1 for failure)
447 """
448 args: argparse.Namespace = parse_arguments()
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 )
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']}")
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
479 return 0 # Success exit code
482if __name__ == "__main__":
483 sys.exit(main())