sheridan-diffract¶
Detects how a Python package's public API changed between two git commits and classifies that change as a conventional commit type.
Most semver tools trust the developer to classify their own changes correctly. diffract verifies that classification against the actual diff — catching the common case where someone writes fix: but actually removed a public function.
How it works¶
- Uses sheridan-iceberg to extract the public API surface at two points in git history
- Diffs those two surfaces to find what was added or removed
- Maps the diff to a conventional commit classification:
| Change | Commit type |
|---|---|
| Public name removed | feat! (breaking) |
| Public name added | feat |
| Only internal/private changes | refactor |
| No changes detected | fix |
Installation¶
Usage¶
CLI¶
# Compare the last two commits (default)
diffract
# Compare specific refs
diffract HEAD~3 HEAD
# Custom source directory (default: src/)
diffract --src lib/
# Emit JSON for CI consumption
diffract --json
# Exit non-zero if a breaking change is detected (for CI gates)
diffract --exit-code
Exit codes with --exit-code:
- 0 — no API surface changes
- 1 — breaking change (public name removed)
- 2 — non-breaking API change (public name added)
- 3 — error (git or surface extraction failure)
Example output:
Detected: feat! (breaking change)
Removed public names:
sheridan.diffract.enums:
- OldHelper
Suggested commit prefix: feat!:
Scopes are passed through to the suggestion — if your commit message includes (parser), the suggested prefix will too:
Python API¶
from sheridan.diffract import check
result = check(base_ref="v1.2.0", head_ref="HEAD")
print(result.commit_type) # e.g. "feat!"
print(result.summary) # human-readable description
print(result.diff.removed) # tuple of NameChange objects
print(result.diff.added)
# JSON-serialisable dict for CI output
import json
print(json.dumps(result.to_dict(), indent=2))
To compare HEAD against the staging area (what the next commit will contain) — the same comparison the pre-commit hook uses:
from sheridan.diffract import check_staged
result = check_staged()
print(result.commit_type) # reflects what is staged, not the last commit
Validate commit messages with pre-commit¶
diffract ships a commit-msg hook that rejects commits whose conventional commit type doesn't match the detected API change — catching fix: when you actually removed a public name.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/sheridan/diffract
rev: v<VERSION>
hooks:
- id: diffract-validate
pre-commit install --hook-type commit-msg first
The hook compares HEAD against the git staging area (not HEAD~1 → HEAD), so it correctly detects what is about to be committed rather than what was committed last. Explicit BASE/HEAD refs can be added to override this (see the GitHub Actions example below).
Scopes are preserved in all output — if you write fix(parser): …, the mismatch message will show fix(parser): as written and feat(parser): as the suggested replacement:
Non-conventional commit types (docs:, chore:, test:, etc.) are never blocked.
Configuration¶
If your source code isn't in src/, tell diffract once in a config file rather than repeating it on every command:
diffract.toml (takes precedence):
pyproject.toml:
Priority: explicit --src flag → diffract.toml → pyproject.toml → default (src/).
As a GitHub Actions check¶
Validate the PR title against detected API changes — the CI equivalent of the pre-commit hook. The refs to diff are the PR branch HEAD versus the target branch HEAD:
# .github/workflows/diffract.yml
name: diffract
on:
pull_request:
types: [opened, synchronize, reopened, edited] # 'edited' catches title-only changes
jobs:
api-change-check:
name: Validate PR title against API changes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required to access both commit SHAs
- name: Install diffract
run: pip install sheridan-diffract
- name: Validate PR title type matches API change
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
printf '%s' "$PR_TITLE" > /tmp/pr-title.txt
diffract \
${{ github.event.pull_request.base.sha }} \
${{ github.event.pull_request.head.sha }} \
--validate-msg-file /tmp/pr-title.txt
base.sha— HEAD of the target branch (e.g.main) at the time of the PR eventhead.sha— HEAD of the PR branch- Non-conventional title prefixes (
docs:,chore:,test:, etc.) are never blocked - Scopes are passed through: a title of
feat(parser): …will suggestfeat(parser):on mismatch
What it does not do¶
diffracthas no AST logic of its own. It delegates all API surface extraction tosheridan-iceberg.- It compares public names only (additions and removals). Signature changes (argument renames, return type changes) are not currently detected — iceberg returns name lists, not signatures.
- It does not modify commits or rewrite history. It only reports.
Development¶
task install # install all dependencies
task check # lint + format + typecheck + tests (must all pass)
task test # pytest with coverage (≥90% required)
See CONTRIBUTING.md for the full workflow.