Repository navigation
A requirements.txt pin spelled ===X, ==X.0 or ==vX doesn't contest another lock's Socket wiring of release X, so vex attests not_affected while pip install -r installs the upstream release (#1549 residual) #1602
Description
[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
Since #1562 (fix for #1549), a requirements.txt line that installs the same release from somewhere other than Socket contests another lock's Socket wiring of that release. For example, a Pipfile.lock wired hosted or vendored beside a requirements.txt that says vexdemo==1.2.3 makes vex withhold the statement with patched_ref_unattributable ("… but requirements.txt resolves the same version").
That only works for the canonical name==X spelling. pip installs exactly the same upstream release for these lines, but none of them contests the wiring:
| requirements.txt line (wired release 1.2.3) | pip installs | vex today |
|---|---|---|
vexdemo==1.2.3 (control) |
upstream 1.2.3 | contested, exit ≠ 0 ✅ |
vexdemo===1.2.3 (arbitrary equality) |
upstream 1.2.3 | not_affected, exit 0 |
vexdemo==1.2.3.0 (PEP 440-equal; also six==1.16 for 1.16.0) |
upstream 1.2.3 | not_affected, exit 0 |
vexdemo==v1.2.3 (PEP 440 v prefix) |
upstream 1.2.3 | not_affected, exit 0 |
The same holds for single-version ranges that pip resolves to the wired release, such as ~=1.2.3, ==1.2.* and >=1.2.3,<=1.2.3. Those depend on what PyPI holds, so they may be a design call. The three exact spellings above are not: they can only install release X.
Impact
vex emits a not_affected statement (exit 0, with or without --no-verify), and vendor --check presumably stays green as well, for a project whose pip install -r requirements.txt ships the unpatched upstream wheel. That's the #1549 harm, reached through a different spelling. A hand-written six==1.16 (two segments) for release 1.16.0 is a common real-world form; socket-patch already treats it as equal to 1.16.0 everywhere else (#475, #604).
Repro
This uses the repo's own #1549 harness (crates/socket-patch-cli/tests/vex_pipenv_pip_common, the real CLI binary). Add this throwaway test to crates/socket-patch-cli/tests/e2e_vex_lockfile/pipenv.rs:
#[test]
#[ignore]
fn requirements_spellings_contest() {
for (label, other) in [
("control", format!("{NAME}=={VERSION}\n")),
("===", format!("{NAME}==={VERSION}\n")),
("==X.0", format!("{NAME}==1.2.3.0\n")),
("==vX", format!("{NAME}==v{VERSION}\n")),
] {
let r = std::panic::catch_unwind(|| {
contested_by_a_non_socket_direct_reference(
&flavors()[..1], "requirements.txt", &[(label, other.clone())]);
});
eprintln!("{label}: {}", if r.is_ok() { "contested" } else { "NOT contested" });
}
}cargo test -p socket-patch-cli --test e2e_vex_lockfile requirements_spellings_contest -- --ignored --nocapture
control: contested
===: NOT contested # vex exit 0, "status": "not_affected" for pkg:pypi/vexdemo@1.2.3
==X.0: NOT contested
==vX: NOT contested
pip installs the upstream release for each spelling (real pip, six from PyPI):
for line in 'six===1.16.0' 'six==1.16.0.0' 'six==v1.16.0' 'six==1.16'; do
echo "$line" > r.txt; python -m pip install --dry-run --no-deps -r r.txt # pip 20.3.4: pip download --no-deps -r r.txt
done
# every cell → six-1.16.0-py2.py3-none-any.whl from files.pythonhosted.org
Expected vs actual
- Expected: CLI_CONTRACT.md, "Contested locks": a lock that "resolves the same
name@versionfrom a non-Socket source" drops the reference withpatched_ref_unattributable. pip resolves===1.2.3,==1.2.3.0and==v1.2.3to the samename@versionas==1.2.3, so each should contest the wiring the same way the canonical pin does. - Actual: those lines are skipped as evidence.
vexattestsnot_affectedwith exit 0.
OS × version
| OS | pip / Python | reproduces |
|---|---|---|
| Linux | real CLI harness (Pipfile.lock wired, hosted and vendored cells) | yes, twice |
| Linux | pip 20.3.4 / py3.8, 24.0 / py3.11, 26.2.1 / py3.13 install upstream 1.16.0 for all four spellings | yes |
| macOS / Windows | not run (probe branches are blocked for this routine) | n/a; the code path is OS-independent |
First bad: the contest itself is new in 0d741d8 (#1562), so this is a residual of that fix and not a regression. Tested on main 657fce2.
Suspect code
crates/socket-patch-core/src/vex/discover/pypi_other.rs:347:registry_pinreads the line throughutils::requirements::exact_pin(crates/socket-patch-core/src/utils/requirements.rs:542). That function returnsNonefor===and for a version that doesn't start with a digit (v1.2.3), and it returns1.2.3.0verbatim.crates/socket-patch-core/src/vex/discover/mod.rs:924:resolved_elsewherestorescanonical_base_purl(purl), and the contest compares purl strings, sopkg:pypi/vexdemo@1.2.3.0never matches@1.2.3. The hosted rewriter already compares withutils::pep440::versions_equal(patch/redirect/requirements.rs); the contest needs the same PEP 440 equality, plus===(which pip compares as a string).
Related: #1549 / #1562 (the original contest), #1587 / #1599 (bare wheel paths, a separate residual), #475 / #604 (PEP 440-equal pins in the rewriter and in lock-only discovery).
Activity
mikolalysenko commentedon Oct 16, 2026
[agent] Triaged as
priority:p1(pip / requirements.txt, falsenot_affected). Confirmed on main657fce2:utils::requirements::exact_pinreturnsNonefor===and for a version not starting with a digit, and returns1.2.3.0verbatim, and the contest compares canonical purl strings rather than PEP 440 versions. Residual of #1549 / #1562. Separate from #1587 / #1599 (wheel-path lines), which goes throughrequirement_location, notexact_pin. No open PR covers it.Generated by Claude Code