Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,25 @@ on:

concurrency:
group: ${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true

jobs:
typing:
name: Run type checking
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Install uv
uses: astral-sh/setup-uv@v7

- name: Install deps
run: uv sync --all-groups

- name: Run type checker
run: uv run basedpyright

test:
name: Run tests & display coverage
runs-on: ubuntu-latest
Expand All @@ -21,22 +38,14 @@ jobs:

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"

- name: Poetry caches
uses: actions/cache@v4
with:
path: |
~/.cache/
key: ${{ hashFiles('uv.lock') }}

- name: Install deps
run: uv sync --all-groups

- name: Run tests
run: uv run pytest
env:
PY_COLORS: 1
COVERAGE_COMMENT_E2E_GITHUB_TOKEN_USER_1: ${{ secrets.COVERAGE_COMMENT_E2E_GITHUB_TOKEN_USER_1 }}
COVERAGE_COMMENT_E2E_GITHUB_TOKEN_USER_2: ${{ secrets.COVERAGE_COMMENT_E2E_GITHUB_TOKEN_USER_2 }}
COVERAGE_COMMENT_E2E_ACTION_REF: ${{ github.sha }}
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/e2e-external-phase-2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,6 @@ jobs:

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"

- name: Poetry caches
uses: actions/cache@v4
Expand All @@ -106,6 +104,7 @@ jobs:
- name: Run end-to-end tests
run: uv run pytest tests/end_to_end
env:
PY_COLORS: 1
COVERAGE_COMMENT_E2E_GITHUB_TOKEN_USER_1: ${{ secrets.COVERAGE_COMMENT_E2E_GITHUB_TOKEN_USER_1 }}
COVERAGE_COMMENT_E2E_GITHUB_TOKEN_USER_2: ${{ secrets.COVERAGE_COMMENT_E2E_GITHUB_TOKEN_USER_2 }}
COVERAGE_COMMENT_E2E_ACTION_REF: ${{ steps.extract_commit.outputs.COMMIT_ID }}
Expand Down
33 changes: 24 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
default_language_version:
python: python3.12
python: python3.14

ci:
# Renovate updates the file. We can't disable pre-commit CI's autoupdate entirely
# but this is the least frequent we can make it.
autoupdate_schedule: quarterly
skip: [basedpyright]

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
Expand All @@ -18,18 +19,32 @@ repos:
- id: check-added-large-files

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.0
rev: v0.13.3
hooks:
- id: ruff
args: [--fix]
- id: ruff-format

- repo: local
- repo: https://github.com/DetachHead/basedpyright-prek-mirror
rev: 1.32.1
hooks:
- id: sync-pre-commit
name: Sync pre-commit hooks
language: python
entry: scripts/sync-pre-commit.py
- id: basedpyright

additional_dependencies:
- uv
- ruamel.yaml
- anyio==4.11.0
- certifi==2025.10.5
- coverage==7.10.7
- h11==0.16.0
- h2==4.3.0
- hpack==4.1.0
- httpcore==1.0.9
- httpx==0.28.1
- hyperframe==6.1.0
- idna==3.10
- jinja2==3.1.6
- markupsafe==3.0.3
- sniffio==1.3.1
- repo: https://github.com/ewjoachim/sync-pre-commit-with-uv
rev: 1.1.0
hooks:
- id: sync
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.14
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ You're welcome to contribute, though I can't promise the experience will be as s

### Things to know:

- Python3.12
- Python3.14
- Use [uv](https://docs.astral.sh/uv/)
- Launch tests with `pytest`, config is in setup.cfg
- `ruff` runs through `pre-commit`, so you can install hooks with `pre-commit install`.
Expand Down Expand Up @@ -78,6 +78,7 @@ In case "branch coverage" is enabled, the coverage rate is
`(covered_lines + covered_branches) / (total_lines + total_branches)`.
In order to display coverage rates, we need to round the values. Depending on
the situation, we either round to 0 or 2 decimal places. Rounding rules are:

- We always round down (truncate) the value.
- We don't display the trailing zeros in the decimal part (nor the decimal point
if the decimal part is 0).
2 changes: 1 addition & 1 deletion Dockerfile.build
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# - Dockerfile
# - .github/workflows/release.yml

FROM python:3.12-slim
FROM python:3.14-slim

RUN set -eux; \
apt-get update; \
Expand Down
56 changes: 28 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ jobs:

- name: Set up Python
id: setup-python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python_version }}

Expand Down Expand Up @@ -350,37 +350,37 @@ jobs:

The action makes available some data for downstream processing.

| Name | Description |
| --- | --- |
| Name | Description |
| -------------- | --------------------------------------------------------------------------------------------------- |
| `activity_run` | The type of activity that was run. One of `process_pr`, `post_comment`, `save_coverage_data_files`. |

