Created
March 10, 2026 14:07
-
-
Save certik/327b417b53677939cc7fe853f77ebc13 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Compute PR-per-person statistics from LFortran release notes. | |
| Usage: | |
| python pr_stats.py [VERSION] | |
| VERSION e.g. "0.61.0" or "v0.61.0". Omit for the latest release. | |
| Looks up each contributor's real name via the GitHub API. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import re | |
| import sys | |
| import json | |
| import urllib.request | |
| import urllib.error | |
| from collections import Counter | |
| REPO = "lfortran/lfortran" | |
| def fetch_release_body(version: str | None = None) -> tuple[str, str]: | |
| """Fetch release notes markdown from GitHub. Returns (tag_name, body).""" | |
| if version: | |
| tag = version if version.startswith("v") else f"v{version}" | |
| url = f"https://api.github.com/repos/{REPO}/releases/tags/{tag}" | |
| else: | |
| url = f"https://api.github.com/repos/{REPO}/releases/latest" | |
| headers = {"User-Agent": "lfortran-stats"} | |
| token = os.environ.get("GITHUB_TOKEN") | |
| if token: | |
| headers["Authorization"] = f"token {token}" | |
| req = urllib.request.Request(url, headers=headers) | |
| try: | |
| with urllib.request.urlopen(req) as resp: | |
| data = json.loads(resp.read()) | |
| except urllib.error.HTTPError as e: | |
| sys.exit(f"Error fetching release: {e.code} {e.reason}\n" | |
| f" URL: {url}\n" | |
| f" Hint: set GITHUB_TOKEN env var to avoid rate limits") | |
| return data["tag_name"], data.get("body", "") | |
| def parse_prs(text: str) -> Counter: | |
| """Return a Counter mapping GitHub username -> number of PRs.""" | |
| counts: Counter = Counter() | |
| for line in text.splitlines(): | |
| m = re.search(r"by @([A-Za-z0-9_-]+(?:\[bot\])?)", line) | |
| if m: | |
| counts[m.group(1)] += 1 | |
| return counts | |
| def lookup_name(username: str) -> str: | |
| """Fetch the user's real name from the GitHub API.""" | |
| if username.endswith("[bot]"): | |
| return username | |
| url = f"https://api.github.com/users/{username}" | |
| headers = {"User-Agent": "lfortran-stats"} | |
| token = os.environ.get("GITHUB_TOKEN") | |
| if token: | |
| headers["Authorization"] = f"token {token}" | |
| req = urllib.request.Request(url, headers=headers) | |
| try: | |
| with urllib.request.urlopen(req) as resp: | |
| data = json.loads(resp.read()) | |
| return data.get("name") or username | |
| except Exception: | |
| return username | |
| def main(): | |
| version = sys.argv[1] if len(sys.argv) > 1 else None | |
| tag, body = fetch_release_body(version) | |
| print(f"Release: {tag}\n") | |
| counts = parse_prs(body) | |
| # Look up real names | |
| names: dict[str, str] = {} | |
| for username in sorted(counts): | |
| names[username] = lookup_name(username) | |
| # Build rows sorted by PR count descending, then alphabetically | |
| rows = sorted(counts.items(), key=lambda x: (-x[1], x[0])) | |
| total = sum(counts.values()) | |
| # Compute column widths | |
| name_col = [] | |
| for username, count in rows: | |
| real = names[username] | |
| if real != username: | |
| label = f"{real} (@{username})" | |
| else: | |
| label = f"@{username}" | |
| name_col.append(label) | |
| max_name = max(len(n) for n in name_col) | |
| max_name = max(max_name, len("Total")) | |
| count_width = len(str(total)) | |
| # Print table | |
| hdr_name = "Contributor" | |
| hdr_prs = "PRs" | |
| max_name = max(max_name, len(hdr_name)) | |
| count_width = max(count_width, len(hdr_prs)) | |
| print(f"{'─' * (max_name + count_width + 5)}") | |
| print(f" {hdr_name:<{max_name}} {hdr_prs:>{count_width}}") | |
| print(f"{'─' * (max_name + count_width + 5)}") | |
| for label, (username, count) in zip(name_col, rows): | |
| print(f" {label:<{max_name}} {count:>{count_width}}") | |
| print(f"{'─' * (max_name + count_width + 5)}") | |
| print(f" {'Total':<{max_name}} {total:>{count_width}}") | |
| print(f"{'─' * (max_name + count_width + 5)}") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment