Repository navigation
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
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 --revertexits 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-runvendor --revert", can't be followed, because the user changed nothing.remove pkg:pypi/zope-event@5.0exits 1 (vendor_revert_kept). Its remedy, "re-runscan --mode vendoredto normalize, then remove", is a no-op: the re-scan leaves the same mixed entry, andremovefails again.- The hosted takeover (
scan --mode hosted) refuses withredirect_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 thereActual 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.tomlandrequirements.txtboth exist at the first vendored scan, onlypylock.tomlis wired (one record, flavorpython-lock), and the revert works. That matches the documentedpypi_multiple_lockfilesrouting. - A
pylock.tomlmade withpip lock -r requirements.txtafter vendoring holds afile://…/.socket/vendor/…whlarchive, so the second scan wires nothing new (flavor staysrequirements), and the revert keeps the wheel asvendor_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 --revertas restoring each recorded wiring and deleting the artifact.vendor_revert_kept/vendor_lock_entry_driftedare for user edits since vendoring. An entry the CLI itself wired into two files should revert both: requirements.txt back tozope.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_linerecord is classed as drift. Revert exits 0 with nothing restored.removeand 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 onentry.flavoronly (Some("python-lock") => revert_python_locks), so a mixed entry never reachesrevert_requirementsfor itsrequirements_linerecord.crates/socket-patch-core/src/vendor/pypi_lock.rs:1189-1198:revert_python_locksturns every non-lock record intovendor_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 holdsrequirements_linewiring (and emits thepypi_multiple_lockfiles"requirements.txt retain their existing sources" event).
Activity
mikolalysenko commentedon Oct 14, 2026
[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 byentry.flavor, so an entry with flavorpython-lockthat still holds arequirements_linerecord never reachesrevert_requirements. #977 and #668 also touch vendored requirements.txt, but each has a different cause.Generated by Claude Code
mikolalysenko commentedon Oct 14, 2026
[agent] Claiming this issue (shared root cause: a vendored PyPI entry can hold both
requirements_lineandpython_lock_documentrecords, 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-7d15f1Generated by Claude Code
mikolalysenko commentedon Oct 14, 2026
[agent] Draft PR: #1526
Generated by Claude Code
Revert both files of a mixed PyPI vendor entry