Skip to content

Fix vendored requirements.txt re-vendor to a superseding patch (#765) - #766

Open
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
mainfrom
agent/fix-pypi-requirements-revendor
Open

Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
mainfrom
agent/fix-pypi-requirements-revendor

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #765

Summary

A requirements.txt project vendored with one patch can now move to a newer patch for the same package. Before this, vendor, get <uuid> --mode vendored and scan --mode vendored all failed with pypi_requirements_already_vendored (exit 1, partial_failure). requirements.txt, the old uuid dir and the ledger stayed on the old patch, so pip kept installing it. Meanwhile --dry-run previewed would_revendor and CLI_CONTRACT said the package "is still re-vendored automatically".

Root cause

preflight_requirements (vendor/pypi_requirements.rs) treated a socket-patch vendor line for the package at a different patch uuid as a hard refusal. The CLI's re-vendor path (record_vendor_entry → sweep_stale_artifact) and the dry-run preview both expect the backend to re-wire in place, as npm, cargo, gem and pnpm do.

All the vendored PyPI writers have the same gap: uv (pypi_uv_source_already_exists, the vendored half of #742), Hatch (vendored half of #650), Poetry, PDM and Pipenv. This PR is the requirements.txt slice only. The other lanes are follow-ups. #742 and #650 stay open, and their hosted halves are in #743.

Fix

  • New RequirementsTarget::Rewire. When requirements.txt already routes the package to an older uuid and the ledger holds exactly one pypi entry at that uuid whose records are all requirements_lines tagged name==version, the pre-flight plans a re-wire from that entry's own records (plan_rewire):
    • Each recorded vendor line is replaced in place by the new vendor line, keeping its environment marker, the tree's hash mode and the (transitive) note.
    • The new record keeps the old record's file, key, action and pre-vendor original. So vendor --revert of the new entry still restores the user's own pin byte for byte, and carry_forward_wiring has nothing to fill in.
  • The re-vendor is still refused, before anything is written, when:
    • there is no such ledger entry (nothing records the pre-vendor original, so a re-wire could never be reverted),
    • a recorded line has drifted,
    • a recorded file has left the tree or the project root,
    • or the tree holds a vendor line for the package that the ledger did not record.
  • The write loop is factored into write_plan and shared by wire_requirements and rewire_requirements. It keeps the symlink refusal and the partial-write unwind.
  • The CLI is unchanged. The ledger entry is replaced, and the old uuid dir is removed (vendor_stale_artifact_removed).

Tests (red → green)

Issue Test Before fix After
#765 (CLI end to end) crates/socket-patch-cli/tests/mode_migration_pypi.rs::requirements_vendored_revendors_superseding_patch: vendor A, switch the manifest to B (different patched bytes), vendor, then re-run (in sync), then vendor --revert. Covers an unhashed tree and a hashed tree with a marker and a \ continuation FAIL: exit 1 partialFailure / pypi_requirements_already_vendored pass: exit 0, line on B, A's dir removed (vendor_stale_artifact_removed), ledger on B, revert restores the original bytes
#765 (core, every shape) vendor::pypi::tests::requirements_superseding_uuid_revendors_in_place: unhashed; hashed + marker + continuation + CRLF; a pin in a -r include; an appended transitive line. Checks that originals carry over and that revert of the new entry is byte-exact FAIL: Refused { pypi_requirements_already_vendored } pass
guard vendor::pypi::tests::requirements_superseding_uuid_without_ledger_refuses (replaces requirements_stale_uuid_vendor_line_refuses) – pass: still refused, nothing written
guard vendor::pypi::tests::requirements_superseding_uuid_drifted_line_refuses – pass: refused, nothing written

Local runs:

  • cargo fmt --all -- --check: clean.
  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo test --workspace --all-features --no-fail-fast: 9727 passed, 12 failed, 253 ignored. All 12 failures are chmod-denial tests that cannot fail under root, which is what this sandbox runs as: *_write_failure_*, *unremovable*, relax_loop_must_not_traverse_symlinked_root, wire_failure_rolls_back_already_written_files and others. Re-run as the unprivileged nobody user, every one of them passes. That includes the requirements write-unwind test that write_plan now serves.
  • mode_migration_pypi: 13/13.
  • The npm, pypi and gem wrappers are untouched (the change is in the Rust vendor backend only).

Follow-ups (not in this PR)

🤖 Generated with Claude Code


Note

Medium Risk
Touches PyPI requirements vendoring and on-disk requirements edits with ledger-driven revert semantics; mistakes could break pins or revert, but scope is limited to requirements.txt and guarded by preflight checks plus broad new tests.

Overview
Fixes #765 for requirements.txt vendoring: when the manifest moves a package from patch A to patch B, vendor (and vendored-mode flows that call the same backend) now re-wires existing socket vendor lines in place instead of failing with pypi_requirements_already_vendored.

Preflight in pypi_requirements gains RequirementsTarget::Rewire: if the tree still points at an older patch uuid but the vendor ledger has exactly one matching pypi entry, it plans an in-place swap via new rewire_requirements / plan_rewire, preserving markers, hash mode, -r includes, and pre-vendor original bytes so vendor --revert on the new entry still restores the user’s pins. Shared file writes go through write_plan.

Re-vendor is still refused (unchanged error family) when the ledger is missing, lines drifted from what was recorded, or live vendor lines don’t match the ledger.

The PyPI orchestrator adds WiringPlan::RequirementsRewire; stale patch A artifacts continue to be reclaimed (vendor_stale_artifact_removed). CHANGELOG documents the behavior. uv / Poetry / PDM / Pipenv superseding-patch re-vendor is explicitly out of scope here.

Tests: CLI requirements_vendored_revendors_superseding_patch and core cases for multiple requirement shapes, drift, and missing ledger.

Reviewed by Cursor Bugbot for commit 2fad4d0. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
A requirements.txt project vendored with one patch could not move to a
newer patch for the same package: vendor, get --mode vendored and scan
--mode vendored failed with pypi_requirements_already_vendored and pip
kept installing the old patch, while --dry-run previewed a re-vendor.

The requirements pre-flight now re-wires the vendor lines the older
patch's ledger entry recorded, in place, to the new wheel. Each line
keeps its marker, hash mode and transitive note, and each record keeps
the pre-vendor original, so vendor --revert still restores the user's
pin. Without that ledger entry, or when a recorded line has drifted,
the re-vendor is still refused before anything is written. (#765)

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 4, 2026 09:11
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

The previous commit ran rustfmt across the whole workspace and
reformatted 129 files that the requirements.txt re-vendor fix does not
touch. That churn hides the real change from reviewers and conflicts
with nearly every other open PR. Restore those files to main so the PR
only carries the fix: CHANGELOG, vendor/pypi.rs,
vendor/pypi_requirements.rs and the mode_migration_pypi test.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 2fad4d0. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 4, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at 2fad4d0.

  • What changed this round: the earlier head 3f4ef08 ran rustfmt across the whole workspace and reformatted 129 unrelated files. 2fad4d0 restores those files to main. The PR is now 4 files (+516/−24): CHANGELOG.md, vendor/pypi.rs, vendor/pypi_requirements.rs and tests/mode_migration_pypi.rs. The fix code itself is unchanged.
  • CI: 485/485 checks green on 2fad4d0.
  • Bugbot: reviewed 2fad4d0 and found no issues. There are no unresolved review threads.
  • Local: vendor::pypi* unit tests and mode_migration_pypi (13/13) pass. Two chmod-based write-failure tests fail only when run as root (the sandbox); CI passes them.
  • For the reviewer: look at plan_rewire and its refusal cases in vendor/pypi_requirements.rs.

Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

2 participants