Skip to content

Hosted uv rollback and remove refuse when the patched package reaches a dependency group through PEP 735 include-group #473

Description

[agent] Found by the scheduled uv bug-hunt routine (ledger #310).

Summary

Hosted rollback and remove can't restore a uv project whose patched package reaches a dependency group through a PEP 735 { include-group = "…" } member. uv's lock expands included groups, so the hosted rewrite repoints six's entry in both groups' requires-dev arrays. But the restore only reads the group's own string members from pyproject.toml. For the including group it finds no declaration and refuses: pyproject.toml no longer declares six. The hosted pin stays, and every later install keeps the patched wheel.

Vendored mode handles the same project: vendor --revert is byte-identical.

Impact

dev = [{ include-group = "test" }, { include-group = "lint" }] is the usual PEP 735 layout, and uv has supported it since dependency groups landed. In any such project, a hosted patch on a package from an included group can't be unwound with rollback or remove. Both exit 1 with the "restore it from version control" refusal, although nothing about the project changed after the scan.

Repro (uv 0.8.17, Linux, main 6e7ef74)

Uses a local mock of the patch API serving a patched six-1.16.0 wheel (the same mock as the earlier uv issues, embedded in the probe workflow below), plus SOCKET_PYPI_JSON_API pointing at a pypi.org forwarder.

cat > pyproject.toml <<'TOML'
[project]
name = "uvp"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["python-dateutil==2.8.2"]

[dependency-groups]
test = ["six==1.16.0"]
dev = ["idna==3.7", {include-group = "test"}]
TOML
uv lock && git init -q && git add -A && git commit -qm init
socket-patch scan --mode hosted --json --yes $API        # redirected: 1, uv sync --locked --all-groups installs the patch
socket-patch rollback --json --yes $API                 # exit 1, partial_failure
socket-patch remove pkg:pypi/six@1.16.0 --json --yes $API  # exit 1, hosted_revert_failed

Output (rollback and remove alike):

cannot restore pkg:pypi/six@1.16.0 to its upstream registry entry: uv.lock: pyproject.toml no longer declares six, so the lock entry's specifier is not derivable; restore it from version control instead (`git checkout -- uv.lock`)

After the hosted scan, the lock repoints both the test entry and the expanded dev entry:

[package.metadata.requires-dev]
dev = [
    { name = "idna", specifier = "==3.7" },
    { name = "six", url = "http://127.0.0.1:18080/patch/pypi/six/…/six-1.16.0-py2.py3-none-any.whl" },
]
test = [{ name = "six", url = "…" }]

Controls: the same project with six listed directly in a group (no include-group), and with [tool.uv] default-groups = ["dev", "lint"], both roll back byte-identically. A self-referencing extra (all = ["uvp[test]"]) also rolls back fine.

Expected vs actual

  • Expected: CLI_CONTRACT.md, "Hosted unwind coverage" (pypi): "Every file wiring the pin is rewritten back to the DEFAULT UPSTREAM registry entry … the root package's requires-dist / requires-dev entries … re-derived from the paired metadata's declarations". The declaration exists. It's reachable through include-group, as uv itself resolves it. The refusal list (non-pure wheels, file filters, registry ambiguity, [[distribution]]) doesn't include this case.
  • Actual: refused, exit 1. uv.lock and pyproject.toml keep the hosted URL, and uv sync --locked --all-groups keeps installing the patched bytes.

OS × uv matrix

uv 0.5.31 uv 0.8.17 uv 0.12.21
Linux (sandbox), rollback ❌ ❌ (rollback, remove) ❌
Linux (sandbox), vendored revert – ✅ –
ubuntu-latest (probe): rollback / remove / vendored ❌ / ❌ / ✅ – ❌ / ❌ / ✅
macos-latest (probe): rollback / remove / vendored ❌ / ❌ / ✅ – ❌ / ❌ / ✅
windows-latest (probe): rollback / remove / vendored ❌ / ❌ / ✅ – ❌ / ❌ / ✅

First bad

Hosted upstream restore arrived in v5 (#277). The 4.0.0 release predates it, so there's nothing earlier to bisect.

Suspect code

crates/socket-patch-core/src/patch/redirect/upstream/uv.rs:716-725: the Declared::Dev(group) arm returns strings(dependency-groups[group]), which skips table members. It should expand { include-group = "<g>" } recursively, as PEP 735 and uv do. The vendored classifier also skips include-group members (vendor/pypi_uv.rs:274), but there it doesn't block the revert.

Probe: https://github.com/SocketDev/socket-patch/actions/runs/36878842786 (ubuntu, macos, windows × uv 0.5.31 / 0.12.21, all jobs print the same result)

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions