Skip to content

Hosted rollback and remove always refuse a pip lock pylock.toml ("no sibling registry package shows the registry and artifact fields"), because pip writes [[packages.wheels]] tables, not uv's inline wheels = [...] #804

Description

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

Summary

pip lock (pip 25.1+) writes a PEP 751 pylock.toml, and pip 26.1+ installs from it with pip install -r pylock.toml. Unlike uv, pip writes each artifact as an array of tables:

[[packages]]
name = "idna"
version = "3.7"

[[packages.wheels]]
name = "idna-3.7-py3-none-any.whl"
url = "https://files.pythonhosted.org/packages/…/idna-3.7-py3-none-any.whl"

[packages.wheels.hashes]
sha256 = "82fe…"

scan --mode hosted handles this file fine. It rewrites six to archive = { url = <hosted wheel>, hashes = … }, and pip 26.2.1 installs the patched wheel from it. But the upstream restore can't read pip's shape: artifact_tables and the shape probe only accept an inline wheels array. Every sibling (here, idna with only files.pythonhosted.org wheels) is then skipped as "not a registry package". So rollback and remove always refuse:

cannot restore pkg:pypi/six@1.16.0 to its upstream registry entry: pylock.toml: no sibling registry package shows the registry and artifact fields this uv release records; restore it from version control instead (`git checkout -- pylock.toml`)

They exit 1 (partial_failure / hosted_revert_failed), and rollback --dry-run predicts the same failure. The same lock with the same siblings written as inline wheels = [{…}] rolls back with exit 0 (control below). This is a new shape, not #407: #407 (fixed) was the uv pip compile pylock that has no index. pip's lock has no index either, and that part is now handled, but its siblings are never recognised.

Impact

On any project that uses pip's own lock format, a hosted patch can be applied but never unpatched with socket-patch rollback / remove, the same class as #410 for requirements.txt. The error message also blames "this uv release" for a file that created-by = "pip" wrote. The hosted → vendored takeover goes through the same upstream restore and is probably refused too; I haven't verified that.

Repro (Linux, main 045d7ec)

The hosted API is a local wiremock: the mount_hosted_api routes from crates/socket-patch-cli/tests/mode_migration_pypi.rs, served by a throwaway, uncommitted driver. The upstream restore uses the real PyPI JSON API.

python3 -m venv v && v/bin/pip install pip==26.2.1     # also: pip==25.1, 25.3, 26.0 (same file shape)
printf 'six==1.16.0\nidna==3.7\n' > requirements.txt
v/bin/pip lock -r requirements.txt -o pylock.toml       # created-by = "pip", [[packages.wheels]] tables
rm requirements.txt
F="--yes --patch-server-url $MOCK"
socket-patch scan --mode hosted --json --api-url $MOCK --org test-org --api-token fake $F
#   exit 0, redirected 1, six -> archive = { url = "$MOCK/patch/pypi/six/1.16.0/…/six-1.16.0-py2.py3-none-any.whl", hashes = … }
python3 -m venv f && f/bin/pip install pip==26.2.1 && f/bin/pip install -r pylock.toml   # installs the patched six (ok)
socket-patch rollback $F                         # exit 1: "no sibling registry package shows the registry and artifact fields this uv release records"
socket-patch remove pkg:pypi/six@1.16.0 $F       # exit 1: hosted_revert_failed, same message
socket-patch rollback --dry-run --json $F        # partial_failure, same message

Control: rewrite the same original lock with inline wheels = [{ name, url, hashes }] for both packages. scan --mode hosted then rollback exits 0 with hosted.reverted: ["pkg:pypi/six@1.16.0"].

Reproduced twice: once on a pip 26.2.1 lock and once on a separate project with a pip 25.1 lock.

Expected vs actual

CLI_CONTRACT.md (the rollback upstream-restore list) refuses only a pylock "whose other registry packages show neither an index nor (as uv pip compile writes them) only PyPI files with none, which restores the entry without an index too". The pip lock's sibling has no index and only PyPI files, so it should restore without an index. PEP 751 allows both TOML spellings of packages.wheels.

Expected: rollback / remove restore six's [[packages.wheels]] entry from PyPI (in the siblings' array-of-tables shape) and exit 0. Or, if that shape isn't supported, scan --mode hosted should refuse it up front instead of writing a patch it can't undo. Actual: the scan succeeds, and every rollback / remove exits 1.

OS × version

OS pip that wrote the lock hosted scan + pip install -r pylock.toml rollback / remove
Linux 26.2.1 (py3.11) pass (pip 26.2.1 installs the patched wheel) fail
Linux 25.1 (py3.11) pass (scan; pip < 26.1 can't install a pylock) fail
Linux 25.3, 26.0 same [[packages.wheels]] shape (not run end to end) expected fail
macOS / Windows — not probed (TOML parsing only, OS-independent) not probed

First bad version

Not bisected. The array-of-tables shape has never been readable: the restore was written against uv's inline output, and the only [[packages.wheels]] fixture in the repo (tests/e2e_vex_lockfile/uv.rs:198) is a vex test.

Suspect code

  • crates/socket-patch-core/src/patch/redirect/upstream/uv.rs:328 artifact_tables: package.get("wheels").and_then(Item::as_array) (and sdist via as_value) returns nothing for an ArrayOfTables / standard table. So pylock_unindexed_registry (:353) returns None for every pip sibling.
  • uv.rs:407-426, the shape probe: same as_array / as_inline_table assumption.
  • uv.rs:442: the resulting refusal, worded for uv.

Activity

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions