Skip to main content

Static linter that checks a requirements file is fully version-pinned and hash-pinned, for CI and pre-commit.

Project description

pinlint

pinlint logo

PyPI CI License: MIT

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-unpinned do not require exact version pins.
  • --no-hashes do not require --hash entries.
  • --no-follow do not follow -r and -c includes.
  • --allow PACKAGE ignore findings for a package name (repeatable).
  • --format text|json|sarif choose the output format. json suits CI and editors; sarif emits SARIF 2.1.0 for GitHub code scanning and other analysis tools.
  • --write-baseline PATH write all current findings to a baseline JSON file, then exit 0.
  • --baseline PATH suppress findings present in the baseline; exit nonzero only when new findings remain.
  • --error CODE treat CODE as an error (repeatable).
  • --warning CODE treat CODE as a warning; warnings are printed but do not cause a nonzero exit (repeatable).
  • --off CODE silence 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

pinlint-0.5.0.tar.gz (992.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pinlint-0.5.0-py3-none-any.whl (15.7 kB view details)

Uploaded Python 3

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

Hashes for pinlint-0.5.0.tar.gz
Algorithm Hash digest
SHA256 000fa4f5dd004b99381ce466bb06400600c443b608d718228897074a66d7bcb9
MD5 ce53523ada70c6678d86e915f1bced16
BLAKE2b-256 871766f61d324f01ae3db80f1251c7a9b1d6e0fef345e9c96101a02ddcafe72c

See more details on using hashes here.

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

Hashes for pinlint-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7e5f776b55593b85fc40dce7c74382af097f2245666bc0d9f71191e629fa0061
MD5 5fae7ae8bd34994544e73573845e8402
BLAKE2b-256 b92949d8d7d379adcd97d144bd0db496db7a00a628813a33b7dc8d7267a452a5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page