Skip to main content

Acquit

CI PyPI version Python versions License

Provably skip unaffected pytest tests on every pull request. Static analysis, fail-closed, with evidence for every skip.

Status: alpha. The engine works: on real pull requests acquit makes selective decisions, and every skip ships with a machine-checkable witness that is re-verified by replay before pytest honors it. Every failure mode still converges on "run everything".

The CLI is on PyPI (pip install acquit==0.3.0), so the analysis itself pins cleanly.

Both the CLI and the GitHub Action pin cleanly to v0.3.0.

Re-export narrowing for fat package inits exists and ships disabled: it survived an adversarial break-and-fix cycle before anyone relied on it, and its measured applicability where it engages is 6.7 percent of submodules (docs/study.md has the full story).

What it does

Acquit builds a dependency graph of your repository from import statements alone (it never executes your code), then works out which test files a pull request cannot possibly affect. Those tests are skipped, and every skip comes with a machine-checkable witness: the test's import closure and proof that it does not intersect the changed files.

Anything Acquit cannot reason about (dynamic imports, changed conftest files, dependency bumps, data files, and a documented list of other triggers) makes it fall back to running the full suite. It can only be wrong in the safe direction.

Measured on real history

Acquit replayed 198 merged PRs from four real repositories, running each full suite at the base and head commits and checking every skip against the observed outcomes.

Repo Merged PRs replayed Unsafe skips Median suite time skipped when selective Selective out of the box Selective with a docs config
pallets/click 10 0 99.6% 20.0% (2/10) 80.0% (8/10)
pallets/flask 41 0 - 0.0% (0/41) 63.4% (26/41)
Textualize/rich 64 0 93.4% 6.3% (4/64) 64.1% (41/64)
encode/httpx 83 0 97.5% 4.8% (4/83) 53.0% (44/83)
Total 198 0 - 5.1% (10/198) 60.1% (119/198)

The docs-config column is computed from the recorded findings for flask, rich, and httpx; click's docs-config counterfactual was re-replayed for real, with every additional selective decision verified (see docs/study.md).

The out-of-the-box share is low because acquit is fail-closed: any PR it cannot fully prove safe runs the whole suite. Docs and changelog churn (rule R001) is by far the dominant blocker; the docs-config column counts PRs where it was the only one, and the sticky PR comment suggests the exact assume_inert entry to lift it. Every number above regenerates from committed manifests via acquit-study; see study/README.md. The full writeup, including the parts that went wrong, is in docs/study.md.

Try it on a PR

One complete workflow file, ready to paste:

name: tests

on: pull_request

permissions:
  contents: read
  pull-requests: write

jobs:
  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          # The base ref must exist locally; shallow clones always fall back to run-all.
          fetch-depth: 0

      - uses: rajeev-chaurasia/acquit@v0.3.0 # x-release-please-version
        with:
          mode: report

      - uses: actions/setup-python@v7
        with:
          python-version: "3.12"

      # The pytest plugin ships in the acquit package. Enforce mode silently
      # changes nothing unless acquit is importable in the environment that
      # runs pytest.
      - run: pip install acquit
      - run: pip install -e . pytest

      - run: pytest

Report mode never skips a test: the action analyzes the diff, verifies the evidence with a replay, posts a sticky PR comment explaining the decision, and sets outputs. mode: canary also runs everything, but verifies the selection against reality: every would-be-skipped test still executes, a failure among them raises a loud alarm, and it is the recommended step between report and enforce. Switch to mode: enforce once you trust what the comments say, and the pytest plugin will skip the provably unaffected files; any unrecognized mode value means report. The pip install acquit line (or its uv equivalent, uv pip install acquit) is what puts the plugin into the environment that runs pytest.

Run it locally

The whole loop runs without CI:

pip install acquit                # or: uv pip install acquit
acquit select --base origin/main
ACQUIT_SELECTION_FILE=acquit-selection.json pytest

acquit select --base origin/main compares the working tree against the base ref and writes three documents: the report (acquit-report.json), the selection (acquit-selection.json), and the witnesses (acquit-witnesses.json). The defaults land in the current directory and work as they are, because the selection records its own outputs and they never invalidate the tree fingerprint; pointing --report, --selection, and --witnesses at an out-of-tree or gitignored directory keeps the checkout tidier.

From there:

  • ACQUIT_SELECTION_FILE=... pytest verifies the selection against the tree and deselects the provably unaffected files; without the variable, pytest is untouched.
  • acquit explain tests/test_something.py --base origin/main walks through the decision for one test file.
  • acquit replay acquit-report.json re-verifies every witness from first principles. Replay rebuilds the analyzed commit, and a working-tree report records no head sha, so pass --head <commit> at select time if you want the run to be replayable.
  • acquit analyze prints the dependency graph's health.

