You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
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
[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 form six (==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 === as Arbitrary, and compares == under PEP 440.
Vendored planner uses parse_requirement_line (lines 1148-1191) plus scan_pins (lines 78-112) in pypi_requirements.rs. It takes everything after the name and extras as the specifier, strips whitespace and calls pep440::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.
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-compile users who hand-edit, and older tooling, write name (==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::Requirement parse of a logical line's code part: name as spelled, extras, the specifier split into clauses (parentheses removed), marker, hash options and direct reference. Add Requirement::exact_version(), an exact pin under PEP 440 (== with no wildcard; === reported separately).
Make exact_pin a thin wrapper over it (or delete it and move its callers).
Deleteparse_requirement_line/ParsedRequirement in vendor/pypi_requirements.rs. scan_pins reads Requirement.
Delete the hosted name_re regex and requirement_version in patch/redirect/requirements.rs. The hosted rewrite reads Requirement and keeps its own Unpinned/Arbitrary policy 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), vendor/pypi_requirements.rs (−60), patch/redirect/requirements.rs (−~50), plus tests. About 300 production lines changed.
One requirements-line grammar in utils::requirements, used by the inventory, VEX discovery, the hosted rewrite and the vendored planner. parse_requirement_line and the hosted name_re/requirement_version are 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) and six (== 1.16.0).
[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.
[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 on9ab72d4. 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.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.requirement_version(redirect/requirements.rs, lines 146-187 and 198-201). It strips( … ), accepts===asArbitrary, and compares==under PEP 440.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 invendor/common.rs::pep508_name,vendor/pypi_lock.rs::value_identityandvex/discover/pypi_other.rs::pep508_direct_reference.Proof by execution. A throwaway unit test in
vendor::pypi_requirements::tests, run twice on9ab72d4and then reverted, fed the same one-linerequirements.txtto all three readers forsix@1.16.0:exact_pinfind_pinrewrite_registry_redirectsix==1.16.0("six","1.16.0")six (==1.16.0)("six","1.16.0")pypi_requirement_not_pinnedsix (== 1.16.0)("six","1.16.0")pypi_requirement_not_pinnedsix===1.16.0Nonesix==1.16.0,!=1.15Noneredirect_requirements_version_ambiguouspip reads
six (==1.16.0)as the exact pin==1.16.0: PEP 508'sversionspecadmits the parenthesised form, andexact_pin's own doc relies on that. So scan offers the package (the inventory finds the pin) and hosted mode wires it, butsocket-patch vendorfails with "requirements.txt: six is not pinned to ==1.16.0; pin it exactly …". That message is false.Symptoms
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.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
pip-compileusers who hand-edit, and older tooling, writename (==x).===, parentheses, markers,--hash) has to be fixed in three places, and history shows it is fixed in one at a time.Proposed change
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).exact_pina thin wrapper over it (or delete it and move its callers).parse_requirement_line/ParsedRequirementinvendor/pypi_requirements.rs.scan_pinsreadsRequirement.name_reregex andrequirement_versioninpatch/redirect/requirements.rs. The hosted rewrite readsRequirementand keeps its ownUnpinned/Arbitrarypolicy on top.===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.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
utils::requirements, used by the inventory, VEX discovery, the hosted rewrite and the vendored planner.parse_requirement_lineand the hostedname_re/requirement_versionare gone.six (==1.16.0)andsix (== 1.16.0).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
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.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.