[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/python/python-docs-fa/3.15/scripts/contributor_chart.py [Back]  [Original]

#!/usr/bin/env python3
"""Generate a top-N bar chart of translated counts and refresh README.

The chart uses the translated totals recorded in ``TEAM.md``
(``teammd_totals``), which the nightly maintenance run keeps in sync with
git-blame attribution while preserving Transifex-era and restored counts, and
draws a pastel bar chart saved to ``reports/contributor_stats_latest.png``
(fixed filename, overwritten each run). The README block between the
``STATS_START``/``STATS_END`` markers is refreshed with a new date, but only
when its content (counts or chart path) actually changed, so a nightly run
with no real changes doesn't create a commit.

Requires: ``pip install polib matplotlib``

Usage::

    python3 scripts/contributor_chart.py              # top 10 contributors
    python3 scripts/contributor_chart.py --top-n 15   # different cutoff
    python3 scripts/contributor_chart.py --dry-run    # report only, no writes
"""
import argparse
import datetime
import re
import sys
from pathlib import Path

import matplotlib

matplotlib.use("Agg")  # must be selected before pyplot is imported

import matplotlib.pyplot as plt  # noqa: E402
import matplotlib.ticker as mticker  # noqa: E402

REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))

from team_stats import teammd_totals  # noqa: E402

CHART_DIR = REPO_ROOT / "reports"
CHART_PREFIX = "contributor_stats_"
CHART_FILENAME = f"{CHART_PREFIX}latest.png"

README_PATH = REPO_ROOT / "README.md"
STATS_START = ""
STATS_END = ""

DATE_RE = re.compile(r"\d{4}-\d{2}-\d{2}")

PASTEL_COLORS = [
    "#A6C7E8",
    "#B5EAD7",
    "#FFDFD3",
    "#FFF1AC",
    "#E2D1F9",
    "#FFD7BA",
    "#FFABAB",
    "#C7F0DB",
    "#FFDAC1",
    "#C7CEEA",
]


def chart_data(counts: dict[str, int], top_n: int) -> list[tuple[str, int]]:
    """Return contributors with a positive count, sorted, trimmed to top_n."""
    eligible = [
        (name, count)
        for name, count in counts.items()
        if name != "(unassigned)" and count > 0
    ]
    return sorted(eligible, key=lambda item: item[1], reverse=True)[:top_n]


def draw_chart(data: list[tuple[str, int]], top_n: int) -> None:
    usernames = [name for name, _ in data]
    totals = [count for _, count in data]
    colors = [PASTEL_COLORS[i % len(PASTEL_COLORS)] for i in range(len(usernames))]

    plt.figure(figsize=(12, 7))
    bars = plt.bar(usernames, totals, color=colors)

    title = "User Contributions"
    if top_n:
        title += f" (Top {top_n})"
    plt.title(title)
    plt.xlabel("Username")
    plt.ylabel("Translated Count")
    plt.xticks(rotation=45, ha="right")

    plt.gca().yaxis.set_major_locator(mticker.MaxNLocator(integer=True))
    plt.grid(axis="y", linestyle="--", alpha=0.7)

    for bar in bars:
        height = bar.get_height()
        if height > 0:
            plt.text(
                bar.get_x() + bar.get_width() / 2.0,
                height + 0.1,
                f"{int(height)}",
                ha="center",
                fontweight="bold",
            )
    plt.tight_layout()


def refresh_readme(
    chart_file: Path,
    dry_run: bool,
    top_contribs: list[tuple[str, int]],
) -> bool:
    text = README_PATH.read_text(encoding="utf-8")
    date_iso = datetime.date.today().isoformat()
    rel = chart_file.relative_to(REPO_ROOT).as_posix()
    alt = "   " + " ".join(
        f"{name} {count}" for name, count in top_contribs
    )
    block = (
        f"{STATS_START}\n"
        "###  \n"
        f"![{alt}]({rel})\n"
        f"(: {date_iso})\n"
        f"{STATS_END}"
    )
    pattern = re.compile(
        re.escape(STATS_START) + ".*?" + re.escape(STATS_END),
        flags=re.DOTALL,
    )
    match = pattern.search(text)
    if not match:
        print(f"  error: could not find {STATS_START}/{STATS_END} in {README_PATH}")
        return False

    # Ignore the date when deciding whether anything changed; otherwise the
    # block differs every day and the nightly run always makes a commit.
    if DATE_RE.sub("", match.group(0)) == DATE_RE.sub("", block):
        print("  README block unchanged (ignoring date)")
        return False

    new_text = pattern.sub(lambda _: block, text)
    if dry_run:
        print("[dry-run] would update README stats block")
    else:
        README_PATH.write_text(new_text, encoding="utf-8")
        print(f"Updated README stats block to {rel} ({date_iso})")
    return True


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument(
        "--top-n",
        type=int,
        default=10,
        help="limit the chart to the top N contributors (default: 10)",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="print what would be done without changing files",
    )
    args = parser.parse_args()

    counts = teammd_totals()
    data = chart_data(counts, args.top_n)
    if not data:
        print("No contributor data to generate chart.")
        return

    out_path = CHART_DIR / CHART_FILENAME

    draw_chart(data, args.top_n)
    if args.dry_run:
        print(f"[dry-run] would save chart to {out_path}")
    else:
        CHART_DIR.mkdir(parents=True, exist_ok=True)
        # Software=None drops the matplotlib version string from the PNG
        # metadata, so upgrading matplotlib alone doesn't change the file.
        plt.savefig(out_path, metadata={"Software": None})
        print(f"Saved user contributions chart to {out_path}")
    plt.close()

    refresh_readme(out_path, args.dry_run, data)


if __name__ == "__main__":
    main()

Web Proxy Viewer  |  New URL  |  Original Page