How it compares

Tool Mechanism Needs code execution Deterministic Explains each skip Works with plain pytest Cost
acquit static import graph, fail-closed rules no yes yes: a witness per skip, a named rule per run-all yes free, Apache-2.0
pytest-testmon runtime coverage database yes given the same database, yes; the database is per-machine state dependency data exists, but no proof artifact per skip yes free
Codecov ATS testmon-based, hosted yes same caveats as testmon no yes commercial
Launchable ML over test-result history needs outcome history no, probabilistic by design no, it predicts yes commercial
Bazel / Pants declared build dependency graph no yes queryable graph, no per-skip artifact no, requires build-system migration free, plus the migration
dorny/paths-filter hand-written path globs no yes no, the globs are the reasoning yes free

Acquit occupies one niche: fail-closed static selection with a proof per skip, on an unmodified pytest project. If you want function-level precision and are willing to maintain runtime coverage state, pytest-testmon is the honest alternative; it is precise where acquit is conservative, and stateful where acquit is a pure function of the diff.

Design principles

  • Never execute user code during analysis
  • Fail closed: every error path converges on "run all tests"
  • Deterministic: same tree and diff produce byte-identical reports
  • Explainable: every decision names the rule or the graph path behind it

FAQ

Static analysis cannot be sound in Python. It cannot bound everything, and acquit does not try. Anything outside what static imports can bound (non-literal importlib, exec, sys.path mutation, changed conftests, dependency bumps, unparseable files, and the rest of a nineteen-rule table) fails closed to running more tests, up to everything. Soundness comes from the direction of the failure, not from the reach of the analysis. Measured: 198 replayed PRs, zero unsafe skips, every skip re-verified by acquit replay from first principles.

Why file-level selection instead of test-level? Module-level imports are the unit static analysis can bound honestly. Function-level selection would require bounding fixture resolution and parametrization behavior, and a bound I cannot defend is a skip I will not make. Function-level granularity is on the research list, not in the product.

What about fixtures and conftest magic? Conftest scoping is modeled directly: a changed non-root conftest forces its whole subtree to run, a changed root conftest or pytest config forces everything, collection-altering hooks and unresolvable pytest_plugins entries fail closed globally. Fixtures defined in a conftest ride on those edges; fixture changes inside a test's own import closure are ordinary graph reachability.

My PRs all touch the changelog, so this will never skip anything. That is rule R001 doing its job until you say otherwise. An assume_inert list under [tool.acquit] vouches that named docs or data files feed no test, and the sticky PR comment suggests the exact entry when docs files are the only blocker. The proof obligation for those files becomes yours; treat .acquit.toml diffs like CI config in review.

Documentation

  • CLI reference: every subcommand, flag, document schema, exit code, and the action's inputs and outputs
  • Rule reference: R001 to R019, with triggers, scopes, and the exact reason texts
  • Soundness contract: what "provably unaffected" means, its assumptions, and its limits
  • Architecture decision records: fail-closed policy, static analysis, rustworkx, the action, the study, granularity, tree binding, and the precision work (re-export narrowing, constant folding, derived registries)
  • The replay study: 198 replayed PRs, and how to re-run it
  • Contributing: dev setup, the CI gate, and the bar for soundness-critical changes
  • Security policy: what counts as a vulnerability here, and how to report one

License

Apache-2.0

Metadata

Release files for acquit 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for acquit 0.3.0
File Size Uploaded
acquit-0.3.0.tar.gz 757.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for acquit 0.3.0
File Interpreter ABI Platform
acquit-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 908.5 kB

Release files / acquit-0.3.0.tar.gz

Download URL acquit-0.3.0.tar.gz
Size 757.8 kB
Tags Source
SHA-256 checksum
How to use checksums
326aad9e7b528700a062992d06fcb15a2a35e82acdf6043858b51eb539241ec2
BLAKE2b-256 checksum
How to use checksums
47fe37db4f41d498dfd7e1166282cd16c808ba5f3be548ab0e6f006945c2680c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / acquit-0.3.0-py3-none-any.whl

Download URL acquit-0.3.0-py3-none-any.whl
Size 150.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e66fa56a22cdffddf93f2dee674783fd7d3837cf40d23e9c0c0cb766bfdb68b9
BLAKE2b-256 checksum
How to use checksums
491f3acfe88783ad92ea1fa4d3a38c734e9e37cae8a52cde06fe9d7c53ad953a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page