Repository navigation
Fix extras pin un-hosted beside a pylock (#1794) - #1796
Mikola Lysenko (mikolalysenko) wants to merge 5 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
A pip project whose requirements.txt pins a package with extras (six[foo]==1.16.0) beside the pylock.toml `pip lock` wrote from it lost its patch on a hosted -> vendored switch. The switch un-hosted requirements.txt, wired only the pylock (a wheel path line can't carry extras), warned that requirements.txt "retains its existing source", and exited 0, so `pip install -r requirements.txt` went back to the unpatched PyPI release and `vendor --check` then failed. Vendored mode now refuses such a package with pypi_extras_unsupported, as it already does for the same pin with no lock beside it. The switch rolls its restore back, so both files keep their hosted pins and the run exits 1 with the reason. The same applies to a requirements.txt exported beside Pipfile.lock or uv.lock, which is wired with its lock the same way. Fixes #1794 Assisted-by: Claude Code:claude-opus-5-5
The first cut refused a package whenever requirements.txt pinned it with extras beside a pylock, Pipfile.lock or uv.lock. That also failed every re-run of a project whose lock was already vendored, and blocked a newer patch from replacing it. The refusal now applies only when the lock is about to be wired fresh; an already-wired lock re-runs in sync and the extras pin is still named as a file that installs unpatched. It also now catches an `===` pin, a pin in an -r include, and a UTF-16 requirements.txt, and skips a file that already carries a vendored line for the package. Refs #1794 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
bugbot run Generated by Claude Code |
|
[agent] Generated by Claude Code |
|
bugbot run Generated by Claude Code |
|
bugbot run Generated by Claude Code |
Hosted mode rewrites a requirements file's one unpinned line for the package too (six[foo] -> six[foo] @ ...), so after a hosted -> vendored switch the takeover restore un-hosts that line as well. fresh_sibling_pin only refused exact ==/=== extras pins, so it wired the lock alone and pip install -r went back to unpatched bytes, the #1794 failure. Count the file's sole unpinned extras line as the hosted-rewritable pin too. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011f9aeRBd9SafVxvTmiMFa7
|
bugbot run Generated by Claude Code |
|
Ready for review at
Generated by Claude Code |
8eafd68 also refuses a file's only line for the package when it is an unpinned extras requirement (six[foo]), since hosted mode rewrites that line too, but the pypi_multiple_lockfiles row only named the exact six[foo]==1.16.0 pin. Name both shapes so the contract matches the code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
|
Final review of I pushed b6e2d88, a docs-only change. CLI_CONTRACT.md now also names the lone unpinned Notes for the approver (non-blocking):
Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b6e2d88. Configure here.
|
Burn-down agent: labeled Ready for review at
Generated by Claude Code |
|
Final review: one product question before this can be briefed for approval. The fix is motivated by the hosted → vendored takeover (#1794), but Concrete case: a plain project that was never hosted, with Question: should the extras refusal apply only when the requirements pin was hosted (the takeover case this PR fixes), or to every fresh vendored wiring, as now? If takeover-only, the narrowing would be to refuse only when the restore just un-hosted that line, or at least to drop the unpinned-extras refusal outside a takeover. The exact Everything else checks out: CI green on b6e2d88, Bugbot clean, CHANGELOG.md untouched. I set Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #1794
Summary
A pip project whose
requirements.txtpins a package with extras (six[foo]==1.16.0) beside thepylock.tomlthatpip lockwrote lost its patch on a hosted → vendored switch (plainvendoreject orscan --mode vendored). The switch un-hostedrequirements.txt, wired only the pylock, saidrequirements.txt"retains its existing sources", and exited 0.pip install -r requirements.txtthen installed unpatched six, andvendor --checkfailed straight afterwards.Vendored mode now refuses that package with
pypi_extras_unsupported, the code the requirements-only wiring already uses for the same pin. The takeover rolls its restore back, so both files keep their hosted pins byte for byte and the run exits 1 with the reason. This matches the contract's "a takeover the vendored backend does not carry through keeps the hosted pin".Root cause
#1759co-wires a requirements pin with the lock only whensibling_pin_wirableaccepts it. A pin it rejects fell through as apypi_multiple_lockfiles"loser". That is accurate for a fresh project, but wrong after a takeover, because the upstream restore has just rewritten the file's source. An exact pin with extras is the one shape hosted mode pins (six[foo] @ https://…) that the vendored wiring can't express.Change
pypi_requirements::fresh_sibling_pin: wrapssibling_pin_wirablefor a lock about to be wired fresh, and refuses withpypi_extras_unsupportedwhen the requirements set (the root file or an in-root-rinclude) pinsname==versionorname===versionwith extras, or whose only line for the package is an unpinned extras requirement (six[foo]), which hosted mode also rewrites. A UTF-16 root file that the planner can't read but pip can is checked throughutils::requirements::decode. A set that already carries a vendor line for the package is left as before.PinScanrecordsexact_extras, so the check reusesscan_pinsinstead of parsing the file a second time.requirements.txtwith the lock (Vendored mode in a Pipenv project wires only Pipfile.lock and silently leaves a sibling requirements.txt unpatched, and the hosted → vendored takeover reverts that file's hosted pin to plain PyPI #612, Vendored mode leaves a Pipenv-written pylock.toml or a uv-export requirements.txt unpatched beside the wired lock #1368). An in-sync re-run and a superseding patch keep the old loser behavior, so a project vendored before this change doesn't start failing.pypi_multiple_lockfilesrow documents the refusal, both the exact and the lone unpinned extras shape (b6e2d88).Tests (red → green)
scan --mode vendoredover hostedsix[foo]==1.16.0+ pylockmode_migration_pypi::pip_lock_hosted_to_vendored_refuses_an_extras_pinappliedpypi_extras_unsupported, both files byte-identical to hosted, nothing vendoredmode_migration_pypi::pip_lock_rerun_keeps_a_wired_lock_beside_an_extras_pin==/===/ normalized name and markers /-rinclude / UTF-16 refuse; other version, range, wirable pin, already vendored don'tpypi_requirements::tests::fresh_sibling_pin_refuses_an_exact_extras_pinCommands run locally on the branch:
cargo fmt --all -- --check: cleancargo clippy --workspace --all-features -- -D warnings: cleancargo test -p socket-patch-cli --all-features --test mode_migration_pypi: 76 passedcargo test --workspace --all-features: 15197 passed, 11 failed, all environmental: 9 read-only-directory tests that cannot fail as root in this container (copy_tree,vlt_heal,pypi_poetry/pypi_requirementswire-failure rollback,in_process_redirect,repair,vlt), and 2e2e_bun_lockbtests that need patches-api.socket.dev, which this sandbox can't reach. None of them touch the changed paths.cargo test -p socket-patch-cli --all-features --test e2e_vendor_pypi_build -- --include-ignored(uv 0.11.32): 56 passedNo wrapper changes are needed.
npm/,pypi/andgem/only dispatch to the binary./code-review high
I ran it on the first cut and fixed these findings in 2d6811c: a refusal on an in-sync re-run or supersede,
===pins missed, UTF-16 files missed, the vendor-line skip, the refusal coming before the lock's own guards, and duplicated parsing. Not changed: on a fresh wiring the requirements tree is now read twice (fresh_sibling_pin, thensibling_pin_wirable). That happens once per vendored PyPI package, on small files, so I left the extra read rather than restructuresibling_pin_wirable.Follow-ups
requirements.txtat all, so an extras pin beside them is still a loser after a takeover, as is any plain exact pin. That gap predates this PR and belongs to the broader "wire exports for every tool lock" work.https://claude.ai/code/session_011f9aeR8BmvdRuS4i2YzgEV
Note
Medium Risk
Changes PyPI vendored wiring and hosted→vendored takeover failure modes, but behavior is narrow (extras-shaped requirements pins), guarded by new tests, and in-sync projects are explicitly preserved.
Overview
Fixes hosted → vendored takeover when
requirements.txtbeside apylock.toml/ uv / Pipenv lock pins the same release with extras (e.g.six[foo]==1.16.0) or has a single unpinned extras line (six[foo]). Vendored wheel lines cannot express extras, so the switch used to un-host the requirements pin, wire only the lock, exit 0, and leavepip install -ron unpatched bytes.Fresh lock wiring now goes through
fresh_sibling_pin, which fails the package withpypi_extras_unsupported(same as requirements-only wiring) so the takeover rolls back and both files keep their hosted pins.PinScan.exact_extrasdrives that check (including UTF-16 roots the planner cannot read). In-sync re-runs and supersede paths still treat an extras sibling as apypi_multiple_lockfilesloser, so projects already vendored before this change do not start failing.CLI_CONTRACT.md documents the refusal; integration and unit tests cover hosted→vendored refusal, in-sync re-run, and pin-shape edge cases.
Reviewed by Cursor Bugbot for commit b6e2d88. Configure here.
Generated by Claude Code