Skip to content

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

  1. Uses sheridan-iceberg to extract the public API surface at two points in git history
  2. Diffs those two surfaces to find what was added or removed
  3. 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

pip install sheridan-diffract

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!:
Detected: fix

No public API changes detected.

Suggested commit prefix: fix:

Scopes are passed through to the suggestion — if your commit message includes (parser), the suggested prefix will too:

Suggested commit prefix: feat(parser):

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
- make sure to 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:

diffract: commit type mismatch
  written:  fix(parser):
  detected: feat(parser):

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):

src = "python/src"

pyproject.toml:

[tool.diffract]
src = "python/src"

Priority: explicit --src flag → diffract.tomlpyproject.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 event
  • head.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 suggest feat(parser): on mismatch

What it does not do

  • diffract has no AST logic of its own. It delegates all API surface extraction to sheridan-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.