Skip to content

Vendored requirements.txt can't be reverted or removed once a later scan also wires a pylock.toml: the entry flips to flavor python-lock, whose revert calls its own requirements_line wiring "unexpected" drift (exit 0, nothing restored) #1524

Closed

Description

[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).

Summary

A plain-pip project vendors zope.event==5.0 through requirements.txt. Later the user adds a pylock.toml (for example pip lock . from pyproject.toml, pip ≥ 25.1) and runs scan --mode vendored again. That scan wires pylock.toml too. It appends a python_lock_document record to the same ledger entry and switches the entry's flavor from requirements to python-lock, but it keeps the existing requirements_line record:

pkg:pypi/zope-event@5.0  flavor=python-lock  wiring=[(pylock.toml, python_lock_document), (requirements.txt, requirements_line)]

From then on, every unwind path dispatches on the flavor to pypi_lock::revert_python_locks. That function treats any record that isn't a Python-lock record as vendor_lock_entry_drifted: unexpected Python wiring file or kind: requirements.txt (requirements_line). So the whole entry is drift-kept:

  • vendor --revert exits 0 with "status": "success", but restores neither file and keeps the wheel (vendor_lock_entry_drifted + vendor_artifact_kept + vendor_revert_kept). Its remedy, "undo the drift and re-run vendor --revert", can't be followed, because the user changed nothing.
  • remove pkg:pypi/zope-event@5.0 exits 1 (vendor_revert_kept). Its remedy, "re-run scan --mode vendored to normalize, then remove", is a no-op: the re-scan leaves the same mixed entry, and remove fails again.
  • The hosted takeover (scan --mode hosted) refuses with redirect_vendored_revert_failed ("part of its vendored wiring was edited since vendoring").

This happens to every package in the entry set, not only one. In my 4-package repro all four were stuck.

The second scan's event also misdescribes what it did: pypi_multiple_lockfiles "wiring pylock.toml; installs driven by requirements.txt retain their existing sources". In fact requirements.txt stays vendored and is still recorded in the entry.

Impact

The project can't leave vendored mode or drop the patch through any CLI command. vendor --revert reports success while nothing changes (exit 0), so CI scripts that revert and re-vendor silently keep the old wiring. Recovery means hand-editing both files, .socket/vendor/state.json and the wheel directory.

Repro (Linux, pip 26.2.1 / CPython 3.13, main 313d532; also v5.0.1)

The patch API / vendoring service is a local mock that grants a patched copy of the real zope.event-5.0-py3-none-any.whl, with consistent canonical purls (pkg:pypi/zope-event@5.0) and sha512 SRI integrity. $M is its URL. sp is socket-patch … --api-url $M --org test-org --api-token fake --patch-server-url $M.

mkdir app && cd app && git init -q
uv venv -q --seed -p python3.13 .venv && .venv/bin/pip install -q pip==26.2.1
echo 'zope.event==5.0' > requirements.txt
cat > pyproject.toml <<'EOF'
[project]
name = "app"
version = "0.1"
dependencies = ["zope.event==5.0"]
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
EOF
.venv/bin/pip install -q --no-deps -r requirements.txt
VIRTUAL_ENV=$PWD/.venv sp scan --mode vendored --vendor-url $M --yes --json --ecosystems pypi   # wires requirements.txt (flavor requirements)
git add -A && git commit -qm vendored
.venv/bin/pip lock -q . -o pylock.toml                                                            # PyPI wheel for zope-event 5.0
VIRTUAL_ENV=$PWD/.venv sp scan --mode vendored --vendor-url $M --yes --json --ecosystems pypi   # also wires pylock.toml; flavor -> python-lock
python3 -c "import json;[print(e['basePurl'],e['flavor'],[(w['file'],w['kind']) for w in e['wiring']]) for e in json.load(open('.socket/vendor/state.json'))['entries'].values()]"
VIRTUAL_ENV=$PWD/.venv sp vendor --revert --vendor-url $M --yes --json; echo "exit $?"
git status --short    # requirements.txt / pylock.toml still vendored, .socket/vendor/pypi/<uuid>/ still there

