Fix requirements.txt pins not matched under PEP 440 (#475) - #478
Open
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
Open
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
Conversation
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
Mikola Lysenko (mikolalysenko)
marked this pull request as ready for review
October 1, 2026 16:03
Collaborator
Author
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ 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.
Collaborator
Author
|
Burn-down agent: ready for review at
Generated by Claude Code |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 hostedskipped 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.0and==01.16.0all select exactly 1.16.0. The same raw comparison was in three writers:patch/redirect/requirements.rsskipped the pin withredirect_requirements_entry_not_foundand exited 0;vendor/pypi_requirements.rsrefused with a falsepypi_requirement_not_pinned;utils/hatch.rs(used by hosted and vendored Hatch) refused hand-writtenpyproject.tomlpins likeurllib3==1.26.18.0with "requires an exact ==1.26.18 declaration". Same cause at the same boundary, so it is fixed here too.Fix
utils/pep440.rs: a small parser for thepackagingversion 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.==pins.===(arbitrary equality) keeps plain string comparison, as PEP 440 defines it, and wildcards (==1.*) are still ranges.Notes / follow-ups
name==<patch version>. A redirectedsix==1.16therefore rolls back tosix==1.16.0, which installs the same release. This matches how rollback already normalizessix == 1.16.0today. Vendored revert is ledger-based and stays byte-exact.utils/requirements.rs::exact_pinstill carries the spelled version into the purl (pkg:pypi/six@1.16). Matching that to the1.16.0release 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:
==X.Y,==X.Y.Z.0,==0X.Y.Z, spaced + markerpatch::redirect::requirements::tests::pep440_equivalent_pins_are_rewrittenredirect_requirements_entry_not_foundget <uuid> --mode hostedoverrequests==2.31,==2.31.0.0,Requests==02.31.0)in_process_get_hosted_ecosystems::pypi_requirements_hosted_rewrites_pep440_equivalent_pinpypi_requirement_not_pinnedvendor::pypi_requirements::tests::find_pin_classifies_every_shape(new PEP 440 cases;===/ wildcard still Range)utils::hatch::tests::pep440_equivalent_pins_are_exact_declarationsutils::pep440::tests::*(equal spellings, non-equal releases, invalid input,==vs===/wildcards)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 onmain, 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 andmainitself isn't fmt-clean (rustfmt 1.8 rewrites ~125 unrelated files), so unrelated reflows were left out.npm/,pypi/andgem/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/pep440with packaging-style parsing and equality (versions_equal,is_exact_pin_of). Hostedrequirements.txtredirect, vendoredpypi_requirementspin discovery, and Hatchpyproject.tomlrewrites 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.0now redirect to the patched wheel when the grant is2.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 hostedtest over equivalentrequirements.txtpins.Reviewed by Cursor Bugbot for commit bcc5335. Configure here.
Generated by Claude Code