Skip to content

Fix requirements.txt pins not matched under PEP 440 (#475) - #478

Open
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
mainfrom
agent/fix-requirements-pep440-pin-match
Open

Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
mainfrom
agent/fix-requirements-pep440-pin-match

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #475

Summary

Hand-written requirements.txt pins like six==1.16 (for an installed six 1.16.0) are now matched the way pip matches them, under PEP 440 equality. Before this change, scan --mode hosted skipped such a pin, reported "no requirements.txt entry", exited 0, and left the project installing the unpatched release. This is a regression from v4.0.0, introduced in #239.

Root cause

Every pin matcher compared the pinned version with the patch version as a raw string. pip compares them under PEP 440, which zero-pads release segments and ignores leading zeros, case and alternate pre/post/dev spellings. So ==1.16, ==1.16.0.0 and ==01.16.0 all select exactly 1.16.0. The same raw comparison was in three writers:

  • hosted patch/redirect/requirements.rs skipped the pin with redirect_requirements_entry_not_found and exited 0;
  • vendored vendor/pypi_requirements.rs refused with a false pypi_requirement_not_pinned;
  • Hatch utils/hatch.rs (used by hosted and vendored Hatch) refused hand-written pyproject.toml pins like urllib3==1.26.18.0 with "requires an exact ==1.26.18 declaration". Same cause at the same boundary, so it is fixed here too.

Fix

  • New utils/pep440.rs: a small parser for the packaging version grammar, plus PEP 440 equality (epoch, zero-trimmed release, normalized pre/post/dev, case-folded local segments). Digit runs are compared as leading-zero-trimmed strings, so long numbers can't overflow. Invalid versions are never equal to anything, so callers fail closed.
  • All three writers now use it for == pins. === (arbitrary equality) keeps plain string comparison, as PEP 440 defines it, and wildcards (==1.*) are still ranges.

Notes / follow-ups

  • Rollback spelling: hosted mode keeps no ledger in v5, so hosted rollback re-derives name==<patch version>. A redirected six==1.16 therefore rolls back to six==1.16.0, which installs the same release. This matches how rollback already normalizes six == 1.16.0 today. Vendored revert is ledger-based and stays byte-exact.
  • Lock-only discovery (no venv): utils/requirements.rs::exact_pin still carries the spelled version into the purl (pkg:pypi/six@1.16). Matching that to the 1.16.0 release needs PyPI's canonical release spelling, not just local normalization. The issue lists this as a "may" and its repro uses an installed venv, so it's left as a follow-up rather than guessed at here.

Test evidence

Regression tests, each shown failing on main and passing with the fix:

Issue variant Test main this PR
hosted ==X.Y, ==X.Y.Z.0, ==0X.Y.Z, spaced + marker patch::redirect::requirements::tests::pep440_equivalent_pins_are_rewritten ❌ redirect_requirements_entry_not_found ✅
hosted, end to end (get <uuid> --mode hosted over requests==2.31, ==2.31.0.0, Requests==02.31.0) in_process_get_hosted_ecosystems::pypi_requirements_hosted_rewrites_pep440_equivalent_pin ❌ file unchanged ✅
vendored false pypi_requirement_not_pinned vendor::pypi_requirements::tests::find_pin_classifies_every_shape (new PEP 440 cases; === / wildcard still Range) ❌ ✅
Hatch equivalent pins utils::hatch::tests::pep440_equivalent_pins_are_exact_declarations ❌ ✅
helper utils::pep440::tests::* (equal spellings, non-equal releases, invalid input, == vs ===/wildcards) new ✅

Local runs (Linux):

  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo test --workspace --all-features --no-fail-fast: everything passes except 12 permission-based write-failure tests, which fail because this sandbox runs as uid 0 (root ignores read-only bits). They fail identically on main, and all 12 pass when re-run as uid 65534.
  • cargo test -p socket-patch-cli --all-features --test e2e_vendor_pypi_build -- --ignored (real uv + pip + PyPI): 7/7 pass.
  • cargo fmt: the changed code is rustfmt-formatted. CI has no fmt step and main itself isn't fmt-clean (rustfmt 1.8 rewrites ~125 unrelated files), so unrelated reflows were left out.
  • No wrapper changes: npm/, pypi/ and gem/ only dispatch to the binary.

🤖 Generated with Claude Code


Note

Medium Risk
Changes pin-matching on the patch redirect path for PyPI; incorrect PEP 440 logic could mis-redirect or skip pins, but invalid versions fail closed and ===/wildcards are unchanged.

Overview
Fixes #475: PyPI pin matching now treats == the same way pip/uv/Hatch do under PEP 440, instead of comparing version strings literally.

Adds utils/pep440 with packaging-style parsing and equality (versions_equal, is_exact_pin_of). Hosted requirements.txt redirect, vendored pypi_requirements pin discovery, and Hatch pyproject.toml rewrites all use it for == pins. === stays plain string equality; wildcards and ranges are unchanged.

Pins like requests==2.31, ==2.31.0.0, or ==02.31.0 now redirect to the patched wheel when the grant is 2.31.0, instead of skipping with “no entry” / false “not pinned” and leaving the project unpatched.

Regression coverage: unit tests for the helper and each rewriter, plus an in-process get --mode hosted test over equivalent requirements.txt pins.

Reviewed by Cursor Bugbot for commit bcc5335. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
A hand-written pin such as `six==1.16` installs six 1.16.0, but the
hosted requirements.txt rewrite compared versions as raw strings and
skipped it, so `scan` exited 0 and the project stayed unpatched. The
vendored requirements writer and the Hatch rewriter refused the same
pins as "not pinned".

Add a small PEP 440 equality helper (zero-padded release segments,
leading zeros, case and pre/post/dev spellings) and use it for `==`
pins in all three writers. `===` keeps plain string equality, as PEP
440 defines it.

Fixes #475

Assisted-by: Claude Code:claude-opus-5-5
Mirrors the #475 repro end to end: `get <uuid> --mode hosted` over
`requests==2.31`, `==2.31.0.0` and `Requests==02.31.0` must redirect
the pin to the hosted wheel. Fails on main, passes with the fix.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 1, 2026 16:03
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit bcc5335. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 1, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: ready for review at bcc5335 (bcc533518a74ca2234958cb56379a4333e97d6cb).

  • CI: 97/97 non-skipped checks green (3 skipped).
  • Bugbot: reviewed bcc5335, no findings; no unresolved review threads.
  • Mergeable with no conflicts (15 commits behind main, merges cleanly).
  • Reviewer focus: the new utils/pep440.rs equality rules, and the note that hosted rollback re-derives name==<patch version> (so six==1.16 rolls back as six==1.16.0).

Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

2 participants