Metadata-Version: 2.4
Name: pinlint
Version: 0.4.0
Summary: Static linter that checks a requirements file is fully version-pinned and hash-pinned, for CI and pre-commit.
Project-URL: Homepage, https://github.com/amaar-mc/pinlint
Project-URL: Repository, https://github.com/amaar-mc/pinlint
Project-URL: Issues, https://github.com/amaar-mc/pinlint/issues
Project-URL: Changelog, https://github.com/amaar-mc/pinlint/blob/main/CHANGELOG.md
Author: Amaar Chughtai
License: MIT License
        
        Copyright (c) 2026 Amaar Chughtai
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: ci,hashes,linter,pinning,pip,pre-commit,reproducible-builds,requirements,security,supply-chain
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: packaging>=21
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# pinlint

<p align="center">
  <img src="assets/logo.png" alt="pinlint logo" width="160">
</p>

[![PyPI](https://img.shields.io/pypi/v/pinlint)](https://pypi.org/project/pinlint/)
[![CI](https://github.com/amaar-mc/pinlint/actions/workflows/ci.yml/badge.svg)](https://github.com/amaar-mc/pinlint/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)

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

```sh
pip install pinlint
```

## 30-second example

```sh
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:

```python
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.

## 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.

```sh
# 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`:

```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

```sh
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](./CONTRIBUTING.md).

## License

MIT. See [LICENSE](./LICENSE).