All the following outputs are only available when running in PR mode.

| Name | Description |
| --- | --- |
| `comment_file_written` | A boolean indicating whether a comment file was written to `COMMENT_FILENAME` or not. |
| `new_covered_lines` | The number of covered lines in the pull request. |
| `new_num_statements` | The number of statements in the pull request. |
| `new_percent_covered` | The coverage percentage of the pull request. |
| `new_missing_lines` | The number of lines with missing coverage in the pull request. |
| `new_excluded_lines` | The number of excluded lines in the pull request. |
| `new_num_branches` | The number of branches in the pull request. |
| `new_num_partial_branches` | The number of partial branches in the pull request. |
| `new_covered_branches` | The number of covered branches in the pull request. |
| `new_missing_branches` | The number of branches with missing coverage in the pull request. |
| `reference_covered_lines` | The number of covered lines in the base branch. |
| `reference_num_statements` | The number of statements in the base branch. |
| `reference_percent_covered` | The coverage percentage of the base branch. |
| `reference_missing_lines` | The number of lines with missing coverage in the base branch. |
| `reference_excluded_lines` | The number of excluded lines in the base branch. |
| `reference_num_branches` | The number of branches in the base branch. |
| `reference_num_partial_branches` | The number of partial branches in the base branch. |
| `reference_covered_branches` | The number of covered branches in the base branch. |
| `reference_missing_branches` | The number of branches with missing coverage in the base branch. |
| `diff_total_num_lines` | The total number of lines in the diff. |
| `diff_total_num_violations` | The total number of lines with missing coverage in the diff. |
| `diff_total_percent_covered` | The coverage percentage of the diff. |
| `diff_num_changed_lines` | The number of changed lines in the diff. |
| Name | Description |
| -------------------------------- | ------------------------------------------------------------------------------------- |
| `comment_file_written` | A boolean indicating whether a comment file was written to `COMMENT_FILENAME` or not. |
| `new_covered_lines` | The number of covered lines in the pull request. |
| `new_num_statements` | The number of statements in the pull request. |
| `new_percent_covered` | The coverage percentage of the pull request. |
| `new_missing_lines` | The number of lines with missing coverage in the pull request. |
| `new_excluded_lines` | The number of excluded lines in the pull request. |
| `new_num_branches` | The number of branches in the pull request. |
| `new_num_partial_branches` | The number of partial branches in the pull request. |
| `new_covered_branches` | The number of covered branches in the pull request. |
| `new_missing_branches` | The number of branches with missing coverage in the pull request. |
| `reference_covered_lines` | The number of covered lines in the base branch. |
| `reference_num_statements` | The number of statements in the base branch. |
| `reference_percent_covered` | The coverage percentage of the base branch. |
| `reference_missing_lines` | The number of lines with missing coverage in the base branch. |
| `reference_excluded_lines` | The number of excluded lines in the base branch. |
| `reference_num_branches` | The number of branches in the base branch. |
| `reference_num_partial_branches` | The number of partial branches in the base branch. |
| `reference_covered_branches` | The number of covered branches in the base branch. |
| `reference_missing_branches` | The number of branches with missing coverage in the base branch. |
| `diff_total_num_lines` | The total number of lines in the diff. |
| `diff_total_num_violations` | The total number of lines with missing coverage in the diff. |
| `diff_total_percent_covered` | The coverage percentage of the diff. |
| `diff_num_changed_lines` | The number of changed lines in the diff. |

Usage may look like this

Expand Down
2 changes: 1 addition & 1 deletion coverage_comment/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from coverage_comment import main


def main_call(name):
def main_call(name: str):
if name == "__main__":
main.main()

Expand Down
2 changes: 1 addition & 1 deletion coverage_comment/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class ActivityNotFound(Exception):
def find_activity(
event_name: str,
is_default_branch: bool,
event_type: str,
event_type: str | None,
is_pr_merged: bool,
) -> str:
"""Find the activity to perform based on the event type and payload."""
Expand Down
32 changes: 16 additions & 16 deletions coverage_comment/coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import pathlib
from collections.abc import Sequence
from typing import Any

from coverage_comment import log, subprocess

Expand All @@ -21,20 +22,19 @@ class CoverageMetadata:
show_contexts: bool


class OutputMixin:
def as_output(self, prefix: str) -> dict:
data = dataclasses.asdict(self)
output = {}
for key, value in data.items():
if value is not None and not isinstance(value, dict):
output[f"{prefix}_{key}"] = (
float(value) if isinstance(value, decimal.Decimal) else value
)
return output
def as_output(obj: Any, prefix: str) -> dict[str, Any]:
data = dataclasses.asdict(obj)
output: dict[str, Any] = {}
for key, value in data.items():
if value is not None and not isinstance(value, dict):
output[f"{prefix}_{key}"] = (
float(value) if isinstance(value, decimal.Decimal) else value
)
return output


