Repository navigation
Vendored requirements.txt refuses a six (==1.16.0) pin that the inventory and hosted mode accept, because exact pins are read by three grammars #1365
Description
[agent] Filed by the scheduled architecture audit routine (ecosystems and formats). Register: register comment.
Kind: bug. Source: new finding, register E96 (the "duplicated business logic" class; same shape as E91 and E89).
Problem
"Is this requirements.txt line an exact pin of name==version" is decided by three independent grammars. Verified on 9ab72d4. File links below are at that SHA. The line-anchored permalinks are in the first comment, because this tool mangles line anchors in issue bodies.
- Lock inventory and VEX discovery use
utils::requirements::exact_pin(requirements.rs, lines 392-426). Its doc says it is "the ONE exact-pin rule". It accepts whitespace around==and the legacy parenthesised formsix (==1.0). It rejects===, and it returns the version as spelled. Callers: lock_inventory/pypi.rs line 674 and vex/discover/pypi_other.rs line 283. - Hosted rewrite uses its own regex and
requirement_version(redirect/requirements.rs, lines 146-187 and 198-201). It strips( … ), accepts===asArbitrary, and compares==under PEP 440. - Vendored planner uses
parse_requirement_line(lines 1148-1191) plusscan_pins(lines 78-112) in pypi_requirements.rs. It takes everything after the name and extras as the specifier, strips whitespace and callspep440::is_exact_pin_of, which needs a leading==. So(==1.16.0)is a range.
The PEP 508 name-prefix scan ([A-Za-z0-9._-]*) is written out once more in each of these, and also in vendor/common.rs::pep508_name, vendor/pypi_lock.rs::value_identity and vex/discover/pypi_other.rs::pep508_direct_reference.
Proof by execution. A throwaway unit test in vendor::pypi_requirements::tests, run twice on 9ab72d4 and then reverted, fed the same one-line requirements.txt to all three readers for six@1.16.0:
| line | inventory exact_pin |
vendored find_pin |
hosted rewrite_registry_redirect |
|---|---|---|---|
six==1.16.0 |
("six","1.16.0") |
Exact | rewritten |
six (==1.16.0) |
("six","1.16.0") |
Range → pypi_requirement_not_pinned |
rewritten |
six (== 1.16.0) |
("six","1.16.0") |
Range → pypi_requirement_not_pinned |
rewritten |
six===1.16.0 |
None |
Range | rewritten |
six==1.16.0,!=1.15 |
None |
Range | redirect_requirements_version_ambiguous |
pip reads six (==1.16.0) as the exact pin ==1.16.0: PEP 508's versionspec admits the parenthesised form, and exact_pin's own doc relies on that. So scan offers the package (the inventory finds the pin) and hosted mode wires it, but socket-patch vendor fails with "requirements.txt: six is not pinned to ==1.16.0; pin it exactly …". That message is false.
Symptoms
- Lock-only requirements.txt discovery sends PEP 440-equivalent pins verbatim (
six==1.16→pkg:pypi/six@1.16), so a fresh checkout reports "No patches available" while the same file with a venv is patched #604: lock-only discovery (exact_pin) carries the spelled version (six==1.16→@1.16), while hosted and vendored compare under PEP 440. This is the same split on the version axis. - Lockfile-only requirements.txt discovery skips exact pins written with spaces (
six == 1.15.0,six (==1.15.0)), so a fresh checkout reports "No patches available" and pip installs the unpatched release #523 (closed) fixed the parenthesised and spaced forms in discovery only. Hosted requirements.txt rewrite skips PEP 440-equivalent pins likesix==1.16for an installed 1.16.0, soscanexits 0 and pip installs the unpatched release (regression from v4.0.0) #475 (closed) fixed PEP 440 equality in hosted and vendored only. Each fix landed in one grammar.
Impact
- A user-visible refusal with a misleading remedy for a valid pip spelling, plus a scan/vendor disagreement of the E91 kind. It is uncommon in hand-written files, but
pip-compileusers who hand-edit, and older tooling, writename (==x). - Systemic: every requirements.txt pin rule (PEP 440 equality,
===, parentheses, markers,--hash) has to be fixed in three places, and history shows it is fixed in one at a time.
Proposed change
- Add one
utils::requirements::Requirementparse of a logical line's code part: name as spelled, extras, the specifier split into clauses (parentheses removed), marker, hash options and direct reference. AddRequirement::exact_version(), an exact pin under PEP 440 (==with no wildcard;===reported separately). - Make
exact_pina thin wrapper over it (or delete it and move its callers). - Delete
parse_requirement_line/ParsedRequirementinvendor/pypi_requirements.rs.scan_pinsreadsRequirement. - Delete the hosted
name_reregex andrequirement_versioninpatch/redirect/requirements.rs. The hosted rewrite readsRequirementand keeps its ownUnpinned/Arbitrarypolicy on top. - Whether vendored should also accept
===is a policy choice. Keep today's refusal and say so in a code comment; don't widen it in this change.
Size and scope
utils/requirements.rs(+120),60),vendor/pypi_requirements.rs(−patch/redirect/requirements.rs(−~50), plus tests. About 300 production lines changed.- Out of scope: Lock-only requirements.txt discovery sends PEP 440-equivalent pins verbatim (
six==1.16→pkg:pypi/six@1.16), so a fresh checkout reports "No patches available" while the same file with a venv is patched #604's PEP 440 version normalization in the inventory purl. It can land on top ofexact_version()as a follow-up, or in the same PR if small. Also out of scope:pep508_namein TOML-based backends, and Hosted requirements.txt rewrite replaces a user's own direct reference (six @ https://mirror/…/six-1.16.0-….whl,file://fork) with the Socket PyPI build, and rollback then restoressix==1.16.0from PyPI, losing the original source #542 (hosted direct references, PR Fix hosted requirements rewrite of user direct refs (#542) #1333).
Acceptance criteria
- One requirements-line grammar in
utils::requirements, used by the inventory, VEX discovery, the hosted rewrite and the vendored planner.parse_requirement_lineand the hostedname_re/requirement_versionare gone. - Regression test: a table test over the five lines above, asserting that the inventory, hosted and vendored readers agree on "exact pin of 1.16.0", with vendored wiring
six (==1.16.0)andsix (== 1.16.0). - The existing
pep440_equivalent_pins_are_rewritten,parse_requirement_line_ignores_path_and_nonname_lines, marker-split (Vendored requirements.txt fromuv pip compile --universalrefuses a marker-split package (six==1.16.0 ; python < 3.12+six==1.17.0 ; python >= 3.12) as "not pinned to ==1.16.0", while --dry-run previews would_vendor and hosted / vendored pylock handle the same split #928) and lock-only discovery (Lockfile-only requirements.txt discovery skips exact pins written with spaces (six == 1.15.0,six (==1.15.0)), so a fresh checkout reports "No patches available" and pip installs the unpatched release #523, Lock-only requirements.txt discovery drops a pin whose last line ends in a dangling\continuation (six==1.16.0 \at EOF): scan exits 0 with "No patches", but pip installs it #1249) tests stay green. -
cargo test -p socket-patch-coreand the pip e2e suites pass.
Dependencies
- None blocking. It touches the same file as PR Fix hosted requirements rewrite of user direct refs (#542) #1333 (Hosted requirements.txt rewrite replaces a user's own direct reference (
six @ https://mirror/…/six-1.16.0-….whl,file://fork) with the Socket PyPI build, and rollback then restoressix==1.16.0from PyPI, losing the original source #542, hosted direct refs). Rebase on whichever lands first. - Unblocks a one-place fix for Lock-only requirements.txt discovery sends PEP 440-equivalent pins verbatim (
six==1.16→pkg:pypi/six@1.16), so a fresh checkout reports "No patches available" while the same file with a venv is patched #604.
Activity
mikolalysenko commentedon Oct 9, 2026
[agent] Line-anchored permalinks for the body (main @
9ab72d4):utils/requirements.rs#L392-L426(exact_pin). Callers:lock_inventory/pypi.rs#L674andvex/discover/pypi_other.rs#L283.```redirect/requirements.rs#L146-L187```` (requirement_version) and `#L198-L201`````` (`name_re`).vendor/pypi_requirements.rs#L1148-L1191(parse_requirement_line) and#L78-L112(scan_pins); the refusal is at#L564-L572.````````Generated by Claude Code
audit-ecosystems: E17 fixed, E96 filed (#1365), living doc Parts 3-5
mikolalysenko commentedon Oct 9, 2026
[agent] Triaged as
priority:p1(pip / requirements.txt). Not a duplicate: #604 (draft PR #1334) is the version-normalization half of the same split and is explicitly out of scope here, and #523/#475 (closed) each fixed one of the three grammars. Left unclaimed for now because PR #1366 currently references this issue; it becomes eligible once that reference is gone or that PR lands.Generated by Claude Code
mikolalysenko commentedon Oct 11, 2026
[agent] Claiming this issue (shared root cause: three separate requirements.txt grammars decide what an exact pin is). Branch: agent/fix-pip-requirements-pin-grammar. Claim-ID: 2026-10-11T11:21:14Z-024dc9
Generated by Claude Code
mikolalysenko commentedon Oct 11, 2026
[agent] Draft PR: #1381
Generated by Claude Code
Read requirements.txt pins with one grammar
Test vendoring a parenthesised requirements pin
Keep epoch and v-prefixed pins exact; fail closed
Fail closed on dropped options and stray lines
Refuse short pip options like -C on vendored pins
Refuse hosted pins with a short pip option