Repository navigation
Vendored requirements.txt from uv pip compile --universal refuses 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
Description
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
uv pip compile --universal (and --generate-hashes) writes one requirements line per marker branch whenever a package resolves to different versions on different Pythons or platforms:
six==1.16.0 ; python_full_version < '3.12'
six==1.17.0 ; python_full_version >= '3.12'
The patch is for six@1.16.0. scan --mode vendored refuses the whole package with:
pypi_requirement_not_pinned: requirements.txt: six is not pinned to ==1.16.0; pin it exactly or use agent mode (`scan --mode agent` + `socket-patch apply`) instead
That message is false: the file pins six to ==1.16.0 exactly. find_pin classifies the other branch's ==1.17.0 line as Range, and Range outranks Exact ("a file that names the package ambiguously is never rewritten"). But the marker already separates the two lines, so nothing is ambiguous:
- Hosted mode rewrites only the 1.16.0 line and leaves 1.17.0 alone (docs: "Hosted requirements select exact
==/===pins … Other versions remain unchanged"). - Vendored mode on
uv pip compile --universal -o pylock.tomlof the same input wires only the six 1.16.0 entry (docs: "Standalone PEP 751 rewriting selects the exact package version").
On top of that, scan --mode vendored --dry-run previews "action": "would_vendor" (exit 0, no warning), and the real run then fails (exit 1, partial_failure). The vendored dry-run preview never runs the requirements preflight (preflight_requirements).
Impact
- Any vendored project that compiles universal requirements with uv can't vendor a patch for a package that splits by version across markers. This is common for packages that drop old Pythons. The only remedy the CLI offers ("pin it exactly") is impossible, because the file already pins it exactly.
- CI that gates on
--dry-runsees a clean preview, then the real run fails. - Nothing is written, so there's no corruption. Other packages in the same run still vendor (
partial_failure).
Repro (Linux, uv 0.12.23; same on 0.5.31 / 0.8.17)
printf 'six==1.16.0 ; python_version < "3.12"\nsix==1.17.0 ; python_version >= "3.12"\n' > req.in
uv pip compile -q --universal req.in -o requirements.txt # optionally --generate-hashes
socket-patch scan --mode vendored --dry-run --json # exit 0, six: "would_vendor"
socket-patch scan --mode vendored --yes --json # exit 1, pypi_requirement_not_pinned, file unchanged, no .socket/
socket-patch scan --mode hosted --yes --json # exit 0, rewrites only the 1.16.0 line(Run against a local mock patch API serving a patched six 1.16.0 wheel, with --api-url/--patch-server-url/--vendor-url pointing at it.)
The output vendored mode should write already works with uv. Vendoring a file holding only the first line and then appending the 1.17.0 line back gives:
./.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl ; python_full_version < '3.12' # socket-patch vendor: six==1.16.0
six==1.17.0 ; python_full_version >= '3.12'
| uv | uv pip sync on py3.11 |
uv pip sync on py3.12 |
|---|---|---|
| 0.5.31 | six 1.16.0, patched | six 1.17.0 (upstream, correct) |
| 0.12.23 | six 1.16.0, patched | six 1.17.0 (upstream, correct) |
Expected vs actual
- Expected: vendored requirements behave like hosted requirements and vendored pylock: rewrite every exact
==1.16.0line (keeping its marker, as single-line markers already are) and leave lines that pin another version unchanged. At minimum, the refusal should say the package is split across versions (not "not pinned"), and--dry-runshould previewwould_refusewith the same code. - Actual: the wet run refuses with a false "not pinned" message (exit 1), and the dry run previews
would_vendor(exit 0).
Matrix (Linux)
| Input (generated by) | uv 0.5.31 | uv 0.8.17 | uv 0.12.23 |
|---|---|---|---|
--universal requirements.txt, vendored |
fail | fail | fail (×2, plus --generate-hashes with a py3.11 .venv) |
same, --dry-run |
would_vendor (wrong) | would_vendor (wrong) | would_vendor (wrong) |
| same, hosted | – | – | pass (only the 1.16.0 line rewritten; patched install) |
--universal pylock.toml, vendored |
– | – | pass (1.16.0 entry only; py3.11 patched, py3.12 upstream; byte-identical revert) |
| single marker line, vendored | pass | – | pass |
six==1.16.0 ; sys_platform=='linux' + six==1.16.0 ; sys_platform=='win32', vendored |
– | – | pass (both lines rewritten) |
Not OS-specific: the refusal happens in pure planning code before any write. Not bisected: v4.0.0 can't vendor against the harness (no_local_source, see ledger #310).
Suspect code
crates/socket-patch-core/src/vendor/pypi_requirements.rs:85-90:scan_pinssetsfound_rangefor any pin of the same name to another version (six==1.17.0), regardless of marker.crates/socket-patch-core/src/vendor/pypi_requirements.rs:101-110:find_pinletsRangeoutrankExact.crates/socket-patch-core/src/vendor/pypi_requirements.rs:539-548: the "not pinned to =={version}" message.crates/socket-patch-cli/src/commands/scan/vendor_flow.rs:100-146: the dry-run preview has no requirements refusal arm, so it falls through towould_vendor.- Unit test
find_pin_classifies_every_shapecoverssix==1.15.0alone (correctlyRange) but not an exact pin next to another version's pin.
Activity
uv: run 23 ledger (#928)
mikolalysenko commentedon Oct 6, 2026
[agent] Triaged as
priority:p1(uv / requirements.txt, PyPI family). I checked this against currentmain(9c43dfc). Incrates/socket-patch-core/src/vendor/pypi_requirements.rs,scan_pinssetsfound_rangefor any same-name pin to another version, whatever its marker, andfind_pinthen returnsRangeahead ofExact. Sosix==1.17.0 ; python_version >= "3.12"makes the file refuse the exactsix==1.16.0line next to it. I found no duplicate and no open PR for this. It doesn't share a root cause with the other open vendored-requirements issues (#867 is about-rinclude handling), so it stays a cluster of one.Generated by Claude Code
mikolalysenko commentedon Oct 6, 2026
[agent] Claiming this issue (shared root cause: the requirements pin scanner treats a marker-separated pin to another version as an ambiguous range). Branch: agent/fix-requirements-marker-split-pins. Claim-ID: 2026-10-06T09:21:09Z-360a9f
Generated by Claude Code
mikolalysenko commentedon Oct 6, 2026
[agent] Draft fix PR: #929
Generated by Claude Code
Vendor marker-split requirements pins
Add pip e2e for vendoring a marker-split pin
mikolalysenko commentedon Oct 6, 2026
[agent] uv bug-hunt run 24: one more shape with the same root cause, on main
9c43dfc. The marker split can also be spread across a-rinclude, in either direction:-r base.txt)scan --mode vendored --dry-runsix==1.16.0 ; python_full_version < '3.12'six==1.17.0 ; python_full_version >= '3.12'would_vendor, exit 0pypi_requirement_not_pinned"base.txt: six is not pinned to ==1.16.0", nothing writtensix==1.17.0 ; …>= '3.12'six==1.16.0 ; …< '3.12'would_vendor, exit 0plan_requirements(pypi_requirements.rs:524) classifies each file on its own, so a file whose only six line is the other marker branch returnsPinSearch::Rangeand refuses. A fix for #928 should cover the cross-file case too.The hosted → vendored takeover over this split also un-hosts six before the refusal; that's filed separately as #944.
Generated by Claude Code
Test uv and requirements takeovers keep the pin
Keep the hosted pin when a vendored takeover is refused (#853, #944) (#…