@dataclasses.dataclass(kw_only=True)
class CoverageInfo(OutputMixin):
class CoverageInfo:
covered_lines: int
num_statements: int
percent_covered: decimal.Decimal
Expand Down Expand Up @@ -88,7 +88,7 @@ def violation_lines(self) -> list[int]:


@dataclasses.dataclass(kw_only=True)
class DiffCoverage(OutputMixin):
class DiffCoverage:
total_num_lines: int
total_num_violations: int
total_percent_covered: decimal.Decimal
Expand All @@ -112,7 +112,7 @@ def compute_coverage(

def get_coverage_info(
merge: bool, coverage_path: pathlib.Path
) -> tuple[dict, Coverage]:
) -> tuple[dict[str, Any], Coverage]:
try:
if merge:
subprocess.run("coverage", "combine", path=coverage_path)
Expand Down Expand Up @@ -160,7 +160,7 @@ def generate_coverage_markdown(coverage_path: pathlib.Path) -> str:
)


def _make_coverage_info(data: dict) -> CoverageInfo:
def _make_coverage_info(data: dict[str, Any]) -> CoverageInfo:
"""Build a CoverageInfo object from a "summary" or "totals" key."""
return CoverageInfo(
covered_lines=data["covered_lines"],
Expand All @@ -180,7 +180,7 @@ def _make_coverage_info(data: dict) -> CoverageInfo:
)


def extract_info(data: dict, coverage_path: pathlib.Path) -> Coverage:
def extract_info(data: dict[str, Any], coverage_path: pathlib.Path) -> Coverage:
"""
{
"meta": {
Expand Down Expand Up @@ -246,7 +246,7 @@ def extract_info(data: dict, coverage_path: pathlib.Path) -> Coverage:
def get_diff_coverage_info(
added_lines: dict[pathlib.Path, list[int]], coverage: Coverage
) -> DiffCoverage:
files = {}
files: dict[pathlib.Path, FileDiffCoverage] = {}
total_num_lines = 0
total_num_violations = 0
num_changed_lines = 0
Expand Down
21 changes: 14 additions & 7 deletions coverage_comment/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
import pathlib
import shutil
import tempfile
from collections.abc import Callable
from typing import Protocol, TypedDict
from typing import Any, Protocol, TypedDict

import httpx

Expand Down Expand Up @@ -60,7 +59,7 @@ def apply(self):

def compute_files(
line_rate: decimal.Decimal,
raw_coverage_data: dict,
raw_coverage_data: dict[str, Any],
coverage_path: pathlib.Path,
minimum_green: decimal.Decimal,
minimum_orange: decimal.Decimal,
Expand Down Expand Up @@ -97,7 +96,9 @@ def compute_files(


def compute_datafile(
raw_coverage_data: dict, line_rate: decimal.Decimal, coverage_path: pathlib.Path
raw_coverage_data: dict[str, Any],
line_rate: decimal.Decimal,
coverage_path: pathlib.Path,
) -> str:
return json.dumps(
{
Expand All @@ -108,7 +109,7 @@ def compute_datafile(
)


def parse_datafile(contents) -> tuple[coverage.Coverage | None, decimal.Decimal]:
def parse_datafile(contents: str) -> tuple[coverage.Coverage | None, decimal.Decimal]:
file_contents = json.loads(contents)
coverage_rate = decimal.Decimal(str(file_contents["coverage"])) / decimal.Decimal(
"100"
Expand All @@ -128,7 +129,11 @@ class ImageURLs(TypedDict):
dynamic: str


def get_urls(url_getter: Callable) -> ImageURLs:
class URLGetter(Protocol):
def __call__(self, path: pathlib.Path) -> str: ...


def get_urls(url_getter: URLGetter) -> ImageURLs:
return {
"direct": url_getter(path=BADGE_PATH),
"endpoint": badge.get_endpoint_url(endpoint_url=url_getter(path=ENDPOINT_PATH)),
Expand All @@ -137,7 +142,9 @@ def get_urls(url_getter: Callable) -> ImageURLs:


def get_coverage_html_files(
*, coverage_path: pathlib.Path, gen_dir: pathlib.Path = pathlib.Path("/tmp")
*,
coverage_path: pathlib.Path,
gen_dir: pathlib.Path | None = None,
) -> ReplaceDir:
html_dir = pathlib.Path(tempfile.mkdtemp(dir=gen_dir))
coverage.generate_coverage_html_files(
Expand Down
Loading
Loading