Actual output of the revert (abridged):

exit 0
"status": "success"
skipped  vendor_lock_entry_drifted  unexpected Python wiring file or kind: requirements.txt (requirements_line)
skipped  vendor_artifact_kept       kept .socket/vendor/pypi/aaaaaaaa-…-000000000004: some recorded lock entries were left alone …
skipped  vendor_revert_kept         lock entries drifted since vendoring; artifacts and ledger entry kept — undo the drift and re-run `vendor --revert` to finish

Reproduced twice on main (a 4-package project with typing_extensions, ruamel.yaml, backports.tarfile and zope.event, and the one-package project above) and once on the v5.0.1 release binary.

Controls:

  • If pylock.toml and requirements.txt both exist at the first vendored scan, only pylock.toml is wired (one record, flavor python-lock), and the revert works. That matches the documented pypi_multiple_lockfiles routing.
  • A pylock.toml made with pip lock -r requirements.txt after vendoring holds a file://…/.socket/vendor/…whl archive, so the second scan wires nothing new (flavor stays requirements), and the revert keeps the wheel as vendor_revert_residual_reference. That's the fail-safe keep, not this bug.
  • Without the pylock, the same project reverts byte for byte.

Expected vs actual

  • Expected: CLI_CONTRACT.md defines vendor --revert as restoring each recorded wiring and deleting the artifact. vendor_revert_kept / vendor_lock_entry_drifted are for user edits since vendoring. An entry the CLI itself wired into two files should revert both: requirements.txt back to zope.event==5.0, pylock.toml back to its PyPI wheel table, and the wheel deleted. Alternatively, the second scan shouldn't add a record whose kind the entry's new flavor can't unwind.
  • Actual: the CLI's own requirements_line record is classed as drift. Revert exits 0 with nothing restored. remove and the hosted takeover refuse, and every printed remedy loops.
OS pip / Python reproduces
Linux 26.2.1 / 3.13 (main 313d532) yes (twice)
Linux 26.2.1 / 3.13 (release v5.0.1) yes
macOS / Windows — untested (probe branches are blocked for this routine); the code path is OS-independent

The first bad commit wasn't bisected. It's present in v5.0.1. The pylock.toml co-wiring beside requirements.txt came with the #1368 work (#1390 / #1399).

Suspect code

  • crates/socket-patch-core/src/vendor/pypi.rs:3196: the revert dispatches on entry.flavor only (Some("python-lock") => revert_python_locks), so a mixed entry never reaches revert_requirements for its requirements_line record.
  • crates/socket-patch-core/src/vendor/pypi_lock.rs:1189-1198: revert_python_locks turns every non-lock record into vendor_lock_entry_drifted ("unexpected Python wiring file or kind").
  • The vendored scan path that appends the pylock record and restamps flavor = "python-lock" on an entry that already holds requirements_line wiring (and emits the pypi_multiple_lockfiles "requirements.txt retain their existing sources" event).

Activity

  1. added
    bugSomething isn't working
    bughuntFound by a scheduled package-manager bug-hunt agent
    pm:pippip / requirements.txt
    on Oct 14, 2026
  2. mikolalysenko commented on Oct 14, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (pip / requirements.txt, PyPI family). Not a duplicate. The suspect dispatch is confirmed on main 6a06ddd: revert_pypi_opts (vendor/pypi.rs:3194) picks one revert by entry.flavor, so an entry with flavor python-lock that still holds a requirements_line record never reaches revert_requirements. #977 and #668 also touch vendored requirements.txt, but each has a different cause.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 14, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: a vendored PyPI entry can hold both requirements_line and python_lock_document records, but its revert is dispatched by one flavor, so the other flavor's records are treated as drift). Branch: agent/fix-pypi-mixed-flavor-revert. Claim-ID: 2026-10-14T08:21:27Z-7d15f1


    Generated by Claude Code

  4. mikolalysenko commented on Oct 14, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1526


    Generated by Claude Code

  5. added a commit that references this issue on Oct 14, 2026
    5dc58c2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentpm:pippip / requirements.txtpriority:p1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions