Static linter that checks a requirements file is fully version-pinned and hash-pinned, for CI and pre-commit.
Project description
pinlint
A static linter that checks a requirements file is fully version-pinned and hash-pinned. Built for CI and pre-commit, so unpinned or unhashed dependencies fail review before anything is installed.
Install
pip install pinlint
30-second example
pinlint requirements.txt
requirements.txt:3: unpinned: requests is not pinned to an exact version (use ==)
requirements.txt:7: missing-hash: flask has no --hash
2 issue(s) found
Exit code is 1 when there are findings, 0 when the file is clean, so it drops straight into CI or a pre-commit hook.
As a library:
from pinlint import lint_file
findings = lint_file(
"requirements.txt", require_hashes=True, allow_unpinned=False, follow_includes=True
)
for f in findings:
print(f.file, f.line, f.code, f.message)
Why this exists
Reproducible, tamper-evident installs need every requirement pinned to an exact version
and carrying a hash. The existing tools each do something adjacent: pip-compile --generate-hashes generates such a file, pip install --require-hashes enforces hashes
at install time, and requirements-txt-fixer tidies formatting. None of them is a fast,
static check you can run in review to assert that an arbitrary requirements file is fully
pinned and hashed. pinlint is that check.
Comparison
| pinlint | pip-compile | pip --require-hashes | requirements-txt-fixer | |
|---|---|---|---|---|
| Static check, no install | yes | n/a | no (install time) | yes |
| Flags unpinned versions | yes | generates | at install | no |
| Flags missing hashes | yes | generates | at install | no |
| CI / pre-commit gate | yes | partial | no | yes (formatting only) |
Checks
unpinned: the requirement is not pinned with==or===to an exact version.missing-hash: the requirement has no--hash(unless--no-hashes).unpinnable: an editable, URL, or VCS install that cannot be version-pinned.parse-error: the line could not be parsed as a requirement.
It understands comments, blank lines, backslash line continuations, --hash options,
environment markers and extras, and -r and -c includes (followed with cycle
protection). The only dependency is packaging, the canonical PEP 508 parser.
Options
--allow-unpinneddo not require exact version pins.--no-hashesdo not require--hashentries.--no-followdo not follow-rand-cincludes.--allow PACKAGEignore findings for a package name (repeatable).--format text|json|sarifchoose the output format.jsonsuits CI and editors;sarifemits SARIF 2.1.0 for GitHub code scanning and other analysis tools.--write-baseline PATHwrite all current findings to a baseline JSON file, then exit 0.--baseline PATHsuppress findings present in the baseline; exit nonzero only when new findings remain.--error CODEtreat CODE as an error (repeatable).--warning CODEtreat CODE as a warning; warnings are printed but do not cause a nonzero exit (repeatable).--off CODEsilence CODE entirely; matching findings are dropped from output (repeatable).
Per-rule severity
Different teams have different tolerances. Severity flags let you decide which rules block CI and which are advisory.
# Treat unpinned as a warning (printed, exit 0) and silence missing-hash entirely.
pinlint requirements.txt --warning unpinned --off missing-hash
# Escalate unpinnable from its default warning to a hard error.
pinlint requirements.txt --error unpinnable
Exit-code semantics: exit 1 only when at least one ERROR-level finding remains after
applying severity overrides and any --allow filtering. Warnings alone exit 0. This lets
you run pinlint in advisory mode (all warnings) during migration without breaking CI.
Default severities (no flags) match the SARIF rule catalog and are backward compatible with 0.4.0:
| Rule | Default severity |
|---|---|
unpinned |
error |
missing-hash |
error |
unpinnable |
warning |
parse-error |
error |
io-error |
error |
Valid rule codes: unpinned, missing-hash, unpinnable, parse-error, io-error.
Passing an unknown code exits 2 with a clear error message listing the valid codes.
Programmatic API
from pinlint import (
apply_severities,
default_severity_map,
lint_file,
to_sarif_annotated,
)
findings = lint_file(
"requirements.txt", require_hashes=True, allow_unpinned=False, follow_includes=True
)
sev = default_severity_map()
sev["unpinned"] = "warning" # downgrade
sev["missing-hash"] = "off" # silence
annotated = apply_severities(findings=findings, severity_map=sev)
for af in annotated:
print(af.effective_severity, af.code, af.message)
# SARIF output with effective severities:
doc = to_sarif_annotated(annotated, tool_version="0.5.0")
Baseline: adopt pinlint incrementally
If an existing project has many unpinned requirements you cannot fix all at once, use a baseline to suppress the known findings and fail only on new ones.
# Record the current state.
pinlint requirements.txt --write-baseline .pinlint-baseline.json
# In CI, suppress known findings and fail only on new ones.
pinlint requirements.txt --baseline .pinlint-baseline.json
The baseline file is deterministic and human-readable, so it diffs cleanly in code review. Findings are fingerprinted by rule code, file path, requirement text, and package name -- not by line number -- so adding or removing unrelated lines above a requirement does not invalidate its suppression.
Commit .pinlint-baseline.json to version control. When you fix a requirement, re-run
--write-baseline and commit the smaller file; the diff shows the fix.
Pre-commit
pinlint ships a hook, so you can add it to .pre-commit-config.yaml:
repos:
- repo: https://github.com/amaar-mc/pinlint
rev: v0.2.0
hooks:
- id: pinlint
The hook runs on files matching requirements.*\.txt.
Testing
pip install -e ".[dev]"
pytest
Tests use golden requirements files for each rule, including includes, cycles, line continuations, and the CLI exit codes.
Contributing
Issues and pull requests are welcome. See CONTRIBUTING.md.
License
MIT. See LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pinlint-0.5.0.tar.gz.
File metadata
- Download URL: pinlint-0.5.0.tar.gz
- Upload date:
- Size: 992.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
000fa4f5dd004b99381ce466bb06400600c443b608d718228897074a66d7bcb9
|
|
| MD5 |
ce53523ada70c6678d86e915f1bced16
|
|
| BLAKE2b-256 |
871766f61d324f01ae3db80f1251c7a9b1d6e0fef345e9c96101a02ddcafe72c
|
File details
Details for the file pinlint-0.5.0-py3-none-any.whl.
File metadata
- Download URL: pinlint-0.5.0-py3-none-any.whl
- Upload date:
- Size: 15.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7e5f776b55593b85fc40dce7c74382af097f2245666bc0d9f71191e629fa0061
|
|
| MD5 |
5fae7ae8bd34994544e73573845e8402
|
|
| BLAKE2b-256 |
b92949d8d7d379adcd97d144bd0db496db7a00a628813a33b7dc8d7267a452a5
|