-
Notifications
You must be signed in to change notification settings - Fork 106
Type completeness workflow #1042
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+520
−0
Merged
Changes from 5 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
633dc88
CHORE: Add workflow for coverage report.
genedan 50df01c
CHORE: Add additional report items.
genedan 87f97d5
CHORE: Add step summary.
genedan 1d0721c
FIX: Fix bugs and tidy up filenames.
genedan baf44ce
FIX: Apply bugbot fixes, remove external dependencies from report.
genedan 78fa10d
FIX: Apply bugbot fixes.
genedan 233c544
DOCS: Add annotations.
genedan b8cebdd
FIX: Apply bugbot fixes.
genedan f4abd33
FIX: Apply bugbot fixes.
genedan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| """ | ||
| Builds a Markdown summary comparing pyright `--verifytypes` reports for a | ||
| PR's base and head commits, for posting as a PR comment. | ||
|
|
||
| "Project" completeness is read directly from the head report's | ||
| typeCompleteness.completenessScore. "Patch" completeness is computed by | ||
| matching exported symbols between the base and head reports by their | ||
| dotted name: a symbol counts toward the patch if it is new in head, or if | ||
| its known/ambiguous/unknown status changed between base and head. This | ||
| avoids needing to map symbols to source line ranges, since pyright only | ||
| reports a file/line for symbols that already have a type problem. | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import json | ||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
| STATUS_ICON = {"known": "✅", "ambiguous": "⚠️", "unknown": "❌"} | ||
|
|
||
|
|
||
| def status_of(symbol: dict[str, Any]) -> str: | ||
| if symbol["isTypeKnown"]: | ||
| return "known" | ||
| if symbol["isTypeAmbiguous"]: | ||
| return "ambiguous" | ||
| return "unknown" | ||
|
|
||
|
|
||
| def load_type_completeness(report_path: Path) -> dict[str, Any]: | ||
| with report_path.open() as f: | ||
| data = json.load(f) | ||
| return data["typeCompleteness"] | ||
|
|
||
|
|
||
| def exported_symbols(type_completeness: dict[str, Any]) -> dict[str, dict[str, Any]]: | ||
| return {s["name"]: s for s in type_completeness["symbols"] if s["isExported"]} | ||
|
|
||
|
|
||
| def other_symbol_counts(type_completeness: dict[str, Any]) -> dict[str, int]: | ||
| other = type_completeness["otherSymbolCounts"] | ||
| return { | ||
| "known": other["withKnownType"], | ||
| "ambiguous": other["withAmbiguousType"], | ||
| "unknown": other["withUnknownType"], | ||
| } | ||
|
|
||
|
|
||
| def counts_by_status(symbols: list[dict[str, Any]]) -> dict[str, int]: | ||
| counts = {"known": 0, "ambiguous": 0, "unknown": 0} | ||
| for s in symbols: | ||
| counts[status_of(s)] += 1 | ||
| return counts | ||
|
|
||
|
|
||
| def completeness_pct(counts: dict[str, int]) -> float: | ||
| total = sum(counts.values()) | ||
| return 100.0 * counts["known"] / total if total else 0.0 | ||
|
|
||
|
|
||
| def render_counts_table(rows: list[tuple[str, dict[str, int]]]) -> str: | ||
| lines = ["| | Known | Ambiguous | Unknown | Total |", "|---|---|---|---|---|"] | ||
| for label, counts in rows: | ||
| total = sum(counts.values()) | ||
| lines.append( | ||
| f"| {label} | {counts['known']} | {counts['ambiguous']} | " | ||
| f"{counts['unknown']} | {total} |" | ||
| ) | ||
| return "\n".join(lines) | ||
|
|
||
|
|
||
| def render_patch_detail( | ||
| patch_names: list[str], | ||
| base: dict[str, dict[str, Any]], | ||
| head: dict[str, dict[str, Any]], | ||
| ) -> str: | ||
| lines = ["| Symbol | Status | Change |", "|---|---|---|"] | ||
| for name in sorted(patch_names): | ||
| head_status = status_of(head[name]) | ||
| icon = STATUS_ICON[head_status] | ||
| if name not in base: | ||
| change = "new" | ||
| else: | ||
| base_status = status_of(base[name]) | ||
| change = f"changed (was {STATUS_ICON[base_status]} {base_status})" | ||
| lines.append(f"| `{name}` | {icon} {head_status} | {change} |") | ||
| return "\n".join(lines) | ||
|
|
||
|
|
||
| def build_summary(base_path: Path, head_path: Path, run_url: str | None = None) -> str: | ||
| base_tc = load_type_completeness(base_path) | ||
| head_tc = load_type_completeness(head_path) | ||
| base = exported_symbols(base_tc) | ||
| head = exported_symbols(head_tc) | ||
|
|
||
| project_counts = counts_by_status(list(head.values())) | ||
| project_pct = completeness_pct(project_counts) | ||
| other_counts = other_symbol_counts(head_tc) | ||
|
|
||
| patch_names = [ | ||
| name | ||
| for name, symbol in head.items() | ||
| if name not in base or status_of(base[name]) != status_of(symbol) | ||
| ] | ||
|
|
||
| sections = [ | ||
| "## Pyright Type Completeness", | ||
| "", | ||
| ] | ||
| if run_url: | ||
| sections += [ | ||
| f"[View the full `pyright --verifytypes` output for this commit]({run_url})", | ||
| "", | ||
| ] | ||
| sections += [ | ||
| f"**Project (full `chainladder` package, at this PR's head):** " | ||
| f"{project_pct:.1f}% of exported symbols fully typed " | ||
| f"({project_counts['known']} / {sum(project_counts.values())})", | ||
| "", | ||
| render_counts_table([("Project (head)", project_counts)]), | ||
| "", | ||
| f"Other symbols referenced but not exported by `chainladder`: " | ||
| f"{sum(other_counts.values())}", | ||
| "", | ||
| render_counts_table([("Other (head)", other_counts)]), | ||
| "", | ||
| "Symbols without documentation:", | ||
| f"- Functions without docstring: {head_tc['missingFunctionDocStringCount']}", | ||
| f"- Functions without default param: {head_tc['missingDefaultParamCount']}", | ||
| f"- Classes without docstring: {head_tc['missingClassDocStringCount']}", | ||
| "", | ||
| ] | ||
|
|
||
| if patch_names: | ||
| patch_counts = counts_by_status([head[n] for n in patch_names]) | ||
| patch_pct = completeness_pct(patch_counts) | ||
| sections += [ | ||
| f"**Patch (exported symbols added or changed by this PR):** " | ||
| f"{patch_pct:.1f}% fully typed " | ||
| f"({patch_counts['known']} / {sum(patch_counts.values())})", | ||
| "", | ||
| render_counts_table([("Patch", patch_counts)]), | ||
| "", | ||
| "<details>", | ||
| "<summary>Patch symbol details</summary>", | ||
| "", | ||
| render_patch_detail(patch_names, base, head), | ||
| "", | ||
| "</details>", | ||
| ] | ||
| else: | ||
| sections += [ | ||
| "**Patch (exported symbols added or changed by this PR):** " | ||
| "no exported symbol type-completeness changes detected.", | ||
| ] | ||
|
|
||
| return "\n".join(sections) | ||
|
|
||
|
|
||
| def main() -> None: | ||
| parser = argparse.ArgumentParser(description=__doc__) | ||
| parser.add_argument("--base", required=True, type=Path) | ||
| parser.add_argument("--head", required=True, type=Path) | ||
| parser.add_argument("--output", required=True, type=Path) | ||
| parser.add_argument( | ||
| "--run-url", | ||
| default=None, | ||
| help="Link to the workflow run, e.g. for its step summary.", | ||
| ) | ||
| args = parser.parse_args() | ||
|
|
||
| summary = build_summary(args.base, args.head, run_url=args.run_url) | ||
| args.output.write_text(summary) | ||
| print(summary) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| #!/usr/bin/env bash | ||
| # Appends a pyright --verifytypes text report to $GITHUB_STEP_SUMMARY, | ||
| # wrapped in a collapsible <details> block. GitHub caps each step's summary | ||
| # at 1MiB and silently drops the whole upload if exceeded, so the report is | ||
| # truncated to a safe byte budget with a note when that happens, rather than | ||
| # risking the entire block vanishing. | ||
| set -euo pipefail | ||
|
|
||
| report_file="$1" | ||
| label="$2" | ||
| max_bytes=900000 | ||
|
|
||
| total_bytes="$(wc -c < "$report_file")" | ||
|
|
||
| { | ||
| echo "<details><summary>Full pyright --verifytypes output (${label})</summary>" | ||
| echo "" | ||
| echo '```text' | ||
| head -c "$max_bytes" "$report_file" | ||
| if [ "$total_bytes" -gt "$max_bytes" ]; then | ||
| echo "" | ||
| echo "... (truncated: ${total_bytes} bytes total, GITHUB_STEP_SUMMARY caps each step at 1MiB)" | ||
| fi | ||
| echo '```' | ||
| echo "</details>" | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
|
cursor[bot] marked this conversation as resolved.
|
||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| name: Pyright Type Completeness | ||
|
|
||
| on: | ||
| pull_request: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| jobs: | ||
| typecompleteness: | ||
| name: pyright --verifytypes | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| ref: ${{ github.event.pull_request.head.sha }} | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Install uv | ||
| uses: astral-sh/setup-uv@v7 | ||
| with: | ||
| version: "latest" | ||
| python-version: "3.14" | ||
|
|
||
| - name: Install dependencies | ||
| run: uv sync --extra dev | ||
|
|
||
| - name: Run pyright --verifytypes on PR head | ||
| run: | | ||
| uv run pyright --outputjson --verifytypes chainladder --ignoreexternal > head.json || true | ||
| uv run pyright --verifytypes chainladder --ignoreexternal > head.txt || true | ||
| bash .github/scripts/type_completeness_output.sh head.txt "PR head" | ||
|
|
||
| - name: Run pyright --verifytypes on PR base | ||
| env: | ||
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | ||
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | ||
| run: | | ||
| git fetch origin "$BASE_SHA" | ||
| MERGE_BASE="$(git merge-base "$BASE_SHA" "$HEAD_SHA")" | ||
| git checkout "$MERGE_BASE" -- chainladder | ||
| uv run pyright --outputjson --verifytypes chainladder --ignoreexternal > base.json || true | ||
| uv run pyright --verifytypes chainladder --ignoreexternal > base.txt || true | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| git checkout "$HEAD_SHA" -- chainladder | ||
| bash .github/scripts/type_completeness_output.sh base.txt "PR base" | ||
|
|
||
| - name: Build summary | ||
| env: | ||
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | ||
| run: | | ||
| uv run python .github/scripts/type_completeness.py \ | ||
| --base base.json \ | ||
| --head head.json \ | ||
| --run-url "$RUN_URL" \ | ||
| --output summary.md | ||
|
|
||
| - name: Save PR number | ||
| run: echo "${{ github.event.pull_request.number }}" > pr_number.txt | ||
|
|
||
| # This workflow runs on pull_request, so on PRs from forks it only ever | ||
| # gets a read-only GITHUB_TOKEN and can't post a comment directly. The | ||
| # artifact is picked up by type_completeness_comment.yml, which runs on | ||
| # workflow_run (always with the base repo's write-capable token, without | ||
| # ever checking out or executing the fork's code) and posts the comment. | ||
| - name: Upload summary artifact | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: type-completeness-summary | ||
| path: | | ||
| summary.md | ||
| pr_number.txt | ||
| retention-days: 1 | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| name: Pyright Type Completeness Comment | ||
|
|
||
| # Split out from type_completeness.yml so the comment can be posted with a | ||
| # write-capable token even for PRs from forks. workflow_run always executes | ||
| # using the workflow file from the default branch, with the base repo's own | ||
| # token, regardless of whether the triggering run came from a fork - and | ||
| # this workflow never checks out or executes the fork's code, only the | ||
| # already-computed summary.md/pr_number.txt artifact it produced. | ||
| on: | ||
| workflow_run: | ||
| workflows: ["Pyright Type Completeness"] | ||
| types: [completed] | ||
|
|
||
| permissions: | ||
| pull-requests: write | ||
|
|
||
| jobs: | ||
| comment: | ||
| if: github.event.workflow_run.conclusion == 'success' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Download summary artifact | ||
| uses: actions/download-artifact@v4 | ||
| with: | ||
| name: type-completeness-summary | ||
| run-id: ${{ github.event.workflow_run.id }} | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Read PR number | ||
| id: pr | ||
| run: echo "number=$(cat pr_number.txt)" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Comment on PR | ||
| uses: marocchino/sticky-pull-request-comment@v3 | ||
| with: | ||
| header: pyright-typecompleteness | ||
| number_force: ${{ steps.pr.outputs.number }} | ||
| path: summary.md |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.