Repository navigation
Vendored requirements.txt still refuses a marker-split package when the other version's branch sits in a different file of the -r tree (false "not pinned to ==1.16.0", exit 1), so a fresh install stays unpatched #1104
Description
[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
#928 (fixed by #929) taught vendored requirements.txt that a pin to another version on its own environment-marker branch isn't ambiguous: six==1.16.0 ; python_version < '3.12' next to six==1.17.0 ; python_version >= '3.12' now vendors only the 1.16.0 branch. That fix works only when both branches are in the same file. When the branches are split across the root requirements.txt and a -r include, which is common in layered requirements (base.txt plus a root file), vendored mode still refuses the whole package:
pypi_requirement_not_pinned: base.txt: six is not pinned to ==1.16.0; pin it exactly or use agent mode (`scan --mode agent` + `socket-patch apply`) instead
The message is false. The tree pins six to ==1.16.0 exactly, and pip installs exactly one branch. Hosted mode handles the same layout, and so does vendored mode once both lines are moved into one file.
The uv routine raised this case in a comment on #928 (#928 (comment)) before #929 merged. #929's tests and scan_pins change cover only the single-file case, and #928 is now closed, so nothing tracks this gap.
Impact
- A project that keeps a version-specific pin in a separate requirements file can't vendor a patch for that package. The run exits 1 (
partial_failure), andpip install -r requirements.txton a fresh checkout installs the unpatched release. - The remedy the CLI suggests ("pin it exactly") can't be followed, because the pin is already exact.
- Nothing is written, so there's no corruption, and other packages in the same run still vendor.
Repro (Linux, main d7f8679, pip 26.2.1 / CPython 3.11)
I ran this against a local mock patch API serving a patched real six-1.16.0 wheel (the same routes as tests/vex_pypi_real_common).
printf 'idna==3.7\nsix==1.17.0 ; python_version >= "3.12"\n' > base.txt
printf -- '-r base.txt\nsix==1.16.0 ; python_version < "3.12"\n' > requirements.txt
python3.11 -m venv .venv && .venv/bin/pip install -r requirements.txt # six 1.16.0
socket-patch scan --mode vendored --yes --json
# exit 1, vendor.events[0] = {action: failed, errorCode: pypi_requirement_not_pinned,
# error: "base.txt: six is not pinned to ==1.16.0; ..."}; requirements.txt unchanged, no .socket/vendor
python3.11 -m venv fresh && fresh/bin/pip install -r requirements.txt # six 1.16.0 from PyPI, unpatchedWhen the branches are swapped (1.17.0 in the root, 1.16.0 in base.txt), the error names requirements.txt instead and the outcome is the same.
Expected vs actual
- Expected: Vendored requirements.txt from
uv 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's rule applied across the whole-rtree. pip merges the root and its includes into one requirement set (docs/ecosystems.md, PyPI row: vendored mode follows-rincludes). Each file's 1.16.0 marker branch should be rewritten and the other version's marker branch left alone, as happens in the single-file case:If a cross-file split stays unsupported, the refusal should at least say so, not "not pinned"../.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl ; python_version < '3.12' # socket-patch vendor: six==1.16.0 - Actual:
plan_requirementsclassifies each file on its own. The file whose onlysixline is the other version's branch has no exact target pin, soscan_pinssetsfound_range(exact.is_empty()), and the whole package is refused withpypi_requirement_not_pinned.
Matrix (Linux; the refusal is pure planning code, so it doesn't depend on the OS or the pip version)
| Layout | scan --mode vendored |
Fresh pip install -r (pip 26.2.1 / py3.11) |
|---|---|---|
root six==1.16.0 ; <3.12, base.txt six==1.17.0 ; >=3.12 |
fail: exit 1, pypi_requirement_not_pinned naming base.txt (reproduced twice) |
six 1.16.0 unpatched |
root six==1.17.0 ; >=3.12, base.txt six==1.16.0 ; <3.12 |
fail: exit 1, same code naming requirements.txt (reproduced twice) | six 1.16.0 unpatched |
both branches in the root, include holds only idna (#928 control) |
pass: exit 0, only the 1.16.0 branch rewritten | six 1.16.0 patched |
first layout, scan --mode hosted |
pass: exit 0, redirected: 1, root branch rewritten with its marker |
six 1.16.0 patched |
Not bisected: cross-file splits have never been accepted. Before #929 the same-file split was refused too.
(The vendored --dry-run previews would_vendor here. I'm not reporting that separately, because CLI_CONTRACT.md defines that preview as a ledger classification that predicts only the npm-family preflights.)
Suspect code
crates/socket-patch-core/src/vendor/pypi_requirements.rs:107:if other_branches && (exact.is_empty() || …) { found_range = true; }. This is evaluated per file, so a file that holds only the other branch is classifiedRange.crates/socket-patch-core/src/vendor/pypi_requirements.rs:544-565:plan_requirementscallsfind_pinfile by file and returns the firstRangeaspypi_requirement_not_pinned. The "every target pin carries a marker / the other branch is marked" decision would need to be made over the whole collected-rtree (collect_requirements_files).
No probe runs were needed: this is OS-independent planning code.
Backlog review — 2026-10-08
Priority: P1 → P2. A marker split across requirements includes causes an explicit vendored refusal; this fails closed.
Activity
pip: run 30 ledger (#1104)
mikolalysenko commentedon Oct 8, 2026
[agent] Triaged as priority:p1 (PyPI / requirements.txt). Confirmed on main 77f305d by reading the code:
find_pinmakes the marker-branch decision per file (pypi_requirements.rs~L107 setsfound_rangewhen a file holds only the other version's marker branch), andplan_requirementsreturns the first per-fileRangeaspypi_requirement_not_pinnedbefore looking at the rest of the-rtree. Not a duplicate: #928/#929 fixed only the single-file split. Related area but a separate code path from #1086 (open PR #1091, lock contest across an include tree), so not clustered with it. Eligible for a fix in a later run.Generated by Claude Code